From b83d49cdea4b7a24c343c8bcb0918af7f7c1919d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 4 Apr 2025 19:00:35 +0500 Subject: [PATCH 001/151] 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 002/151] 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 003/151] 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 004/151] 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 005/151] 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 006/151] 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 007/151] 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 008/151] 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 009/151] 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 010/151] 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 011/151] 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 012/151] 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 013/151] 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 014/151] 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 015/151] 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 016/151] 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 017/151] 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 018/151] 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 019/151] 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 020/151] 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 021/151] 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 022/151] 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 023/151] 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 024/151] 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 025/151] 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 026/151] 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 027/151] 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 028/151] 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 029/151] 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 030/151] 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 031/151] 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 032/151] 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 033/151] 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 034/151] 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 035/151] 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 036/151] 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 037/151] 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 038/151] 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 039/151] 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 040/151] 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 041/151] 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 042/151] 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 043/151] 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 044/151] 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 045/151] 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 046/151] 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 047/151] 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 048/151] 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 049/151] 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 050/151] 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 051/151] 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 052/151] 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 053/151] 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 054/151] 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 055/151] 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 056/151] 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 057/151] 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 058/151] 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 059/151] 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 060/151] 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 061/151] 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 062/151] 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 063/151] 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 064/151] 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 065/151] 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 066/151] 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 067/151] 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 068/151] 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 069/151] 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 070/151] 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 071/151] 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 072/151] 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( From 2779972795ca607ef5181043c41317977eb06b19 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 13:55:17 +0000 Subject: [PATCH 073/151] 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 40622899bf..ec47aff56d 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-1039" +tangemBlockchainSdk = "develop-1034" #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 af5501fb9b9b21563847de5569fa63f598c46527 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 19:22:02 +0500 Subject: [PATCH 074/151] Updated on 2026-08-14 --- .../di/domain/WalletConnectDomainModule.kt | 8 +++ .../connections/entity/WcConnectionsState.kt | 2 +- .../connections/model/WcConnectionsModel.kt | 54 ++++++++++++++++--- .../transformers/WcSessionsTransformer.kt | 38 +++++++++++++ .../connections/ui/WcConnectionsContent.kt | 10 ++-- .../ui/preview/WcConnectionsPreviewData.kt | 26 ++------- .../configurations/KotlinConfigurations.kt | 10 ++-- 7 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt index e542ddc5fd..fd5d2491d3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletConnectDomainModule.kt @@ -2,6 +2,8 @@ package com.tangem.tap.di.domain import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase import com.tangem.domain.walletconnect.repository.WalletConnectRepository +import com.tangem.domain.walletconnect.repository.WcSessionsManager +import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,4 +21,10 @@ internal object WalletConnectDomainModule { ): CheckIsWalletConnectAvailableUseCase { return CheckIsWalletConnectAvailableUseCase(walletConnectRepository = walletConnectRepository) } + + @Provides + @Singleton + fun providesWcSessionsUseCase(sessionsManager: WcSessionsManager): WcSessionsUseCase { + return WcSessionsUseCase(sessionsManager) + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt index 62474e35b9..0eb7c4e19b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectionsState.kt @@ -14,7 +14,7 @@ internal data class WcConnectionsState( internal data class WcConnectionsTopAppBarConfig( val startButtonUM: TopAppBarButtonUM, - val endActions: ImmutableList, + val disconnectAllItem: TangemDropdownMenuItem, ) internal data class WcConnectionsUM( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index ada41bf7d8..a0b6c6aa91 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -6,15 +6,22 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import com.tangem.features.walletconnect.connections.entity.WcConnectionsState -import com.tangem.features.walletconnect.connections.ui.preview.WcConnectionsPreviewData +import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig +import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsTransformer import com.tangem.features.walletconnect.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.transformer.update +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* import timber.log.Timber import javax.inject.Inject @@ -24,19 +31,16 @@ internal class WcConnectionsModel @Inject constructor( private val router: Router, private val uiMessageSender: UiMessageSender, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, + private val wcSessionsUseCase: WcSessionsUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { - val uiState: StateFlow = MutableStateFlow( - WcConnectionsPreviewData.stateForModel( - onBackClicked = router::pop, - openQrScanScreen = ::openQrScanScreen, - disconnectAll = ::disconnectAll, - ), - ).asStateFlow() + val uiState: StateFlow + field = MutableStateFlow(getInitialState()) init { listenQrUpdates() + listenWcSessions() } private fun listenQrUpdates() { @@ -49,6 +53,21 @@ internal class WcConnectionsModel @Inject constructor( .launchIn(modelScope) } + private fun listenWcSessions() { + wcSessionsUseCase.invoke() + .conflate() + .distinctUntilChanged() + .onEach { sessionsMap -> + uiState.update( + WcSessionsTransformer( + sessionsMap = sessionsMap, + openAppInfoModal = { Timber.d("Open app info for session: $it") }, + ), + ) + } + .launchIn(modelScope) + } + private fun openQrScanScreen() { router.push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.WalletConnect)) } @@ -69,4 +88,23 @@ internal class WcConnectionsModel @Inject constructor( ) uiMessageSender.send(message) } + + private fun getInitialState(): WcConnectionsState { + return WcConnectionsState( + topAppBarConfig = WcConnectionsTopAppBarConfig( + startButtonUM = TopAppBarButtonUM( + iconRes = R.drawable.ic_back_24, + onIconClicked = router::pop, + enabled = true, + ), + disconnectAllItem = TangemDropdownMenuItem( + title = resourceReference(R.string.wc_disconnect_all), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = ::disconnectAll, + ), + ), + connections = persistentListOf(), + onNewConnectionClick = ::openQrScanScreen, + ) + } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt new file mode 100644 index 0000000000..1f3b8a9ebc --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsTransformer.kt @@ -0,0 +1,38 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfo +import com.tangem.features.walletconnect.connections.entity.WcConnectionsState +import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toPersistentList + +internal class WcSessionsTransformer( + private val sessionsMap: Map>, + private val openAppInfoModal: (WcSession) -> Unit, +) : Transformer { + + override fun transform(prevState: WcConnectionsState): WcConnectionsState { + return prevState.copy( + connections = sessionsMap.map { (wallet, sessions) -> + WcConnectionsUM( + userWalletId = wallet.walletId.stringValue, + walletName = wallet.name, + connectedApps = sessions.map { session -> + with(session.sdkModel) { + WcConnectedAppInfo( + name = appMetaData.name, + iconUrl = appMetaData.icons.firstOrNull().orEmpty(), + subtitle = stringReference(appMetaData.description), + isVerified = false, + onClick = { openAppInfoModal(session) }, + ) + } + }.toPersistentList(), + ) + }.toPersistentList(), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt index 005ff5c7a4..e529ec9d02 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectionsContent.kt @@ -259,12 +259,10 @@ private fun ConnectionsTopBar( modifier = Modifier.background(TangemTheme.colors.background.primary), onDismissRequest = { showDropdownMenu = false }, content = { - config.endActions.fastForEach { - TangemDropdownItem( - item = it, - dismissParent = { showDropdownMenu = false }, - ) - } + TangemDropdownItem( + item = config.disconnectAllItem, + dismissParent = { showDropdownMenu = false }, + ) }, ) }, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt index 57ce635a39..31759bb4d4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/preview/WcConnectionsPreviewData.kt @@ -10,7 +10,6 @@ import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppB import com.tangem.features.walletconnect.connections.entity.WcConnectionsUM import com.tangem.features.walletconnect.impl.R import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList import timber.log.Timber internal object WcConnectionsPreviewData { @@ -111,31 +110,14 @@ internal object WcConnectionsPreviewData { onIconClicked = {}, enabled = true, ), - endActions = persistentListOf( - TangemDropdownMenuItem( - title = resourceReference(R.string.wc_disconnect_all), - textColorProvider = { TangemTheme.colors.text.warning }, - onClick = { }, - ), + disconnectAllItem = TangemDropdownMenuItem( + title = resourceReference(R.string.wc_disconnect_all), + textColorProvider = { TangemTheme.colors.text.warning }, + onClick = { }, ), ), connections = persistentListOf(), onNewConnectionClick = {}, ) val fullState = stateWithEmptyConnections.copy(connections = connections) - fun stateForModel( - onBackClicked: () -> Unit, - openQrScanScreen: () -> Unit, - disconnectAll: () -> Unit, - ): WcConnectionsState { - return fullState.copy( - topAppBarConfig = fullState.topAppBarConfig.copy( - startButtonUM = fullState.topAppBarConfig.startButtonUM.copy(onIconClicked = onBackClicked), - endActions = fullState.topAppBarConfig.endActions - .map { it.copy(onClick = disconnectAll) } - .toImmutableList(), - ), - onNewConnectionClick = openQrScanScreen, - ) - } } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt index ae7e54965a..1ff0e4258f 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/KotlinConfigurations.kt @@ -1,15 +1,15 @@ package com.tangem.plugin.configuration.configurations import org.gradle.api.Project -import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.dsl.JvmTarget -import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension -import org.jetbrains.kotlin.gradle.model.KotlinAndroidExtension +import org.jetbrains.kotlin.gradle.dsl.kotlinExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile internal fun Project.configureKotlinCompilerOptions() { + kotlinExtension.sourceSets.all { + // https://github.com/Kotlin/KEEP/blob/explicit-backing-fields-re/proposals/explicit-backing-fields.md + languageSettings.enableLanguageFeature("ExplicitBackingFields") + } project.tasks.withType { kotlinOptions { jvmTarget = "17" From 8a0838a13f147d5a0b288d6615a05231edc7a929 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 18 Apr 2025 17:42:27 +0300 Subject: [PATCH 075/151] Updated on 2026-08-14 --- .../v2/send/confirm/model/SendConfirmModel.kt | 2 +- .../features/send/v2/send/model/SendModel.kt | 20 +++++++++++++------ gradle/tangem_dependencies.toml | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) 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 cbc35be461..8b0a2cad19 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 @@ -430,7 +430,7 @@ internal class SendConfirmModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).filter { it.second is SendRoute.Confirm }.onEach { (state, _) -> + ).filter { it.second is CommonSendRoute.Confirm }.onEach { (state, _) -> val amountUM = state.amountUM as? AmountState.Data val confirmUM = state.confirmUM params.callback.onResult( 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 a2c5df9f21..06a72af707 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,14 +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.NavigationUM 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.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory -import com.tangem.features.send.v2.send.confirm.ui.state.ConfirmUM 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.amount.SendAmountUpdateQRTrigger @@ -115,7 +113,7 @@ internal class SendModel @Inject constructor( } override fun onAmountResult(amountUM: AmountState) { - predefinedAmountValue = null // reset predefined amount + resetPredefinedAmount() _uiState.update { it.copy(amountUM = amountUM) } } @@ -124,10 +122,20 @@ internal class SendModel @Inject constructor( } override fun onResult(sendUM: SendUM) { - predefinedAmountValue = null // reset predefined amount + resetPredefinedAmount() _uiState.update { sendUM } } + private fun resetPredefinedAmount() { + // reset predefined amount + val internalPredefinedValues = predefinedValues + predefinedValues = when (internalPredefinedValues) { + is PredefinedValues.Content.Deeplink -> internalPredefinedValues.copy(amount = "") + is PredefinedValues.Content.QrCode -> internalPredefinedValues.copy(amount = "") + PredefinedValues.Empty -> internalPredefinedValues + } + } + private fun initAppCurrency() { modelScope.launch { appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } @@ -236,9 +244,9 @@ internal class SendModel @Inject constructor( val parsedQrCode = parseQrCodeUseCase(address, cryptoCurrency).getOrNull() // Decompose component can be active or inactive depending on its state and navigation stack // If it is in inactive state use parameter to pass value to amount component - val amount = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals).orEmpty() + val amount = parsedQrCode?.amount?.parseBigDecimal(cryptoCurrency.decimals) predefinedValues = PredefinedValues.Content.QrCode( - amount = amount, + amount = amount.orEmpty(), address = parsedQrCode?.address.orEmpty(), memo = parsedQrCode?.memo, ) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ec47aff56d..bcba22609f 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-1034" +tangemBlockchainSdk = "develop-1040" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From c2ad62c94c6cc6d009682bb181237920ebc8359f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Apr 2025 11:50:08 +0500 Subject: [PATCH 076/151] Updated on 2026-08-14 --- .../send/v2/send/DefaultSendComponent.kt | 2 + .../v2/send/confirm/SendConfirmComponent.kt | 7 +- .../features/send/v2/send/model/SendModel.kt | 18 +++++ .../v2/sendnft/DefaultNFTSendComponent.kt | 2 + .../confirm/NFTSendConfirmComponent.kt | 5 ++ .../send/v2/sendnft/model/NFTSendModel.kt | 26 +++++- .../fee/SendFeeComponentParams.kt | 6 ++ .../subcomponents/fee/model/SendFeeModel.kt | 81 +++++++------------ 8 files changed, 91 insertions(+), 56 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 82168f4518..d94ae93e49 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 @@ -201,6 +201,7 @@ internal class DefaultSendComponent @AssistedInject constructor( cryptoCurrencyStatus = model.cryptoCurrencyStatus, feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, appCurrency = model.appCurrency, + onLoadFee = model::loadFee, sendAmount = sendAmount, destinationAddress = destinationAddress, callback = model, @@ -240,6 +241,7 @@ internal class DefaultSendComponent @AssistedInject constructor( appCurrency = model.appCurrency, callback = model, predefinedValues = predefinedValues, + onLoadFee = model::loadFee, ), ) } 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 8b93d2cb71..a7145d5d6e 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 @@ -4,18 +4,21 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import arrow.core.Either +import com.tangem.blockchain.common.transaction.TransactionFee 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.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError 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.common.ui.state.ConfirmUM 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 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 @@ -79,6 +82,7 @@ internal class SendConfirmComponent( sendAmount = model.confirmData.enteredAmount.orZero(), destinationAddress = model.confirmData.enteredDestination.orEmpty(), blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), + onLoadFee = params.onLoadFee, ), onResult = model::onFeeResult, onClick = model::showEditFee, @@ -144,6 +148,7 @@ internal class SendConfirmComponent( val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, val predefinedValues: PredefinedValues, + val onLoadFee: suspend () -> Either, ) interface ModelCallback { 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 06a72af707..c153cbe90e 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 @@ -3,6 +3,7 @@ package com.tangem.features.send.v2.send.model import androidx.compose.runtime.Stable import arrow.core.Either import arrow.core.getOrElse +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -26,6 +27,8 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetPrimaryCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.usecase.GetTransferFeeUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.SendComponent @@ -76,6 +79,7 @@ internal class SendModel @Inject constructor( private val getCardInfoUseCase: GetCardInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val getTransferFeeUseCase: GetTransferFeeUseCase, private val sendAmountUpdateQRTrigger: SendAmountUpdateQRTrigger, ) : Model(), SendComponentCallback { @@ -126,6 +130,20 @@ internal class SendModel @Inject constructor( _uiState.update { sendUM } } + suspend fun loadFee(): Either { + val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") + val amountUM = uiState.value.amountUM as? AmountState.Data ?: error("Invalid amount") + val enteredDestinationAddress = destinationUM.addressTextField.value + val enteredAmount = amountUM.amountTextField.cryptoAmount.value ?: error("Invalid amount") + + return getTransferFeeUseCase( + destination = enteredDestinationAddress, + amount = enteredAmount, + userWallet = userWallet, + cryptoCurrency = params.currency, + ) + } + private fun resetPredefinedAmount() { // reset predefined amount val internalPredefinedValues = predefinedValues 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 095cb06ce3..4b65d6d5ae 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 @@ -151,6 +151,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, appCurrency = model.appCurrency, sendAmount = BigDecimal.ZERO, + onLoadFee = model::loadFee, destinationAddress = destinationAddress, callback = model, onNextClick = ::onChildBack, @@ -176,6 +177,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( callback = model, currentRoute = currentRouteFlow.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, + onLoadFee = model::loadFee, ), ) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt index ead66f202b..486dd72606 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/NFTSendConfirmComponent.kt @@ -4,6 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import arrow.core.Either +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel @@ -11,6 +13,7 @@ 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.transaction.error.GetFeeError import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.nft.component.NFTDetailsBlockComponent import com.tangem.features.send.v2.common.CommonSendRoute @@ -63,6 +66,7 @@ internal class NFTSendConfirmComponent( feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, appCurrency = params.appCurrency, sendAmount = BigDecimal.ZERO, + onLoadFee = params.onLoadFee, destinationAddress = model.confirmData.enteredDestination.orEmpty(), blockClickEnableFlow = blockClickEnableFlow.asStateFlow(), ), @@ -139,6 +143,7 @@ internal class NFTSendConfirmComponent( val callback: ModelCallback, val currentRoute: Flow, val isBalanceHidingFlow: StateFlow, + val onLoadFee: suspend () -> Either, ) interface ModelCallback { 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 7b3be9e2b3..65d322cf3f 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 @@ -1,7 +1,9 @@ package com.tangem.features.send.v2.sendnft.model import androidx.compose.runtime.Stable +import arrow.core.Either import arrow.core.getOrElse +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -14,12 +16,13 @@ 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.transaction.error.GetFeeError 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.common.ui.state.NavigationUM 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 @@ -88,6 +91,23 @@ internal class NFTSendModel @Inject constructor( _uiState.update { it.copy(feeUM = feeUM) } } + suspend fun loadFee(): Either { + TODO() + // val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") + // val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + // ?: error("Invalid owner address") + // val enteredDestinationAddress = destinationUM.addressTextField.value + // val nftAsset = null // NFTSdkAssetConverter.convertBack(params.nftAsset).second + + // getNFTTransferFeeUseCase( + // ownerAddress = ownerAddress, + // destinationAddress = enteredDestinationAddress, + // nftAsset = nftAsset, + // userWallet = userWallet, + // cryptoCurrency = cryptoCurrency, + // ) + } + private fun initAppCurrency() { modelScope.launch { appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default } @@ -131,7 +151,9 @@ internal class NFTSendModel @Inject constructor( cryptoCurrencyStatus = cryptoStatus, ).getOrNull() ?: cryptoStatus - router.replaceAll(CommonSendRoute.Destination(isEditMode = false)) + if (uiState.value.destinationUM is DestinationUM.Empty) { + 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/SendFeeComponentParams.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/fee/SendFeeComponentParams.kt index 20e3c94ded..60b98fffbf 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 @@ -1,7 +1,10 @@ package com.tangem.features.send.v2.subcomponents.fee +import arrow.core.Either +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM @@ -19,6 +22,7 @@ internal sealed class SendFeeComponentParams { abstract val appCurrency: AppCurrency abstract val sendAmount: BigDecimal abstract val destinationAddress: String + abstract val onLoadFee: suspend () -> Either data class FeeParams( override val state: FeeUM, @@ -29,6 +33,7 @@ internal sealed class SendFeeComponentParams { override val appCurrency: AppCurrency, override val sendAmount: BigDecimal, override val destinationAddress: String, + override val onLoadFee: suspend () -> Either, val currentRoute: Flow, val callback: SendFeeComponent.ModelCallback, val onNextClick: () -> Unit, @@ -43,6 +48,7 @@ internal sealed class SendFeeComponentParams { override val appCurrency: AppCurrency, override val sendAmount: BigDecimal, override val destinationAddress: String, + override val onLoadFee: suspend () -> Either, val blockClickEnableFlow: StateFlow, ) : SendFeeComponentParams() } \ No newline at end of file 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 b2bd054e2a..5bc3264f89 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,6 @@ 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.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 @@ -29,7 +28,6 @@ import com.tangem.utils.coroutines.saveIn import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import java.math.BigDecimal import java.util.Locale import javax.inject.Inject @@ -40,7 +38,6 @@ internal class SendFeeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, - private val getTransferFeeUseCase: GetTransferFeeUseCase, private val urlOpener: UrlOpener, private val analyticsEventHandler: AnalyticsEventHandler, private val sendFeeAlertFactory: SendFeeAlertFactory, @@ -74,10 +71,32 @@ internal class SendFeeModel @Inject constructor( } override fun feeReload() { - loadFee( - amountValue = params.sendAmount, - destinationAddress = params.destinationAddress, - ) + modelScope.launch { + val isShowLoading = (uiState.value as? FeeUM.Content)?.feeSelectorUM !is FeeSelectorUM.Content + if (isShowLoading) { + _uiState.update(SendFeeLoadingTransformer) + } + params.onLoadFee().fold( + ifRight = { + _uiState.update( + SendFeeLoadedTransformer( + fees = it, + clickIntents = this@SendFeeModel, + appCurrency = appCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, + isFeeApproximate = isFeeApproximate(it.normal.amount.type), + ), + ) + updateFeeNotifications() + }, + ifLeft = { feeError -> + if (isShowLoading) { + _uiState.update(SendFeeFailedTransformer(feeError)) + } + updateFeeNotifications() + }, + ) + }.saveIn(feeJobHolder) } override fun onFeeSelectorClick(feeType: FeeType) { @@ -159,12 +178,7 @@ internal class SendFeeModel @Inject constructor( private fun subscribeOnFeeReloadTriggerUpdates() { feeReloadListener.reloadTriggerFlow - .onEach { (amount, destination) -> - loadFee( - amountValue = amount ?: params.sendAmount, - destinationAddress = destination ?: params.destinationAddress, - ) - } + .onEach { feeReload() } .launchIn(modelScope) } @@ -186,48 +200,9 @@ internal class SendFeeModel @Inject constructor( params.callback.onFeeResult(uiState.value) } - private fun loadFee(amountValue: BigDecimal, destinationAddress: String) { - modelScope.launch { - val isShowLoading = (uiState.value as? FeeUM.Content)?.feeSelectorUM !is FeeSelectorUM.Content - if (isShowLoading) { - _uiState.update(SendFeeLoadingTransformer) - } - getTransferFeeUseCase.invoke( - amount = amountValue, - destination = destinationAddress, - userWallet = params.userWallet, - cryptoCurrency = cryptoCurrencyStatus.currency, - ).fold( - ifRight = { - _uiState.update( - SendFeeLoadedTransformer( - fees = it, - clickIntents = this@SendFeeModel, - appCurrency = appCurrency, - feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - isFeeApproximate = isFeeApproximate(it.normal.amount.type), - ), - ) - updateFeeNotifications() - }, - ifLeft = { feeError -> - if (isShowLoading) { - _uiState.update(SendFeeFailedTransformer(feeError)) - } - updateFeeNotifications() - }, - ) - }.saveIn(feeJobHolder) - } - private fun checkLoadFee() { modelScope.launch { - getTransferFeeUseCase.invoke( - amount = params.sendAmount, - destination = params.destinationAddress, - userWallet = params.userWallet, - cryptoCurrency = cryptoCurrencyStatus.currency, - ).fold( + params.onLoadFee().fold( ifRight = { sendFeeAlertFactory.getFeeUpdatedAlert( newFee = it, From d13ce62ad93e1f7e276dc2378e2560d4acda33da Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Apr 2025 11:57:49 +0500 Subject: [PATCH 077/151] Updated on 2026-08-14 --- .../subcomponents/destination/model/SendDestinationModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 e9ff9401a2..b2e71ae80a 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 @@ -269,8 +269,10 @@ internal class SendDestinationModel @Inject constructor( private fun autoNextFromRecipient(type: EnterAddressSource?, isValidAddress: Boolean, isValidMemo: Boolean) { val isRecent = type == EnterAddressSource.RecentAddress - if (isRecent && isValidAddress && isValidMemo) saveResult() - (params as? SendDestinationComponentParams.DestinationParams)?.onNextClick?.invoke() + if (isRecent && isValidAddress && isValidMemo) { + saveResult() + (params as? SendDestinationComponentParams.DestinationParams)?.onNextClick?.invoke() + } } private fun saveResult() { From 6ecbc20e26df18f506ff111cffef45d651037d70 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 17 Apr 2025 18:00:29 +0300 Subject: [PATCH 078/151] Updated on 2026-08-14 --- .../data/staking/DefaultStakingRepository.kt | 43 +--- .../DefaultSingleYieldBalanceProducer.kt | 72 ++++++ .../data/staking/utils/StakingIdFactory.kt | 76 ++++++ .../DefaultSingleYieldBalanceProducerTest.kt | 239 ++++++++++++++++++ .../single/SingleYieldBalanceProducer.kt | 23 ++ .../single/SingleYieldBalanceSupplier.kt | 18 ++ 6 files changed, 434 insertions(+), 37 deletions(-) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 198ecb2e5a..3ccb9e47aa 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -24,6 +24,7 @@ import com.tangem.data.staking.converters.transaction.GasEstimateConverter import com.tangem.data.staking.converters.transaction.StakingTransactionConverter import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter +import com.tangem.data.staking.utils.StakingIdFactory.Companion.integrationIdMap import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.stakekit.StakeKitApi @@ -729,45 +730,13 @@ internal class DefaultStakingRepository( } @Suppress("unused") - private companion object { - const val YIELDS_STORE_KEY = "yields" + companion object { + private const val YIELDS_STORE_KEY = "yields" - const val TON_INTEGRATION_ID = "ton-ton-chorus-one-pools-staking" - const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" - const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" - const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" - const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" - const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" - const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" - const val TRON_INTEGRATION_ID = "tron-trx-native-staking" - const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" - const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" - const val NEAR_INTEGRATION_ID = "near-near-native-staking" - const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" - const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking" + private const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908" - const val ETHEREUM_POLYGON_APPROVE_SPENDER = "0x5e3Ef299fDDf15eAa0432E6e66473ace8c13D908" + internal val YIELDS_WATITING_TIMEOUT = 15.seconds - val YIELDS_WATITING_TIMEOUT = 15.seconds - - val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79") - - // uncomment items as implementation is ready - val integrationIdMap = mapOf( - Blockchain.TON.run { id + toCoinId() } to TON_INTEGRATION_ID, - Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID, - Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID, - Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID, - Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, - // Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, - Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID, - // Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID, - // Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID, - // Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID, - // Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID, - // Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID, - // Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID, - Blockchain.Cardano.run { id + toCoinId() } to CARDANO_INTEGRATION_ID, - ) + private val INVALID_BATCHES_FOR_SOLANA = listOf("AC01", "CB79") } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt new file mode 100644 index 0000000000..3327ef9790 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt @@ -0,0 +1,72 @@ +package com.tangem.data.staking.single + +import com.tangem.data.staking.store.YieldsBalancesStore.StakingID +import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier +import com.tangem.domain.staking.single.SingleYieldBalanceProducer +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 + +/** + * Default implementation of [SingleYieldBalanceProducer] + * + * @property params params + * @property multiYieldBalanceSupplier multi yield balance supplier + * @property stakingIdFactory factory for creating [StakingID] + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultSingleYieldBalanceProducer @AssistedInject constructor( + @Assisted private val params: SingleYieldBalanceProducer.Params, + private val multiYieldBalanceSupplier: MultiYieldBalanceSupplier, + private val stakingIdFactory: StakingIdFactory, + private val dispatchers: CoroutineDispatcherProvider, +) : SingleYieldBalanceProducer { + + override val fallback: YieldBalance by lazy { + YieldBalance.Error( + integrationId = stakingIdFactory.createIntegrationId(currencyId = params.currencyId), + address = null, + ) + } + + override fun produce(): Flow { + return multiYieldBalanceSupplier( + params = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId), + ) + .mapNotNull { + val currentStakingIds = stakingIdFactory.create( + userWalletId = params.userWalletId, + currencyId = params.currencyId, + network = params.network, + ) + + it.firstOrNull { balance -> + val integrationId = balance.integrationId + val address = balance.address + + if (integrationId == null || address == null) return@mapNotNull null + + val balanceStakingId = StakingID(integrationId = integrationId, address = address) + + currentStakingIds.contains(balanceStakingId) + } + } + .distinctUntilChanged() + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : SingleYieldBalanceProducer.Factory { + override fun create(params: SingleYieldBalanceProducer.Params): DefaultSingleYieldBalanceProducer + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt new file mode 100644 index 0000000000..7e8362efb5 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt @@ -0,0 +1,76 @@ +package com.tangem.data.staking.utils + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toCoinId +import com.tangem.blockchainsdk.utils.toMigratedCoinId +import com.tangem.data.staking.store.YieldsBalancesStore.StakingID +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import javax.inject.Inject + +/** + * Factory of [StakingID] + * + * @property walletManagersFacade wallet manager facade + * +[REDACTED_AUTHOR] + */ +internal class StakingIdFactory @Inject constructor( + private val walletManagersFacade: WalletManagersFacade, +) { + + suspend fun create(userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, network: Network): Set { + val addresses = walletManagersFacade.getAddresses(userWalletId = userWalletId, network = network) + + val integrationId = createIntegrationId(currencyId) ?: return emptySet() + + return addresses.mapTo(hashSetOf()) { address -> + StakingID(integrationId = integrationId, address = address.value) + } + } + + fun createIntegrationId(currencyId: CryptoCurrency.ID): String? { + val integrationKey = with(currencyId) { rawNetworkId.plus(rawCurrencyId) } + return integrationIdMap[integrationKey] + } + + @Suppress("UnusedPrivateMember") + companion object { + + private const val TON_INTEGRATION_ID = "ton-ton-chorus-one-pools-staking" + private const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking" + private const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking" + private const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking" + private const val BINANCE_INTEGRATION_ID = "bsc-bnb-native-staking" + private const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking" + private const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking" + private const val TRON_INTEGRATION_ID = "tron-trx-native-staking" + private const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking" + private const val KAVA_INTEGRATION_ID = "kava-kava-native-staking" + private const val NEAR_INTEGRATION_ID = "near-near-native-staking" + private const val TEZOS_INTEGRATION_ID = "tezos-xtz-native-staking" + private const val CARDANO_INTEGRATION_ID = "cardano-ada-native-staking" + + // uncomment items as implementation is ready + val integrationIdMap = mapOf( + Blockchain.TON.toDefaultKey() to TON_INTEGRATION_ID, + Blockchain.Solana.toDefaultKey() to SOLANA_INTEGRATION_ID, + Blockchain.Cosmos.toDefaultKey() to COSMOS_INTEGRATION_ID, + Blockchain.Tron.toDefaultKey() to TRON_INTEGRATION_ID, + Blockchain.Ethereum.id + Blockchain.Polygon.toMigratedCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, + // Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID, + Blockchain.BSC.toDefaultKey() to BINANCE_INTEGRATION_ID, + // Blockchain.Polkadot.toDefaultKey() to POLKADOT_INTEGRATION_ID, + // Blockchain.Avalanche.toDefaultKey() to AVALANCHE_INTEGRATION_ID, + // Blockchain.Cronos.toDefaultKey() to CRONOS_INTEGRATION_ID, + // Blockchain.Kava.toDefaultKey() to KAVA_INTEGRATION_ID, + // Blockchain.Near.toDefaultKey() to NEAR_INTEGRATION_ID, + // Blockchain.Tezos.toDefaultKey() to TEZOS_INTEGRATION_ID, + Blockchain.Cardano.toDefaultKey() to CARDANO_INTEGRATION_ID, + ) + + private fun Blockchain.toDefaultKey(): String = id + toCoinId() + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt new file mode 100644 index 0000000000..3522d505db --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt @@ -0,0 +1,239 @@ +package com.tangem.data.staking.single + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.getEmittedValues +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.toDomain +import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier +import com.tangem.domain.staking.single.SingleYieldBalanceProducer +import com.tangem.domain.wallets.models.UserWalletId +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 DefaultSingleYieldBalanceProducerTest { + + private val params = SingleYieldBalanceProducer.Params( + userWalletId = UserWalletId(stringValue = "011"), + currencyId = ton.id, + network = ton.network, + ) + + private val multiNetworkStatusSupplier = mockk() + private val stakingIdFactory = mockk() + private val dispatchers = TestingCoroutineDispatcherProvider() + + private val producer = DefaultSingleYieldBalanceProducer( + params = params, + stakingIdFactory = stakingIdFactory, + multiYieldBalanceSupplier = multiNetworkStatusSupplier, + dispatchers = dispatchers, + ) + + @Test + fun `test that flow is mapped for data from params`() = runTest { + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() + + val expected = flowOf( + setOf( + balance, + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain(), + ), + ) + + val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + + val actual = producer.produce() + + verify { multiNetworkStatusSupplier(multiParams) } + + val values = getEmittedValues(flow = actual) + + coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + + Truth.assertThat(values.size).isEqualTo(1) + Truth.assertThat(values).isEqualTo(listOf(balance)) + } + + @Test + fun `test that flow is updated if yield balance is updated`() = runTest { + val expected = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + + val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + + val actual = producer.produceWithFallback() + + verify { multiNetworkStatusSupplier(multiParams) } + + // first emit + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() + expected.emit(value = setOf(balance)) + + val values1 = getEmittedValues(flow = actual) + + coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(balance)) + + // second emit + val updatedStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = tonId.address) + expected.emit(value = setOf(updatedStatus)) + + val values2 = getEmittedValues(flow = actual) + + coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + + Truth.assertThat(values2.size).isEqualTo(2) + Truth.assertThat(values2).isEqualTo(listOf(balance, updatedStatus)) + } + + @Test + fun `test that flow is filtered the same status`() = runTest { + val expected = MutableSharedFlow>(replay = 2, extraBufferCapacity = 1) + + val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + + val actual = producer.produceWithFallback() + + verify { multiNetworkStatusSupplier(multiParams) } + + // first emit + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() + expected.emit(value = setOf(balance)) + + val values1 = getEmittedValues(flow = actual) + + coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + + Truth.assertThat(values1.size).isEqualTo(1) + Truth.assertThat(values1).isEqualTo(listOf(balance)) + + // second emit + expected.emit(value = setOf(balance)) + + val values2 = getEmittedValues(flow = actual) + + coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(balance)) + } + + @Test + fun `test if flow throws exception`() = runTest { + val exception = IllegalStateException() + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() + + val innerFlow = MutableStateFlow(value = false) + val expected = flow { + if (innerFlow.value) { + emit(setOf(balance)) + } else { + throw exception + } + } + .buffer(capacity = 5) + + val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + every { stakingIdFactory.createIntegrationId(currencyId = params.currencyId) } returns tonId.integrationId + + val actual = producer.produceWithFallback() + + verify { multiNetworkStatusSupplier(multiParams) } + + val values1 = getEmittedValues(flow = actual) + + coVerify(inverse = true) { stakingIdFactory.create(any(), any(), any()) } + + Truth.assertThat(values1.size).isEqualTo(1) + val fallbackStatus = YieldBalance.Error(integrationId = tonId.integrationId, address = null) + Truth.assertThat(values1).isEqualTo(listOf(fallbackStatus)) + + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + + innerFlow.emit(value = true) + + val values2 = getEmittedValues(flow = actual) + + coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + + Truth.assertThat(values2.size).isEqualTo(1) + Truth.assertThat(values2).isEqualTo(listOf(balance)) + } + + @Test + fun `test if flow doesn't contain network from params`() = runTest { + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId).toDomain() + + val expected = flowOf(setOf(balance)) + + val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns stakingIds + + val actual = producer.produce() + + verify { multiNetworkStatusSupplier(multiParams) } + + val values = getEmittedValues(flow = actual) + + coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + + Truth.assertThat(values.size).isEqualTo(0) + } + + @Test + fun `test if wallet manager facade returns empty set`() = runTest { + val balance = MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId).toDomain() + + val expected = flowOf(setOf(balance)) + + val multiParams = MultiYieldBalanceProducer.Params(userWalletId = params.userWalletId) + every { multiNetworkStatusSupplier(multiParams) } returns expected + coEvery { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } returns emptySet() + + val actual = producer.produce() + + verify { multiNetworkStatusSupplier(multiParams) } + + val values = getEmittedValues(flow = actual) + + coVerify { stakingIdFactory.create(params.userWalletId, params.currencyId, params.network) } + + Truth.assertThat(values.size).isEqualTo(0) + } + + private companion object { + + val mocks = MockCryptoCurrencyFactory() + + val ton = mocks.createCoin(Blockchain.TON) + + val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId + val solanaId = YieldsBalancesStore.StakingID( + integrationId = "solana-sol-native-multivalidator-staking", + address = "0x1", + ) + + val stakingIds = setOf(tonId) + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt new file mode 100644 index 0000000000..6951cf3a1b --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceProducer.kt @@ -0,0 +1,23 @@ +package com.tangem.domain.staking.single + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Producer of yield balance for selected wallet [UserWalletId] + * +[REDACTED_AUTHOR] + */ +interface SingleYieldBalanceProducer : FlowProducer { + + data class Params( + val userWalletId: UserWalletId, + val currencyId: CryptoCurrency.ID, + val network: Network, + ) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt new file mode 100644 index 0000000000..f7509dc980 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceSupplier.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.staking.single + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.staking.model.stakekit.YieldBalance + +/** + * Supplier of yield balance for selected wallet [SingleYieldBalanceProducer.Params] + * + * @property factory factory for creating [SingleYieldBalanceProducer] + * @property keyCreator key creator + * +[REDACTED_AUTHOR] + */ +abstract class SingleYieldBalanceSupplier( + override val factory: FlowProducer.Factory, + override val keyCreator: (SingleYieldBalanceProducer.Params) -> String, +) : FlowCachingSupplier() \ No newline at end of file From 17008240b90cafef50966d0f2ede43be33b963e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Apr 2025 14:32:52 +0400 Subject: [PATCH 079/151] Updated on 2026-08-14 --- data/blockaid/build.gradle.kts | 1 + .../tangem/data/blockaid/BlockAidMapper.kt | 3 +- .../blockaid/DefaultBlockAidRepository.kt | 3 +- .../blockaid/di/BlockAidDataInternalModule.kt | 17 ++++-- .../data/blockaid/BlockAidMapperTest.kt | 58 +++++++++---------- .../blockaid/DefaultBlockAidRepositoryTest.kt | 36 ++++-------- data/wallet-connect/build.gradle.kts | 2 + .../di/WalletConnectDataModule.kt | 3 + .../pair/DefaultWcPairUseCase.kt | 27 +++++++-- .../sessions/DefaultWcSessionsManager.kt | 25 +++++--- .../walletconnect/DefaultWcPairUseCaseTest.kt | 15 +++++ domain/wallet-connect/build.gradle.kts | 1 + domain/wallet-connect/models/build.gradle.kts | 1 + .../domain/walletconnect/model/WcSession.kt | 2 + .../walletconnect/model/WcSessionDTO.kt | 2 + .../walletconnect/model/WcSessionProposal.kt | 3 +- 16 files changed, 123 insertions(+), 76 deletions(-) diff --git a/data/blockaid/build.gradle.kts b/data/blockaid/build.gradle.kts index c1855c991d..344e76b061 100644 --- a/data/blockaid/build.gradle.kts +++ b/data/blockaid/build.gradle.kts @@ -38,4 +38,5 @@ dependencies { testImplementation(deps.test.junit) testImplementation(deps.test.mockk) testImplementation(deps.test.turbine) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt index 5336bf9657..a36dea8518 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -10,13 +10,12 @@ import com.tangem.datasource.api.common.blockaid.models.request.EvmTransactionSc 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() { +internal class BlockAidMapper { fun mapToDomain(from: DomainScanResponse): CheckDAppResult { return when { 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 index 837b699192..8659672ffe 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt @@ -9,9 +9,8 @@ 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( +internal class DefaultBlockAidRepository( private val api: BlockAidApi, private val dispatcherProvider: CoroutineDispatcherProvider, private val mapper: BlockAidMapper, 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 index 894e9aca12..4f4ff71605 100644 --- 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 @@ -1,18 +1,27 @@ package com.tangem.data.blockaid.di +import com.tangem.data.blockaid.BlockAidMapper import com.tangem.data.blockaid.BlockAidRepository import com.tangem.data.blockaid.DefaultBlockAidRepository -import dagger.Binds +import com.tangem.datasource.api.common.blockaid.BlockAidApi +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 interface BlockAidDataInternalModule { +internal object BlockAidDataInternalModule { - @Binds + @Provides @Singleton - fun bindRepository(repository: DefaultBlockAidRepository): BlockAidRepository + fun provideRepository(api: BlockAidApi, dispatcherProvider: CoroutineDispatcherProvider): BlockAidRepository { + return DefaultBlockAidRepository( + api = api, + dispatcherProvider = dispatcherProvider, + mapper = BlockAidMapper(), + ) + } } \ 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 index ac9639b167..faaba4926c 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -4,8 +4,8 @@ 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.google.common.truth.Truth import com.tangem.datasource.api.common.blockaid.models.response.* -import org.junit.Assert.* import org.junit.Test import java.math.BigDecimal @@ -14,28 +14,28 @@ class BlockAidMapperTest { private val mapper = BlockAidMapper() @Test - fun whenStatusHitAndIsMaliciousFalseThenMapToDomainReturnsSafe() { + fun `when status hit and is malicious false then map to domain returns safe`() { val response = DomainScanResponse(status = "hit", isMalicious = false) val result = mapper.mapToDomain(response) - assertEquals(CheckDAppResult.SAFE, result) + Truth.assertThat(result).isEqualTo(CheckDAppResult.SAFE) } @Test - fun whenStatusHitAndIsMaliciousTrueThenMapToDomainReturnsUnsafe() { + fun `when status hit and is malicious true then map to domain returns unsafe`() { val response = DomainScanResponse(status = "hit", isMalicious = true) val result = mapper.mapToDomain(response) - assertEquals(CheckDAppResult.UNSAFE, result) + Truth.assertThat(result).isEqualTo(CheckDAppResult.UNSAFE) } @Test - fun whenStatusNotHitThenMapToDomainReturnsFailedToVerify() { + fun `when status not hit then map to domain returns failed to verify`() { val response = DomainScanResponse(status = "miss", isMalicious = false) val result = mapper.mapToDomain(response) - assertEquals(CheckDAppResult.FAILED_TO_VERIFY, result) + Truth.assertThat(result).isEqualTo(CheckDAppResult.FAILED_TO_VERIFY) } @Test - fun whenResponseBenignValidationThenReturnsSafeValidation() { + fun `when response benign validation then returns safe validation`() { val spenderDetails = SpenderDetails( isApprovedForAll = true, exposure = listOf(ExposureDetail(value = "1000.0", rawValue = "0x123")), @@ -53,20 +53,20 @@ class BlockAidMapperTest { ) val result = mapper.mapToDomain(response) - assertEquals(ValidationResult.SAFE, result.validation) + Truth.assertThat(result.validation).isEqualTo(ValidationResult.SAFE) val simulation = result.simulation as? SimulationResult.Success - assertNotNull(simulation) + Truth.assertThat(simulation).isNotNull() 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) + Truth.assertThat(approve).isNotNull() + Truth.assertThat(approve?.approvedAmounts?.size).isEqualTo(1) + Truth.assertThat(approve?.approvedAmounts?.first()?.approvedAmount).isEqualTo(BigDecimal("1000.0")) + Truth.assertThat(approve?.approvedAmounts?.first()?.isUnlimited).isTrue() } @Test - fun whenResponseBenignValidationAndSuccessSimulationThenReturnsSendReceiveResult() { + fun `when response benign validation and success simulation then returns send receive result`() { val assetDiff = AssetDiff( assetType = "ERC20", asset = Asset(chainId = 1, logoUrl = "logo", symbol = "ETH"), @@ -82,19 +82,19 @@ class BlockAidMapperTest { ) val result = mapper.mapToDomain(response) - assertEquals(ValidationResult.SAFE, result.validation) + Truth.assertThat(result.validation).isEqualTo(ValidationResult.SAFE) val simulation = result.simulation as? SimulationResult.Success - assertNotNull(simulation) + Truth.assertThat(simulation).isNotNull() 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) + Truth.assertThat(data).isNotNull() + Truth.assertThat(data?.send?.first()?.amount).isEqualTo(BigDecimal("1.5")) + Truth.assertThat(data?.receive?.first()?.amount).isEqualTo(BigDecimal("2.0")) } @Test - fun whenResponseErrorValidationThenReturnsFailedToValidate() { + fun `when response error validation rhen returns failed to validate`() { val response = TransactionScanResponse( validation = ValidationResponse(status = "Error", resultType = "Benign"), simulation = SimulationResponse( @@ -104,12 +104,12 @@ class BlockAidMapperTest { ) val result = mapper.mapToDomain(response) - assertEquals(ValidationResult.FAILED_TO_VALIDATE, result.validation) - assertTrue(result.simulation is SimulationResult.FailedToSimulate) + Truth.assertThat(result.validation).isEqualTo(ValidationResult.FAILED_TO_VALIDATE) + Truth.assertThat(result.simulation is SimulationResult.FailedToSimulate).isTrue() } @Test - fun whenResponseNotBenignThenReturnsValidationUnsafe() { + fun `when response not benign then returns validation unsafe`() { val response = TransactionScanResponse( validation = ValidationResponse(status = "Success", resultType = "Phishing"), simulation = SimulationResponse( @@ -119,11 +119,11 @@ class BlockAidMapperTest { ) val result = mapper.mapToDomain(response) - assertEquals(ValidationResult.UNSAFE, result.validation) + Truth.assertThat(result.validation).isEqualTo(ValidationResult.UNSAFE) } @Test - fun whenResponseSimulationNotSuccessThenReturnsSimulationFailedToSimulate() { + fun `when response simulation not success then returns simulation failed ro simulate`() { val response = TransactionScanResponse( validation = ValidationResponse(status = "Success", resultType = "Benign"), simulation = SimulationResponse( @@ -133,11 +133,11 @@ class BlockAidMapperTest { ) val result = mapper.mapToDomain(response) - assertTrue(result.simulation is SimulationResult.FailedToSimulate) + Truth.assertThat(result.simulation is SimulationResult.FailedToSimulate).isTrue() } @Test - fun whenResponseSimulationIsEmptyThenReturnsFailedToSimulate() { + fun `when response simulation is empty then returns failed to simulate`() { val txResponse = TransactionScanResponse( validation = ValidationResponse(status = "Success", resultType = "Benign"), simulation = SimulationResponse( @@ -150,6 +150,6 @@ class BlockAidMapperTest { ) val result = mapper.mapToDomain(txResponse) - assertTrue(result.simulation is SimulationResult.FailedToSimulate) + Truth.assertThat(result.simulation is SimulationResult.FailedToSimulate).isTrue() } } \ 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 index bb919dbdb7..036ed43815 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt @@ -5,24 +5,20 @@ 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.google.common.truth.Truth 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 com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider 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 kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -@OptIn(ExperimentalCoroutinesApi::class) class DefaultBlockAidRepositoryTest { @MockK @@ -31,28 +27,18 @@ class DefaultBlockAidRepositoryTest { @MockK private lateinit var mapper: BlockAidMapper - @MockK - private lateinit var dispatcherProvider: CoroutineDispatcherProvider - private lateinit var repository: DefaultBlockAidRepository - private val testDispatcher = StandardTestDispatcher() + private val testDispatcherProvider = TestingCoroutineDispatcherProvider() @Before fun setup() { MockKAnnotations.init(this) - Dispatchers.setMain(testDispatcher) - every { dispatcherProvider.io } returns testDispatcher - repository = DefaultBlockAidRepository(api, dispatcherProvider, mapper) - } - - @After - fun tearDown() { - Dispatchers.resetMain() + repository = DefaultBlockAidRepository(api, testDispatcherProvider, mapper) } @Test - fun whenVerifyAppDomainThenCallsApiAndMapsResult() = runTest { + fun `when verify app domain then calls api and maps result`() = runTest { val url = "https://example.com" val domainData = DAppData(url) val domainResponse = DomainScanResponse(status = "hit", isMalicious = false) @@ -63,13 +49,13 @@ class DefaultBlockAidRepositoryTest { val result = repository.verifyDAppDomain(domainData) - assertEquals(expectedResult, result) + Truth.assertThat(result).isEqualTo(expectedResult) coVerify { api.scanDomain(DomainScanRequest(url)) } verify { mapper.mapToDomain(domainResponse) } } @Test - fun whenVerifyEvmTransactionThenCallsScanJsonRpcAndMaps() = runTest { + fun `when verify evm transaction then calls scan json rpc and maps result`() = runTest { val data = TransactionData( chain = "ethereum", accountAddress = "0xabc", @@ -88,14 +74,14 @@ class DefaultBlockAidRepositoryTest { val result = repository.verifyTransaction(data) - assertEquals(expectedResult, result) + Truth.assertThat(result).isEqualTo(expectedResult) coVerify { api.scanJsonRpc(request) } verify { mapper.mapToEvmRequest(data) } verify { mapper.mapToDomain(response) } } @Test - fun whenVerifySolanaTransactionThenCallsScanSolanaMessageAndMapsResult() = runTest { + fun `when verify solana transaction then calls scan solana message and maps result`() = runTest { val data = TransactionData( chain = "mainnet", accountAddress = "/Rd2TLl...", @@ -114,7 +100,7 @@ class DefaultBlockAidRepositoryTest { val result = repository.verifyTransaction(data) - assertEquals(expectedResult, result) + Truth.assertThat(result).isEqualTo(expectedResult) coVerify { api.scanSolanaMessage(request) } verify { mapper.mapToSolanaRequest(data) } verify { mapper.mapToDomain(response) } diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index cb90e05209..d03ae4e940 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -50,6 +50,8 @@ dependencies { /* Other */ implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) + implementation(projects.domain.blockaid) + implementation(projects.domain.blockaid.models) /* Tests */ testImplementation(projects.common.test) 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 1a5345959e..385089f187 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 @@ -20,6 +20,7 @@ import com.tangem.data.walletconnect.utils.WcNamespaceConverter import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore +import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WalletConnectRepository @@ -71,11 +72,13 @@ internal object WalletConnectDataModule { associateNetworksDelegate: AssociateNetworksDelegate, caipNamespaceDelegate: CaipNamespaceDelegate, sdkDelegate: WcPairSdkDelegate, + blockAidVerifier: BlockAidVerifier, ): DefaultWcPairUseCase = DefaultWcPairUseCase( sessionsManager = sessionsManager, associateNetworksDelegate = associateNetworksDelegate, caipNamespaceDelegate = caipNamespaceDelegate, sdkDelegate = sdkDelegate, + blockAidVerifier = blockAidVerifier, ) @Provides 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 28776d97c2..107fb0289f 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 @@ -1,10 +1,14 @@ package com.tangem.data.walletconnect.pair import arrow.core.Either +import arrow.core.getOrElse import arrow.core.left import arrow.core.right +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData import com.reown.walletkit.client.Wallet import com.tangem.data.walletconnect.utils.WcSdkSessionConverter +import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove @@ -28,6 +32,7 @@ internal class DefaultWcPairUseCase( private val associateNetworksDelegate: AssociateNetworksDelegate, private val caipNamespaceDelegate: CaipNamespaceDelegate, private val sdkDelegate: WcPairSdkDelegate, + private val blockAidVerifier: BlockAidVerifier, ) : WcPairUseCase { private val onCallTerminalAction = Channel() @@ -71,7 +76,10 @@ internal class DefaultWcPairUseCase( sessionForApprove = sessionForApprove, sdkSessionProposal = sdkSessionProposal, ).map { settledSession -> - val newSession = settledSession.session.toDomain(sessionForApprove.wallet) + val newSession = settledSession.session.toDomain( + wallet = sessionForApprove.wallet, + securityStatus = proposalState.dAppSession.securityStatus, + ) sessionsManager.saveSession(newSession) newSession } @@ -107,6 +115,10 @@ internal class DefaultWcPairUseCase( sessionProposal: Wallet.Model.SessionProposal, ): Either = runCatching { val proposalNetwork = associateNetworksDelegate.associate(sessionProposal) + val verificationInfo = blockAidVerifier.verifyDApp(DAppData(sessionProposal.url)).getOrElse { + Timber.e("Failed to verify DApp: ${it.localizedMessage}") + CheckDAppResult.FAILED_TO_VERIFY + } val appMetaData = WcAppMetaData( name = sessionProposal.name, description = sessionProposal.description, @@ -117,7 +129,7 @@ internal class DefaultWcPairUseCase( val dAppSession = WcSessionProposal( dAppMetaData = appMetaData, proposalNetwork = proposalNetwork, - securityStatus = Any(), + securityStatus = verificationInfo, ) WcPairState.Proposal(dAppSession) }.fold(onSuccess = { it.right() }, onFailure = { @@ -127,10 +139,13 @@ internal class DefaultWcPairUseCase( } },) - private fun Wallet.Model.Session.toDomain(wallet: UserWallet): WcSession = WcSession( - wallet = wallet, - sdkModel = WcSdkSessionConverter.convert(this), - ) + private fun Wallet.Model.Session.toDomain(wallet: UserWallet, securityStatus: CheckDAppResult): WcSession { + return WcSession( + wallet = wallet, + sdkModel = WcSdkSessionConverter.convert(this), + securityStatus = securityStatus, + ) + } private sealed interface TerminalAction { data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction 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 341a80d075..d23e135111 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 @@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.sessions import arrow.core.Either import arrow.core.left import arrow.core.right +import com.domain.blockaid.models.dapp.CheckDAppResult import com.reown.walletkit.client.Wallet import com.reown.walletkit.client.WalletKit import com.tangem.data.walletconnect.utils.WcSdkObserver @@ -25,7 +26,7 @@ import timber.log.Timber import kotlin.coroutines.resume import kotlin.time.Duration.Companion.seconds -internal class DefaultWcSessionsManager constructor( +internal class DefaultWcSessionsManager( private val store: WalletConnectStore, private val legacyStore: WalletConnectSessionsRepository, private val getWallets: GetWalletsUseCase, @@ -60,7 +61,7 @@ internal class DefaultWcSessionsManager constructor( } override suspend fun saveSession(session: WcSession) { - store.saveSession(WcSessionDTO(session.sdkModel.topic, session.wallet.walletId)) + store.saveSession(WcSessionDTO(session.sdkModel.topic, session.wallet.walletId, session.securityStatus)) } override suspend fun removeSession(session: WcSession): Either { @@ -82,13 +83,17 @@ internal class DefaultWcSessionsManager constructor( } override suspend fun findSessionByTopic(topic: String): WcSession? = withContext(dispatchers.io) { - val storedSessions = sessions.firstOrNull() + val storedSession = sessions.firstOrNull() ?.values?.flatten() ?.firstOrNull { it.sdkModel.topic == topic } ?: return@withContext null val sdkSession = WalletKit.getActiveSessionByTopic(topic) ?: return@withContext null - val wallet = storedSessions.wallet - WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession)) + val wallet = storedSession.wallet + WcSession( + wallet = wallet, + sdkModel = WcSdkSessionConverter.convert(sdkSession), + securityStatus = storedSession.securityStatus, + ) } override fun onSessionDelete(sessionDelete: Wallet.Model.SessionDelete) { @@ -104,7 +109,13 @@ internal class DefaultWcSessionsManager constructor( val walletIds = wallets.map { wallet -> wallet.walletId } val inLegacyStore = walletIds .map { walletId -> - flow { emit(legacyStore.loadSessions(walletId.stringValue).map { WcSessionDTO(it.topic, walletId) }) } + flow { + emit( + legacyStore.loadSessions(walletId.stringValue).map { + WcSessionDTO(it.topic, walletId, CheckDAppResult.FAILED_TO_VERIFY) + }, + ) + } } .merge() .reduce { accumulator, value -> accumulator.plus(value) } @@ -124,7 +135,7 @@ internal class DefaultWcSessionsManager constructor( 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)) + WcSession(wallet = wallet, sdkModel = WcSdkSessionConverter.convert(sdkSession), session.securityStatus) } return wcSessions } diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 86ed85e03e..2a6827c8c9 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -1,8 +1,11 @@ package com.tangem.domain.walletconnect import app.cash.turbine.test +import arrow.core.Either import arrow.core.left import arrow.core.right +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.domain.blockaid.models.dapp.DAppData import com.reown.walletkit.client.Wallet import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.data.walletconnect.pair.AssociateNetworksDelegate @@ -10,6 +13,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.utils.WcSdkSessionConverter +import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove @@ -30,6 +34,7 @@ internal class DefaultWcPairUseCaseTest { private val associateNetworksDelegate: AssociateNetworksDelegate = mockk() private val caipNamespaceDelegate: CaipNamespaceDelegate = mockk() private val sdkDelegate: WcPairSdkDelegate = mockk() + private val blockAidVerifier: BlockAidVerifier = mockk() private val url = "testUrl" private val source = WcPairUseCase.Source.QR @@ -85,6 +90,7 @@ internal class DefaultWcPairUseCaseTest { get() = WcSession( wallet = sessionForApprove.wallet, sdkModel = WcSdkSessionConverter.convert(this), + securityStatus = CheckDAppResult.SAFE, ) private val useCase = DefaultWcPairUseCase( @@ -92,6 +98,7 @@ internal class DefaultWcPairUseCaseTest { associateNetworksDelegate = associateNetworksDelegate, caipNamespaceDelegate = caipNamespaceDelegate, sdkDelegate = sdkDelegate, + blockAidVerifier = blockAidVerifier, ) @Before @@ -109,11 +116,13 @@ internal class DefaultWcPairUseCaseTest { @Test fun `pair, emmit proposal state and wait actions`() = runTest { coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() + coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } useCase.pairFlow(url, source).test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) + blockAidVerifier.verifyDApp(DAppData(sdkProposal.url)) } assert(awaitItem() is WcPairState.Proposal) expectNoEvents() @@ -129,11 +138,13 @@ internal class DefaultWcPairUseCaseTest { coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.approve(sdkApprove) } returns sdkApproveSuccess.right() coEvery { sessionsManager.saveSession(sessionForSave) } returns Unit + coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } useCase.pairFlow(url, source).test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) + blockAidVerifier.verifyDApp(DAppData(sdkProposal.url)) } assert(awaitItem() is WcPairState.Proposal) useCase.approve(sessionForApprove) @@ -156,11 +167,13 @@ internal class DefaultWcPairUseCaseTest { coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.rejectSession(proposerPublicKey) } returns Unit + coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } useCase.pairFlow(url, source).test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) + blockAidVerifier.verifyDApp(DAppData(sdkProposal.url)) } assert(awaitItem() is WcPairState.Proposal) useCase.reject() @@ -208,6 +221,7 @@ internal class DefaultWcPairUseCaseTest { val error = WcPairError.ExternalApprovalError("error").left() coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { sdkDelegate.approve(sdkApprove) } returns error + coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } val errorResult = WcPairState.Approving.Result(sessionForApprove, error) @@ -216,6 +230,7 @@ internal class DefaultWcPairUseCaseTest { coVerifyOrder { sdkDelegate.pair(url) associateNetworksDelegate.associate(sdkProposal) + blockAidVerifier.verifyDApp(DAppData(sdkProposal.url)) } assert(awaitItem() is WcPairState.Proposal) useCase.approve(sessionForApprove) diff --git a/domain/wallet-connect/build.gradle.kts b/domain/wallet-connect/build.gradle.kts index f20c31f3c4..a2178f32a7 100644 --- a/domain/wallet-connect/build.gradle.kts +++ b/domain/wallet-connect/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) implementation(projects.domain.walletConnect.models) + implementation(projects.domain.blockaid.models) /* Other */ implementation(deps.moshi.adapters) diff --git a/domain/wallet-connect/models/build.gradle.kts b/domain/wallet-connect/models/build.gradle.kts index c471057d7c..4fd593d3d4 100644 --- a/domain/wallet-connect/models/build.gradle.kts +++ b/domain/wallet-connect/models/build.gradle.kts @@ -8,6 +8,7 @@ dependencies { /* Domain */ implementation(projects.domain.wallets.models) implementation(projects.domain.tokens.models) + implementation(projects.domain.blockaid.models) /* Other */ implementation(deps.moshi) 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 cc898e4618..d3707d52f6 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,11 @@ package com.tangem.domain.walletconnect.model +import com.domain.blockaid.models.dapp.CheckDAppResult import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSession import com.tangem.domain.wallets.models.UserWallet data class WcSession( val wallet: UserWallet, val sdkModel: WcSdkSession, + val securityStatus: CheckDAppResult, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt index 63a946b9c6..5698f8421b 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt @@ -1,5 +1,6 @@ package com.tangem.domain.walletconnect.model +import com.domain.blockaid.models.dapp.CheckDAppResult import com.squareup.moshi.JsonClass import com.tangem.domain.wallets.models.UserWalletId @@ -7,4 +8,5 @@ import com.tangem.domain.wallets.models.UserWalletId data class WcSessionDTO( val topic: String, val walletId: UserWalletId, + val securityStatus: CheckDAppResult, ) \ 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 50d299303f..4a110229c4 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,5 +1,6 @@ package com.tangem.domain.walletconnect.model +import com.domain.blockaid.models.dapp.CheckDAppResult import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.wallets.models.UserWallet @@ -7,7 +8,7 @@ import com.tangem.domain.wallets.models.UserWallet data class WcSessionProposal( val dAppMetaData: WcAppMetaData, val proposalNetwork: Map, - val securityStatus: Any, + val securityStatus: CheckDAppResult, ) { data class ProposalNetwork( From aba620bd5952d7e18781ddff0d68c9ab90771563 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Apr 2025 17:15:38 +0500 Subject: [PATCH 080/151] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/routing/utils/ChildFactory.kt | 4 ++-- .../src/main/kotlin/com/tangem/common/routing/AppRoute.kt | 4 ++-- .../com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt | 2 +- .../com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt | 4 ++-- .../features/onramp/component/OnrampSuccessComponent.kt | 2 +- .../onramp/success/model/OnrampSuccessComponentModel.kt | 2 +- .../presentation/router/DefaultTokenDetailsRouter.kt | 4 ++-- .../presentation/router/InnerTokenDetailsRouter.kt | 2 +- .../presentation/tokendetails/model/TokenDetailsModel.kt | 4 ++-- .../child/wallet/model/intents/WalletContentClickIntents.kt | 6 +++--- .../wallet/presentation/router/DefaultWalletRouter.kt | 4 ++-- .../feature/wallet/presentation/router/InnerWalletRouter.kt | 4 ++-- 12 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 362ae78fb2..3eebff1081 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 @@ -180,7 +180,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.OnrampSuccess -> { createComponentChild( context = context, - params = OnrampSuccessComponent.Params(route.externalTxId), + params = OnrampSuccessComponent.Params(route.txId), componentFactory = onrampSuccessComponentFactory, ) } @@ -700,7 +700,7 @@ internal class ChildFactory @Inject constructor( is AppRoute.OnrampSuccess -> { route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = OnrampSuccessComponent.Params(route.externalTxId), + params = OnrampSuccessComponent.Params(route.txId), componentFactory = onrampSuccessComponentFactory, ) } 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 508661dac0..2074653f5f 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 @@ -233,8 +233,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class OnrampSuccess( - val externalTxId: String, - ) : AppRoute(path = "/onramp/success/$externalTxId"), RouteBundleParams { + val txId: String, + ) : AppRoute(path = "/onramp/success/$txId"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) } diff --git a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt index 748afda798..458c57088f 100644 --- a/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt +++ b/core/deep-links/global/src/main/kotlin/com/tangem/core/deeplink/global/BuyCurrencyDeepLink.kt @@ -3,7 +3,7 @@ package com.tangem.core.deeplink.global import com.tangem.core.deeplink.DeepLink class BuyCurrencyDeepLink( - val onReceive: (externalTxId: String) -> Unit, + val onReceive: (txId: String) -> Unit, ) : DeepLink() { override val uri = BUY_REDIRECT_DEEPLINK diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt index 32d3f258db..72ab25fe7c 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampTransactionUseCase.kt @@ -11,10 +11,10 @@ class GetOnrampTransactionUseCase( private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(externalTxId: String): Either { + suspend operator fun invoke(txId: String): Either { return Either.catch { requireNotNull( - onrampTransactionRepository.getTransactionById(externalTxId), + onrampTransactionRepository.getTransactionById(txId), ) }.mapLeft(errorResolver::resolve) } diff --git a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampSuccessComponent.kt b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampSuccessComponent.kt index 02280d517a..ceed88f9c5 100644 --- a/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampSuccessComponent.kt +++ b/features/onramp/api/src/main/kotlin/com/tangem/features/onramp/component/OnrampSuccessComponent.kt @@ -5,7 +5,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent interface OnrampSuccessComponent : ComposableContentComponent { - data class Params(val externalTxId: String) + data class Params(val txId: String) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 622c7055ba..4803e7f98e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -67,7 +67,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( private fun loadData() { modelScope.launch { - getOnrampTransactionUseCase(externalTxId = params.externalTxId) + getOnrampTransactionUseCase(txId = params.txId) .fold( ifLeft = { error -> Timber.e(error.toString()) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt index db52ac752e..b9ba7e893b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/DefaultTokenDetailsRouter.kt @@ -47,13 +47,13 @@ internal class DefaultTokenDetailsRouter @Inject constructor( ) } - override fun openOnrampSuccess(externalTxId: String) { + override fun openOnrampSuccess(txId: String) { // finish current onramp flow and show onramp success screen val replaceOnrampScreens = router.stack .filterNot { it is AppRoute.Onramp } .toMutableList() - replaceOnrampScreens.add(AppRoute.OnrampSuccess(externalTxId)) + replaceOnrampScreens.add(AppRoute.OnrampSuccess(txId)) router.replaceAll(*replaceOnrampScreens.toTypedArray()) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt index c2555aa070..3a26bd9f59 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/router/InnerTokenDetailsRouter.kt @@ -18,5 +18,5 @@ internal interface InnerTokenDetailsRouter { fun openStaking(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, yieldId: String) - fun openOnrampSuccess(externalTxId: String) + fun openOnrampSuccess(txId: String) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 6ca825bee9..d70de8f364 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -195,9 +195,9 @@ internal class TokenDetailsModel @Inject constructor( handleBalanceHiding() } - fun onBuyCurrencyDeepLink(externalTxId: String) { + fun onBuyCurrencyDeepLink(txId: String) { if (onrampFeatureToggles.isFeatureEnabled) { - router.openOnrampSuccess(externalTxId) + router.openOnrampSuccess(txId) } else { val currency = cryptoCurrencyStatus?.currency ?: return analyticsEventsHandler.send(TokenScreenAnalyticsEvent.Bought(currency.symbol)) 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 680e373b9b..9bb3ac509c 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 @@ -39,7 +39,7 @@ internal interface WalletContentClickIntents { fun onOrganizeTokensClick() - fun onOnrampSuccessClick(externalTxId: String) + fun onOnrampSuccessClick(txId: String) fun onDismissMarketsOnboarding() @@ -117,8 +117,8 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } - override fun onOnrampSuccessClick(externalTxId: String) { - router.openOnrampSuccessScreen(externalTxId = externalTxId) + override fun onOnrampSuccessClick(txId: String) { + router.openOnrampSuccessScreen(txId = txId) } override fun onDismissMarketsOnboarding() { 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 23ac155b8b..8028924122 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 @@ -67,13 +67,13 @@ internal class DefaultWalletRouter @Inject constructor( } } - override fun openOnrampSuccessScreen(externalTxId: String) { + override fun openOnrampSuccessScreen(txId: String) { // finish current onramp flow and show onramp success screen val replaceOnrampScreens = router.stack .filterNot { it is AppRoute.Onramp } .toMutableList() - replaceOnrampScreens.add(AppRoute.OnrampSuccess(externalTxId)) + replaceOnrampScreens.add(AppRoute.OnrampSuccess(txId)) router.replaceAll(*replaceOnrampScreens.toTypedArray()) } 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 ad25778b8a..7d9d61c701 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 @@ -33,8 +33,8 @@ internal interface InnerWalletRouter { /** Open onboarding screen */ fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean = false) - /** Open onramp success screen for [externalTxId] */ - fun openOnrampSuccessScreen(externalTxId: String) + /** Open onramp success screen for [txId] */ + fun openOnrampSuccessScreen(txId: String) /** Open transaction history website by [url] */ fun openUrl(url: String) From cb23c1dea37dd9769a9aff8ef949f36a595c677c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Apr 2025 15:03:17 +0300 Subject: [PATCH 081/151] Updated on 2026-08-14 --- common/test/build.gradle.kts | 1 + .../MockYieldBalanceWrapperDTOFactory.kt | 2 +- .../local/token/DefaultStakingYieldsStore.kt | 11 +++++++++++ .../local/token/StakingYieldsStore.kt | 2 ++ .../single/DefaultSingleYieldBalanceProducer.kt | 2 +- .../staking/store/DefaultYieldsBalancesStore.kt | 2 +- .../data/staking/store/YieldsBalancesStore.kt | 3 +-- .../data/staking/utils/StakingIdFactory.kt | 17 +++++++++++++++-- .../DefaultMultiYieldBalanceProducerTest.kt | 3 ++- .../DefaultSingleYieldBalanceProducerTest.kt | 4 ++-- .../YieldsBalancesStoreUpdateMethodsTest.kt | 3 ++- .../tangem/domain/staking/model/StakingID.kt | 4 ++++ 12 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt diff --git a/common/test/build.gradle.kts b/common/test/build.gradle.kts index f2de491c8c..62d076fdff 100644 --- a/common/test/build.gradle.kts +++ b/common/test/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.staking.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) 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 71ad65f3d2..1616a4c847 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 @@ -1,11 +1,11 @@ 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 com.tangem.domain.staking.model.StakingID import java.math.BigDecimal /** diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt index 874e884f87..73800ea6dd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingYieldsStore.kt @@ -4,6 +4,8 @@ import androidx.datastore.core.DataStore import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withTimeoutOrNull +import kotlin.time.Duration.Companion.seconds internal class DefaultStakingYieldsStore( private val dataStore: DataStore>, @@ -17,9 +19,18 @@ internal class DefaultStakingYieldsStore( return dataStore.data.firstOrNull().orEmpty() } + override suspend fun getSyncWithTimeout(): List? { + return withTimeoutOrNull(timeout = YIELDS_WAITING_TIMEOUT, block = { get().firstOrNull() }) + } + override suspend fun store(items: List) { dataStore.updateData { _ -> items } } + + private companion object { + + val YIELDS_WAITING_TIMEOUT = 15.seconds + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt index 428aa1986c..29d2e37b01 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingYieldsStore.kt @@ -9,5 +9,7 @@ interface StakingYieldsStore { suspend fun getSync(): List + suspend fun getSyncWithTimeout(): List? + suspend fun store(items: List) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt index 3327ef9790..e2bb5cc604 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducer.kt @@ -1,7 +1,7 @@ package com.tangem.data.staking.single -import com.tangem.data.staking.store.YieldsBalancesStore.StakingID import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier 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 index 32beb14ec3..c3a8b2a74c 100644 --- 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 @@ -1,11 +1,11 @@ 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.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider 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 index 2e5c062f3d..3371d07f1c 100644 --- 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 @@ -1,6 +1,7 @@ package com.tangem.data.staking.store import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -26,6 +27,4 @@ interface YieldsBalancesStore { /** 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/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt index 7e8362efb5..9caf2f53fe 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/StakingIdFactory.kt @@ -3,7 +3,7 @@ package com.tangem.data.staking.utils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.blockchainsdk.utils.toMigratedCoinId -import com.tangem.data.staking.store.YieldsBalancesStore.StakingID +import com.tangem.domain.staking.model.StakingID import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade @@ -31,12 +31,25 @@ internal class StakingIdFactory @Inject constructor( } } + suspend fun createForDefault( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + network: Network, + ): StakingID? { + val address = walletManagersFacade.getDefaultAddress(userWalletId = userWalletId, network = network) + val integrationId = createIntegrationId(currencyId) + + if (address == null || integrationId == null) return null + + return StakingID(integrationId = integrationId, address = address) + } + fun createIntegrationId(currencyId: CryptoCurrency.ID): String? { val integrationKey = with(currencyId) { rawNetworkId.plus(rawCurrencyId) } return integrationIdMap[integrationKey] } - @Suppress("UnusedPrivateMember") + @Suppress("UnusedPrivateMember", "unused") companion object { private const val TON_INTEGRATION_ID = "ton-ton-chorus-one-pools-staking" 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 index a545c87d92..d558475162 100644 --- 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 @@ -5,6 +5,7 @@ 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.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.wallets.models.UserWalletId @@ -182,7 +183,7 @@ internal class DefaultMultiYieldBalanceProducerTest { private companion object { val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId - val solanaId = YieldsBalancesStore.StakingID( + val solanaId = StakingID( integrationId = "solana-sol-native-multivalidator-staking", address = "0x1", ) diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt index 3522d505db..923d406900 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceProducerTest.kt @@ -5,9 +5,9 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.utils.getEmittedValues -import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.toDomain import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.multi.MultiYieldBalanceProducer import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier @@ -229,7 +229,7 @@ internal class DefaultSingleYieldBalanceProducerTest { val ton = mocks.createCoin(Blockchain.TON) val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId - val solanaId = YieldsBalancesStore.StakingID( + val solanaId = StakingID( integrationId = "solana-sol-native-multivalidator-staking", address = "0x1", ) 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 index f64a6da8e8..8c51eacb19 100644 --- 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 @@ -7,6 +7,7 @@ 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.StakingID import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider @@ -179,7 +180,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { val stakingIds = setOf( stakingId, - YieldsBalancesStore.StakingID( + StakingID( integrationId = "solana-sol-native-multivalidator-staking", address = "0x1", ), diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt new file mode 100644 index 0000000000..6313b9277c --- /dev/null +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/StakingID.kt @@ -0,0 +1,4 @@ +package com.tangem.domain.staking.model + +// TODO: make part of YieldBalance in the future +data class StakingID(val integrationId: String, val address: String) \ No newline at end of file From 538757a7ceca494fb4d8461e4c9af8f1fbebeb73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 11:52:10 +0300 Subject: [PATCH 082/151] Updated on 2026-08-14 --- .../tasks/visa/VisaCardActivationTask.kt | 103 +++++++++++----- .../visa/VisaCustomerWalletApproveTask.kt | 19 ++- .../tap/domain/visa/VisaCardScanHandler.kt | 11 +- .../api/common/config/TangemVisa.kt | 2 +- .../api/common/config/TangemVisaAuth.kt | 2 +- .../datasource/api/visa/TangemVisaApi.kt | 46 +++---- .../datasource/api/visa/TangemVisaAuthApi.kt | 35 +++--- .../request/ActivationByCardWalletRequest.kt | 15 +-- .../ActivationByCustomerWalletRequest.kt | 13 +- .../models/request/ActivationStatusRequest.kt | 10 ++ .../request/GenerateNoneByCardIdRequest.kt | 11 ++ .../GenerateNoneByCardWalletRequest.kt | 10 ++ .../request/GetAccessTokenByCardIdRequest.kt | 12 ++ .../GetAccessTokenByCardWalletRequest.kt | 11 ++ .../request/GetCardWalletAcceptanceRequest.kt | 11 ++ .../GetCustomerWalletAcceptanceRequest.kt | 11 ++ .../request/RefreshTokenByCardIdRequest.kt | 10 ++ .../RefreshTokenByCardWalletRequest.kt | 10 ++ .../visa/models/request/SetPinCodeRequest.kt | 16 +-- .../CardActivationRemoteStateResponse.kt | 14 ++- .../CustomerWalletDataToSignResponse.kt | 14 --- .../models/response/GenerateNonceResponse.kt | 11 +- .../api/visa/models/response/JWTResponse.kt | 23 ++-- ...nResponse.kt => VisaDataToSignResponse.kt} | 6 +- .../managers/ProdApiConfigsManagerTest.kt | 4 +- .../visa/DefaultVisaActivationRepository.kt | 115 ++++++------------ .../data/visa/DefaultVisaAuthRepository.kt | 108 ++++++++-------- .../data/visa/MockVisaActivationRepository.kt | 7 +- .../VisaActivationStatusConverter.kt | 60 --------- .../VisaActivationStatusConverterWithState.kt | 81 ++++++++++++ .../com/tangem/data/visa/di/VisaDataModule.kt | 18 +-- .../data/visa/utils/VisaApiRequestMaker.kt | 13 +- .../common/visa/VisaWalletPublicKeyUtility.kt | 2 +- domain/visa/build.gradle.kts | 3 + .../com/tangem/domain/visa/error/VisaError.kt | 1 + .../visa/model/VisaActivationOrderInfo.kt | 1 + .../visa/model/VisaActivationRemoteState.kt | 52 ++++---- .../domain/visa/model/VisaAuthTokens.kt | 11 +- .../model/VisaCardWalletDataToSignRequest.kt | 11 +- .../VisaCustomerWalletDataToSignRequest.kt | 1 + .../visa/model/VisaDataToSignByCardWallet.kt | 3 +- .../VisaSignedActivationDataByCardWallet.kt | 1 - .../domain/visa/SetVisaPinCodeUseCase.kt | 30 +++-- .../repository/VisaActivationRepository.kt | 2 - .../impl/DefaultOnboardingVisaComponent.kt | 6 +- .../model/OnboardingVisaAccessCodeModel.kt | 26 ++-- .../model/OnboardingVisaApproveModel.kt | 5 +- .../model/OnboardingVisaInProgressModel.kt | 27 +++- .../model/OnboardingVisaOtherWalletModel.kt | 4 +- .../model/OnboardingVisaPinCodeModel.kt | 41 +------ .../child/pincode/model/PinCodeValidation.kt | 5 +- .../model/OnboardingVisaWelcomeModel.kt | 13 +- .../model/analytics/VisaAnalyticsEvent.kt | 10 +- .../v2/visa/impl/model/OnboardingVisaModel.kt | 23 ++-- 54 files changed, 622 insertions(+), 488 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardIdRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardWalletRequest.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CustomerWalletDataToSignResponse.kt rename core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/{CardWalletDataToSignResponse.kt => VisaDataToSignResponse.kt} (66%) delete mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 890ce4ba9b..521c1444d8 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -4,14 +4,12 @@ import arrow.core.Either import arrow.core.getOrElse import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve -import com.tangem.common.core.CardSession -import com.tangem.common.core.CardSessionRunnable -import com.tangem.common.core.CompletionCallback -import com.tangem.common.core.TangemError -import com.tangem.common.core.TangemSdkError +import com.tangem.common.core.* import com.tangem.common.extensions.hexToBytes +import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.common.extensions.toHexString import com.tangem.common.map import com.tangem.common.timemeasure.RealtimeMonotonicTimeSource @@ -46,7 +44,7 @@ import timber.log.Timber import kotlin.coroutines.resume import kotlin.time.measureTimedValue -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") class VisaCardActivationTask @AssistedInject constructor( @Assisted private val mode: VisaCardActivationTaskMode, @Assisted private val activationInput: VisaActivationInput, @@ -97,7 +95,23 @@ class VisaCardActivationTask @AssistedInject constructor( context.signAuthorizationChallenge(mode.authorizationChallenge) } is VisaCardActivationTaskMode.SignOnly -> { - context.signData(mode.dataToSignByCardWallet) + val wallet = + card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } + ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + val derivedPublicKey = when (val deriveKeyResult = context.deriveKey(wallet.publicKey)) { + is CompletionResult.Failure -> { + return CompletionResult.Failure(deriveKeyResult.error) + } + is CompletionResult.Success -> { + deriveKeyResult.data + } + } + + context.signData( + mode.dataToSignByCardWallet, + derivedPublicKey, + ) } } } @@ -138,9 +152,35 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.processSignedAuthorizationChallenge( signedChallenge: VisaAuthSignedChallenge, ): CompletionResult { + when (val createWalletResult = createWallet()) { + is CompletionResult.Failure -> return CompletionResult.Failure(createWalletResult.error) + is CompletionResult.Success -> {} + } + + val card = + session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + val wallet = + card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } + ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + + val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) { + is CompletionResult.Failure -> { + return CompletionResult.Failure(deriveKeyResult.error) + } + is CompletionResult.Success -> { + deriveKeyResult.data + } + } + + val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey) + .getOrElse { return CompletionResult.Failure(it.tangemError) } + .value + return coroutineScope { - val dataToSignDeferred = async { getDataToSign(signedChallenge) } - val otpTaskDeferred = async { createWallet() } + val dataToSignDeferred = + async { getDataToSign(signedChallenge = signedChallenge, cardWalletAddress = walletAddress) } + val otpTaskDeferred = async { createOTP() } val dataToSign = dataToSignDeferred.await() .getOrElse { @@ -150,12 +190,16 @@ class VisaCardActivationTask @AssistedInject constructor( otpTaskDeferred.await() - signData(dataToSign) + signData( + dataToSign = dataToSign, + derivedPublicKey = derivedPublicKey, + ) } } private suspend fun SessionContext.getDataToSign( signedChallenge: VisaAuthSignedChallenge, + cardWalletAddress: String, ): Either = either { catch( block = { @@ -168,7 +212,12 @@ class VisaCardActivationTask @AssistedInject constructor( raise(VisaActivationError.WrongRemoteState.tangemError) } - visaActivationRepository.getCardWalletAcceptanceData(remoteState.request) + visaActivationRepository.getCardWalletAcceptanceData( + VisaCardWalletDataToSignRequest( + activationOrderInfo = remoteState.activationOrderInfo, + cardWalletAddress = cardWalletAddress, + ), + ) }, catch = { raise(VisaAuthorizationAPIError.tangemError) @@ -182,7 +231,7 @@ class VisaCardActivationTask @AssistedInject constructor( val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) return if (card.wallets.any { it.curve == EllipticCurve.Secp256k1 }) { - createOTP() + CompletionResult.Success(Unit) } else { val createWalletTask = CreateWalletTask(EllipticCurve.Secp256k1) @@ -199,7 +248,7 @@ class VisaCardActivationTask @AssistedInject constructor( when (val result = timedResult.value) { is CompletionResult.Success -> { Timber.i("CreateWalletTask success") - createOTP() + CompletionResult.Success(Unit) } is CompletionResult.Failure -> { Timber.e("CreateWalletTask failure ${result.error}") @@ -245,26 +294,15 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.signData( dataToSign: VisaDataToSignByCardWallet, + derivedPublicKey: ExtendedPublicKey, ): CompletionResult { val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) + val wallet = card.wallets.firstOrNull { it.curve == EllipticCurve.Secp256k1 } ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead()) - val derivedPublicKey = when (val deriveKeyResult = deriveKey(wallet.publicKey)) { - is CompletionResult.Failure -> { - return CompletionResult.Failure(deriveKeyResult.error) - } - is CompletionResult.Success -> { - deriveKeyResult.data - } - } - - val walletAddress = VisaWalletPublicKeyUtility.generateAddressOnSecp256k1(derivedPublicKey.publicKey) - .getOrElse { return CompletionResult.Failure(it.tangemError) } - .value - val task = SignHashCommand( hash = dataToSign.hashToSign.hexToBytes(), walletPublicKey = wallet.publicKey, @@ -286,8 +324,8 @@ class VisaCardActivationTask @AssistedInject constructor( Timber.i("SignHashCommand success") handleSignedData( dataToSign = dataToSign, - walletAddress = walletAddress, response = result.data, + derivedPublicKey = derivedPublicKey, ) } is CompletionResult.Failure -> { @@ -313,7 +351,7 @@ class VisaCardActivationTask @AssistedInject constructor( private suspend fun SessionContext.handleSignedData( dataToSign: VisaDataToSignByCardWallet, - walletAddress: String, + derivedPublicKey: ExtendedPublicKey, response: SignHashResponse, ): CompletionResult { val otp = otpStorage.getOTP(cardId) ?: run { @@ -321,11 +359,16 @@ class VisaCardActivationTask @AssistedInject constructor( otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(VisaActivationError.MissingRootOTP.tangemError) } + val rsvSignature = EthereumUtils.prepareSignedMessageData( + signedHash = response.signature, + hashToSign = dataToSign.hashToSign.hexToBytes(), + publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(), + ) + val signedActivationData = dataToSign.sign( - cardWalletAddress = walletAddress, rootOTP = otp.rootOTP.toHexString(), otpCounter = otp.counter, - signature = response.signature.toHexString(), + signature = rsvSignature, ) return setupAccessCode().map { diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 18ec47b7fb..2fb700cbd7 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -1,6 +1,7 @@ package com.tangem.tap.domain.tasks.visa import arrow.core.getOrElse +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.CardWallet @@ -10,7 +11,7 @@ import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes -import com.tangem.common.extensions.toHexString +import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey @@ -129,6 +130,7 @@ class VisaCustomerWalletApproveTask( targetWalletPublicKey = wallet.publicKey, derivationPath = derivationPath, session = session, + extendedPublicKey = extendedPublicKey, callback = callback, ) } @@ -149,6 +151,7 @@ class VisaCustomerWalletApproveTask( signApproveData( targetWalletPublicKey = publicKey, derivationPath = null, + extendedPublicKey = null, session = session, callback = callback, ) @@ -157,11 +160,14 @@ class VisaCustomerWalletApproveTask( private fun signApproveData( targetWalletPublicKey: ByteArray, derivationPath: DerivationPath?, + extendedPublicKey: ExtendedPublicKey?, session: CardSession, callback: CompletionCallback, ) { + val hashToSign = visaDataForApprove.dataToSign.hashToSign.hexToBytes() + val signTask = SignHashCommand( - hash = visaDataForApprove.dataToSign.hashToSign.hexToBytes(), + hash = hashToSign, walletPublicKey = targetWalletPublicKey, derivationPath = derivationPath, ) @@ -169,11 +175,18 @@ class VisaCustomerWalletApproveTask( signTask.run(session) { result -> when (result) { is CompletionResult.Success -> { + val rsvSignature = EthereumUtils.prepareSignedMessageData( + signedHash = result.data.signature, + hashToSign = hashToSign, + publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() + ?: targetWalletPublicKey.toDecompressedPublicKey(), + ) + scanCard( session = session, callback = callback, signedData = visaDataForApprove.dataToSign.sign( - signature = result.data.signature.toHexString(), + signature = rsvSignature, customerWalletAddress = visaDataForApprove.targetAddress, ), ) diff --git a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt index ad20167f85..b83d23f8d9 100644 --- a/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt +++ b/app/src/main/java/com/tangem/tap/domain/visa/VisaCardScanHandler.kt @@ -128,6 +128,8 @@ internal class VisaCardScanHandler @Inject constructor( Timber.i("Requesting challenge for wallet authorization") val challengeResponse = runCatching { + // TODO [REDACTED_TASK_KEY] + error("sign and get specific error to switch to card_id flow") visaAuthRepository.getCardWalletAuthChallenge(cardWalletAddress = walletAddress.value) }.getOrElse { Timber.i( @@ -180,6 +182,7 @@ internal class VisaCardScanHandler @Inject constructor( return CompletionResult.Success(VisaCardActivationStatus.Activated(authorizationTokensResponse)) } + @Suppress("LongMethod") private suspend fun SessionContext.handleCardAuthorization( cardWalletAddress: String, ): CompletionResult { @@ -232,11 +235,17 @@ internal class VisaCardScanHandler @Inject constructor( tokens = authorizationTokensResponse, ) - val activationRemoteState = visaActivationRepository.getActivationRemoteState() + val activationRemoteState = runCatching { + visaActivationRepository.getActivationRemoteState() + }.getOrElse { + Timber.e("Failed to sign challenge with Card public key. Plain error: ${it.message}") + return CompletionResult.Failure(VisaAuthorizationAPIError.tangemError) + } val error = when (activationRemoteState) { VisaActivationRemoteState.BlockedForActivation -> VisaActivationError.BlockedForActivation VisaActivationRemoteState.Activated -> VisaActivationError.InvalidActivationState + VisaActivationRemoteState.Failed -> VisaActivationError.FailedRemoteState else -> null } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt index 9c1f78bb43..5310f5b308 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt @@ -15,7 +15,7 @@ internal class TangemVisa( private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://bff.tangem.com/", + baseUrl = "[REDACTED_ENV_URL]", headers = createHeaders(), ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt index e0a9068c02..833cde2111 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt @@ -15,7 +15,7 @@ internal class TangemVisaAuth( private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.STAGE, - baseUrl = "https://api-s.tangem.org/", + baseUrl = "[REDACTED_ENV_URL]", headers = createHeaders(), ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt index a0d08a74a1..16e643329b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt @@ -1,9 +1,11 @@ package com.tangem.datasource.api.visa import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.utils.ReadTimeout import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest +import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest +import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest +import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest import com.tangem.datasource.api.visa.models.response.* import retrofit2.http.Body @@ -11,60 +13,40 @@ import retrofit2.http.GET import retrofit2.http.Header import retrofit2.http.POST import retrofit2.http.Query -import java.util.concurrent.TimeUnit interface TangemVisaApi { - @GET("product_instance/activation_status") + @POST("v1/activation/status") suspend fun getRemoteActivationStatus( @Header("Authorization") authHeader: String, - @Query("customer_id") customerId: String, - @Query("product_instance_id") productInstanceId: String, - @Query("card_id") cardId: String, - @Query("card_public_key") cardPublicKey: String, + @Body request: ActivationStatusRequest, ): ApiResponse - @ReadTimeout(duration = 20, TimeUnit.MINUTES) - @GET("product_instance/activation_status") - suspend fun getRemoteActivationStatusLongPoll( - @Header("Authorization") authHeader: String, - @Query("customer_id") customerId: String, - @Query("product_instance_id") productInstanceId: String, - @Query("card_id") cardId: String, - @Query("card_public_key") cardPublicKey: String, - ): ApiResponse - - @GET("product_instance/card_wallet_acceptance") + @POST("v1/activation/acceptance/message") suspend fun getCardWalletAcceptance( @Header("Authorization") authHeader: String, - @Query("customer_id") customerId: String, - @Query("product_instance_id") productInstanceId: String, - @Query("activation_order") activationOrderId: String, - @Query("customer_wallet_address") customerWalletAddress: String, - ): ApiResponse + @Body request: GetCardWalletAcceptanceRequest, + ): ApiResponse - @GET("product_instance/customer_wallet_acceptance") + @POST("v1/activation/acceptance/message") suspend fun getCustomerWalletAcceptance( @Header("Authorization") authHeader: String, - @Query("customer_id") customerId: String, - @Query("product_instance_id") productInstanceId: String, - @Query("activation_order") activationOrderId: String, - @Query("card_wallet_address") cardWalletAddress: String, - ): ApiResponse + @Body request: GetCustomerWalletAcceptanceRequest, + ): ApiResponse - @POST("product_instance/activation_by_card_wallet") + @POST("v1/activation/data") suspend fun activateByCardWallet( @Header("Authorization") authHeader: String, @Body body: ActivationByCardWalletRequest, ): ApiResponse - @POST("product_instance/activation_by_customer_wallet") + @POST("v1/activation/data") suspend fun activateByCustomerWallet( @Header("Authorization") authHeader: String, @Body body: ActivationByCustomerWalletRequest, ): ApiResponse - @POST("product_instance/issuer_activation") + @POST("v1/activation/pin") suspend fun setPinCode( @Header("Authorization") authHeader: String, @Body body: SetPinCodeRequest, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt index b2ed8d19be..57b91ca1c3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt @@ -1,32 +1,29 @@ package com.tangem.datasource.api.visa import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.visa.models.request.* import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse import com.tangem.datasource.api.visa.models.response.JWTResponse -import retrofit2.http.Field +import retrofit2.http.Body import retrofit2.http.POST -import retrofit2.http.Query interface TangemVisaAuthApi { - @POST("auth/card_wallet") - suspend fun generateNonceByWalletAddress( - @Query("card_wallet_address") cardWalletAddress: String, - ): GenerateNonceResponse + @POST("v1/auth/challenge") + suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse - @POST("auth/card_id") - suspend fun generateNonceByCard( - @Query("card_id") cardId: String, - @Query("card_public_key") cardPublicKey: String, - ): GenerateNonceResponse + @POST("v1/auth/challenge") + suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse - @POST("auth/get_token") - suspend fun getAccessToken( - @Query("session_id") sessionId: String, - @Query("signature") signature: String, - @Query("salt") salt: String?, - ): JWTResponse + @POST("v1/auth/token") + suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse - @POST("auth/refresh_token") - suspend fun refreshAccessToken(@Field("refresh_token") refreshToken: String): ApiResponse + @POST("v1/auth/token") + suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse + + @POST("v1/auth/token/refresh") + suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse + + @POST("v1/auth/token/refresh") + suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt index d19f87f592..c16198f0c6 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCardWalletRequest.kt @@ -5,22 +5,15 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ActivationByCardWalletRequest( - @Json(name = "customer_id") val customerId: String, - @Json(name = "product_instance_id") val productInstanceId: String, - @Json(name = "activation_order_id") val activationOrderId: String, - @Json(name = "data") val data: Data, + @Json(name = "order_id") val orderId: String, + @Json(name = "card_wallet") val cardWallet: CardWallet, + @Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String, + @Json(name = "otp") val otp: Otp, ) { - @JsonClass(generateAdapter = true) - data class Data( - @Json(name = "card_wallet") val cardWallet: CardWallet, - @Json(name = "otp") val otp: Otp, - ) - @JsonClass(generateAdapter = true) data class CardWallet( @Json(name = "address") val address: String, @Json(name = "card_wallet_confirmation") val cardWalletConfirmation: CardWalletConfirmation?, - @Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String, ) @JsonClass(generateAdapter = true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt index 47bdbc4132..ace2090603 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationByCustomerWalletRequest.kt @@ -5,19 +5,12 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ActivationByCustomerWalletRequest( - @Json(name = "customer_id") val customerId: String, - @Json(name = "product_instance_id") val productInstanceId: String, - @Json(name = "activation_order_id") val activationOrderId: String, - @Json(name = "data") val data: Data, + @Json(name = "order_id") val orderId: String, + @Json(name = "customer_wallet") val customerWallet: CustomerWallet, ) { - @JsonClass(generateAdapter = true) - data class Data( - @Json(name = "customer_wallet") val customerWallet: CustomerWallet, - ) - @JsonClass(generateAdapter = true) data class CustomerWallet( - @Json(name = "address") val address: String, @Json(name = "deploy_acceptance_signature") val deployAcceptanceSignature: String, + @Json(name = "address") val customerWalletAddress: String, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt new file mode 100644 index 0000000000..7ce0614abc --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/ActivationStatusRequest.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ActivationStatusRequest( + @Json(name = "card_id") val cardId: String, + @Json(name = "card_public_key") val cardPublicKey: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt new file mode 100644 index 0000000000..d3aa5eaa50 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardIdRequest.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GenerateNoneByCardIdRequest( + @Json(name = "auth_type") val authType: String = "card_id", + @Json(name = "card_id") val cardId: String, + @Json(name = "card_public_key") val cardPublicKey: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt new file mode 100644 index 0000000000..172c39e727 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GenerateNoneByCardWalletRequest.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GenerateNoneByCardWalletRequest( + @Json(name = "auth_type") val authType: String = "card_wallet", + @Json(name = "card_wallet_address") val cardWalletAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt new file mode 100644 index 0000000000..c212c81412 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardIdRequest.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetAccessTokenByCardIdRequest( + @Json(name = "auth_type") val authType: String = "card_id", + @Json(name = "session_id") val sessionId: String, + @Json(name = "signature") val signature: String, + @Json(name = "salt") val salt: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt new file mode 100644 index 0000000000..c80e97b0c9 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetAccessTokenByCardWalletRequest.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetAccessTokenByCardWalletRequest( + @Json(name = "auth_type") val authType: String = "card_wallet", + @Json(name = "session_id") val sessionId: String, + @Json(name = "signature") val signature: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt new file mode 100644 index 0000000000..dd084fa326 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCardWalletAcceptanceRequest.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetCardWalletAcceptanceRequest( + @Json(name = "type") val type: String = "card_wallet", + @Json(name = "customer_wallet_address") val customerWalletAddress: String, + @Json(name = "card_wallet_address") val cardWalletAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt new file mode 100644 index 0000000000..3948fc43b3 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/GetCustomerWalletAcceptanceRequest.kt @@ -0,0 +1,11 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class GetCustomerWalletAcceptanceRequest( + @Json(name = "type") val type: String = "customer_wallet", + @Json(name = "customer_wallet_address") val customerWalletAddress: String, + @Json(name = "card_wallet_address") val cardWalletAddress: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardIdRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardIdRequest.kt new file mode 100644 index 0000000000..f94a59a280 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardIdRequest.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RefreshTokenByCardIdRequest( + @Json(name = "auth_type") val authType: String = "card_id", + @Json(name = "refresh_token") val refreshToken: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardWalletRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardWalletRequest.kt new file mode 100644 index 0000000000..108469e1b6 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/RefreshTokenByCardWalletRequest.kt @@ -0,0 +1,10 @@ +package com.tangem.datasource.api.visa.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class RefreshTokenByCardWalletRequest( + @Json(name = "auth_type") val authType: String = "card_wallet", + @Json(name = "refresh_token") val refreshToken: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/SetPinCodeRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/SetPinCodeRequest.kt index c3a5290c8c..69e1a5ac08 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/SetPinCodeRequest.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/request/SetPinCodeRequest.kt @@ -5,14 +5,8 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class SetPinCodeRequest( - @Json(name = "customer_id") val customerId: String, - @Json(name = "activation_order_id") val activationOrderId: String, - @Json(name = "product_instance_id") val productInstanceId: String, - @Json(name = "data") val data: Data, -) { - data class Data( - @Json(name = "session_key") val sessionKey: String, - @Json(name = "iv") val iv: String, - @Json(name = "encrypted_pin") val encryptedPin: String, - ) -} \ No newline at end of file + @Json(name = "order_id") val orderId: String, + @Json(name = "session_id") val sessionId: String, + @Json(name = "iv") val iv: String, + @Json(name = "pin") val pin: String, +) \ No newline at end of file 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 aeb498dac8..4134fa9c0e 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 @@ -5,15 +5,21 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) 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?, + @Json(name = "result") val result: Result, ) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "status") val status: String, + @Json(name = "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, + @Json(name = "card_wallet_address") val cardWalletAddress: String, ) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CustomerWalletDataToSignResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CustomerWalletDataToSignResponse.kt deleted file mode 100644 index e4575a1587..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CustomerWalletDataToSignResponse.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.datasource.api.visa.models.response - -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass - -@JsonClass(generateAdapter = true) -data class CustomerWalletDataToSignResponse( - @Json(name = "data_for_customer_wallet") val dataForCardWallet: Data, -) { - @JsonClass(generateAdapter = true) - data class Data( - @Json(name = "hash") val hash: String, - ) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt index 002e43b16e..9a79a457b8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/GenerateNonceResponse.kt @@ -5,6 +5,11 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class GenerateNonceResponse( - @Json(name = "nonce") val nonce: String, - @Json(name = "session_id") val sessionId: String, -) \ No newline at end of file + @Json(name = "result") val result: Result, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "nonce") val nonce: String, + @Json(name = "session_id") val sessionId: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt index e4fc0d2579..0380074d02 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/JWTResponse.kt @@ -5,12 +5,17 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class JWTResponse( - @Json(name = "access_token") val accessToken: String, - @Json(name = "expires_in") val expiresIn: Int, - @Json(name = "refresh_expires_in") val refreshExpiresIn: Int, - @Json(name = "refresh_token") val refreshToken: String, - @Json(name = "token_type") val tokenType: String, - @Json(name = "not-before-policy") val notBeforePolicy: Int, - @Json(name = "session_state") val sessionState: String, - @Json(name = "scope") val scope: String, -) \ No newline at end of file + @Json(name = "result") val result: Result, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "access_token") val accessToken: String, + @Json(name = "expires_in") val expiresIn: Int, + @Json(name = "refresh_expires_in") val refreshExpiresIn: Int, + @Json(name = "refresh_token") val refreshToken: String, + @Json(name = "token_type") val tokenType: String, + @Json(name = "not-before-policy") val notBeforePolicy: Int, + @Json(name = "session_state") val sessionState: String, + @Json(name = "scope") val scope: String, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardWalletDataToSignResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt similarity index 66% rename from core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardWalletDataToSignResponse.kt rename to core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt index 288ab32e39..a19d3e7ca4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/CardWalletDataToSignResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/models/response/VisaDataToSignResponse.kt @@ -4,11 +4,11 @@ import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class CardWalletDataToSignResponse( - @Json(name = "dataForCardWallet") val dataForCardWallet: Data, +data class VisaDataToSignResponse( + @Json(name = "result") val result: Result, ) { @JsonClass(generateAdapter = true) - data class Data( + data class Result( @Json(name = "hash") val hash: String, ) } \ No newline at end of file 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 e0df7b4096..6537f31453 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 @@ -186,7 +186,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { id = ApiConfig.ID.TangemVisaAuth, expected = ApiEnvironmentConfig( environment = ApiEnvironment.STAGE, - baseUrl = "https://api-s.tangem.org/", + baseUrl = "[REDACTED_ENV_URL]", headers = mapOf( "version" to ProviderSuspend { VERSION_NAME }, "platform" to ProviderSuspend { "Android" }, @@ -200,7 +200,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { id = ApiConfig.ID.TangemVisa, expected = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, - baseUrl = "https://bff.tangem.com/", + baseUrl = "[REDACTED_ENV_URL]", headers = mapOf( "version" to ProviderSuspend { VERSION_NAME }, "platform" to ProviderSuspend { "Android" }, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index b4bb976ce9..bf72bc979e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -1,13 +1,15 @@ package com.tangem.data.visa import com.tangem.data.visa.config.VisaLibLoader -import com.tangem.data.visa.converter.AccessCodeDataConverter -import com.tangem.data.visa.converter.VisaActivationStatusConverter +import com.tangem.data.visa.converter.VisaActivationStatusConverterWithState import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.visa.TangemVisaApi import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest +import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest +import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest +import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest import com.tangem.datasource.local.visa.VisaAuthTokenStorage import com.tangem.domain.visa.exception.RefreshTokenExpiredException @@ -25,9 +27,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( @Assisted private val visaCardId: VisaCardId, private val visaApi: TangemVisaApi, private val dispatcherProvider: CoroutineDispatcherProvider, - private val visaActivationStatusConverter: VisaActivationStatusConverter, private val visaAuthTokenStorage: VisaAuthTokenStorage, - private val accessCodeDataConverter: AccessCodeDataConverter, private val visaAuthRepository: VisaAuthRepository, private val visaLibLoader: VisaLibLoader, ) : VisaActivationRepository { @@ -36,59 +36,38 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( val result = request { val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - val accessCodeData = accessCodeDataConverter.convert(authTokens) visaApi.getRemoteActivationStatus( authHeader = authTokens.getAuthHeader(), - customerId = accessCodeData.customerId, - productInstanceId = accessCodeData.productInstanceId, - cardId = visaCardId.cardId, - cardPublicKey = visaCardId.cardPublicKey, + request = ActivationStatusRequest( + cardId = visaCardId.cardId, + cardPublicKey = visaCardId.cardPublicKey, + ), ).getOrThrow() } - visaActivationStatusConverter.convert(result) + VisaActivationStatusConverterWithState.convert(result) } - override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState = - withContext(dispatcherProvider.io) { - val result = request { - val authTokens = - checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - val accessCodeData = accessCodeDataConverter.convert(authTokens) - - visaApi.getRemoteActivationStatusLongPoll( - authHeader = authTokens.getAuthHeader(), - customerId = accessCodeData.customerId, - productInstanceId = accessCodeData.productInstanceId, - cardId = visaCardId.cardId, - cardPublicKey = visaCardId.cardPublicKey, - ).getOrThrow() - } - - visaActivationStatusConverter.convert(result) - } - override suspend fun getCardWalletAcceptanceData( request: VisaCardWalletDataToSignRequest, ): VisaDataToSignByCardWallet = withContext(dispatcherProvider.io) { val result = request { val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - val accessCodeData = accessCodeDataConverter.convert(authTokens) visaApi.getCardWalletAcceptance( authHeader = authTokens.getAuthHeader(), - customerId = accessCodeData.customerId, - productInstanceId = accessCodeData.productInstanceId, - activationOrderId = request.orderId, - customerWalletAddress = request.customerWalletAddress, + request = GetCardWalletAcceptanceRequest( + customerWalletAddress = request.activationOrderInfo.customerWalletAddress, + cardWalletAddress = request.cardWalletAddress, + ), ).getOrThrow() } VisaDataToSignByCardWallet( request = request, - hashToSign = result.dataForCardWallet.hash, + hashToSign = result.result.hash, ) } @@ -98,20 +77,19 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( val result = request { val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - val accessCodeData = accessCodeDataConverter.convert(authTokens) visaApi.getCustomerWalletAcceptance( authHeader = authTokens.getAuthHeader(), - customerId = accessCodeData.customerId, - productInstanceId = accessCodeData.productInstanceId, - activationOrderId = request.orderId, - cardWalletAddress = request.cardWalletAddress, + request = GetCustomerWalletAcceptanceRequest( + cardWalletAddress = request.cardWalletAddress, + customerWalletAddress = request.customerWalletAddress, + ), ).getOrThrow() } VisaDataToSignByCustomerWallet( request = request, - hashToSign = result.dataForCardWallet.hash, + hashToSign = result.result.hash, ) } @@ -120,27 +98,22 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( request { val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - val accessCodeData = accessCodeDataConverter.convert(authTokens) visaApi.activateByCardWallet( authHeader = authTokens.getAuthHeader(), body = ActivationByCardWalletRequest( - customerId = accessCodeData.customerId, - productInstanceId = accessCodeData.productInstanceId, - activationOrderId = signedData.dataToSign.request.orderId, - data = ActivationByCardWalletRequest.Data( - cardWallet = ActivationByCardWalletRequest.CardWallet( - address = signedData.cardWalletAddress, - cardWalletConfirmation = null, // for second iteration - deployAcceptanceSignature = signedData.signature, - ), - otp = ActivationByCardWalletRequest.Otp( - rootOtp = signedData.rootOTP, - counter = signedData.otpCounter, - ), + orderId = signedData.dataToSign.request.activationOrderInfo.orderId, + cardWallet = ActivationByCardWalletRequest.CardWallet( + address = signedData.dataToSign.request.cardWalletAddress, + cardWalletConfirmation = null, // for second iteration + ), + deployAcceptanceSignature = signedData.signature, + otp = ActivationByCardWalletRequest.Otp( + rootOtp = signedData.rootOTP, + counter = signedData.otpCounter, ), ), - ) + ).getOrThrow() } } } @@ -150,22 +123,17 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( request { val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - val accessCodeData = accessCodeDataConverter.convert(authTokens) visaApi.activateByCustomerWallet( authHeader = authTokens.getAuthHeader(), body = ActivationByCustomerWalletRequest( - customerId = accessCodeData.customerId, - productInstanceId = accessCodeData.productInstanceId, - activationOrderId = signedData.dataToSign.request.orderId, - data = ActivationByCustomerWalletRequest.Data( - customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( - address = signedData.customerWalletAddress, - deployAcceptanceSignature = signedData.signature, - ), + orderId = signedData.dataToSign.request.orderId, + customerWallet = ActivationByCustomerWalletRequest.CustomerWallet( + deployAcceptanceSignature = signedData.signature, + customerWalletAddress = signedData.customerWalletAddress, ), ), - ) + ).getOrThrow() } } } @@ -175,21 +143,16 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( request { val authTokens = checkNotNull(visaAuthTokenStorage.get(visaCardId.cardId)) { "Visa auth tokens are not stored" } - val accessCodeData = accessCodeDataConverter.convert(authTokens) visaApi.setPinCode( authHeader = authTokens.getAuthHeader(), body = SetPinCodeRequest( - customerId = accessCodeData.customerId, - productInstanceId = accessCodeData.productInstanceId, - activationOrderId = pinCode.activationOrderId, - data = SetPinCodeRequest.Data( - sessionKey = pinCode.sessionId, - iv = pinCode.iv, - encryptedPin = pinCode.encryptedPin, - ), + orderId = pinCode.activationOrderId, + sessionId = pinCode.sessionId, + iv = pinCode.iv, + pin = pinCode.encryptedPin, ), - ) + ).getOrThrow() } } } diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index 9e5d4a5122..9e3be2327c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -1,8 +1,8 @@ package com.tangem.data.visa -import com.tangem.common.extensions.toHexString -import com.tangem.crypto.CryptoUtils +import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.visa.TangemVisaAuthApi +import com.tangem.datasource.api.visa.models.request.* import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSession import com.tangem.domain.visa.model.VisaAuthSignedChallenge @@ -20,74 +20,80 @@ internal class DefaultVisaAuthRepository @Inject constructor( override suspend fun getCardAuthChallenge(cardId: String, cardPublicKey: String): VisaAuthChallenge.Card = withContext(dispatchers.io) { - // val response = visaAuthApi.generateNonceByCard( - // cardId = cardId, - // cardPublicKey = cardPublicKey, - // ) - // - // VisaAuthChallenge.Card( - // challenge = response.nonce, - // session = VisaAuthSession(response.sessionId), - // ) - + val response = visaAuthApi.generateNonceByCardId( + GenerateNoneByCardIdRequest( + cardId = cardId, + cardPublicKey = cardPublicKey, + ), + ) VisaAuthChallenge.Card( - challenge = CryptoUtils.generateRandomBytes(length = 16).toHexString(), - session = VisaAuthSession("session"), + challenge = response.result.nonce, + session = VisaAuthSession(response.result.sessionId), ) } override suspend fun getCardWalletAuthChallenge(cardWalletAddress: String): VisaAuthChallenge.Wallet = withContext(dispatchers.io) { - // val response = visaAuthApi.generateNonceByWalletAddress( - // customerId = cardId, - // customerWalletAddress = walletPublicKey, - // ) - // - // VisaAuthChallenge.Wallet( - // challenge = response.nonce, - // session = VisaAuthSession(response.sessionId), - // ) + val response = visaAuthApi.generateNonceByCardWallet( + GenerateNoneByCardWalletRequest( + cardWalletAddress = cardWalletAddress, + ), + ) VisaAuthChallenge.Wallet( - challenge = CryptoUtils.generateRandomBytes(length = 32).toHexString(), - session = VisaAuthSession("session"), + challenge = response.result.nonce, + session = VisaAuthSession(response.result.sessionId), ) } override suspend fun getAccessTokens(signedChallenge: VisaAuthSignedChallenge): VisaAuthTokens = withContext(dispatchers.io) { - // val response = when (signedChallenge) { - // is VisaAuthSignedChallenge.ByCardPublicKey -> { - // visaAuthApi.getAccessToken( - // sessionId = signedChallenge.challenge.session.sessionId, - // signature = signedChallenge.signature, - // salt = signedChallenge.salt, - // ) - // } - // is VisaAuthSignedChallenge.ByWallet -> { - // visaAuthApi.getAccessToken( - // sessionId = signedChallenge.challenge.session.sessionId, - // signature = signedChallenge.signature, - // salt = null, - // ) - // } - // } - // - // VisaAuthTokens( - // accessToken = response.accessToken, - // refreshToken = response.refreshToken, - // ) + val response = when (signedChallenge) { + is VisaAuthSignedChallenge.ByCardPublicKey -> { + visaAuthApi.getAccessTokenByCardId( + GetAccessTokenByCardIdRequest( + sessionId = signedChallenge.challenge.session.sessionId, + signature = signedChallenge.signature, + salt = signedChallenge.salt, + ), + ) + } + is VisaAuthSignedChallenge.ByWallet -> { + visaAuthApi.getAccessTokenByCardWallet( + GetAccessTokenByCardWalletRequest( + sessionId = signedChallenge.challenge.session.sessionId, + signature = signedChallenge.signature, + ), + ) + } + } VisaAuthTokens( - accessToken = "accessToken", - refreshToken = VisaAuthTokens.RefreshToken("refreshToken"), + accessToken = response.result.accessToken, + refreshToken = VisaAuthTokens.RefreshToken( + value = response.result.refreshToken, + authType = when (signedChallenge) { + is VisaAuthSignedChallenge.ByCardPublicKey -> VisaAuthTokens.RefreshToken.Type.CardId + is VisaAuthSignedChallenge.ByWallet -> VisaAuthTokens.RefreshToken.Type.CardWallet + }, + ), ) } override suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens = withContext(dispatchers.io) { - // TODO + val response = when (refreshToken.authType) { + VisaAuthTokens.RefreshToken.Type.CardId -> + visaAuthApi.refreshCardIdAccessToken( + RefreshTokenByCardIdRequest(refreshToken = refreshToken.value), + ) + VisaAuthTokens.RefreshToken.Type.CardWallet -> + visaAuthApi.refreshCardIdAccessToken( + RefreshTokenByCardIdRequest(refreshToken = refreshToken.value), + ) + }.getOrThrow() + VisaAuthTokens( - accessToken = "accessToken", - refreshToken = VisaAuthTokens.RefreshToken("new refreshToken"), + accessToken = response.result.accessToken, + refreshToken = refreshToken.copy(value = response.result.refreshToken), ) } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt index 5a33fb6a70..57bddf8b6c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/MockVisaActivationRepository.kt @@ -15,18 +15,15 @@ class MockVisaActivationRepository @AssistedInject constructor( override suspend fun getActivationRemoteState(): VisaActivationRemoteState { return VisaActivationRemoteState.CardWalletSignatureRequired( - request = VisaCardWalletDataToSignRequest( + activationOrderInfo = VisaActivationOrderInfo( orderId = "orderId", customerId = "customerId", customerWalletAddress = "customerWalletAddress", + cardWalletAddress = null, ), ) } - override suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState { - return VisaActivationRemoteState.PaymentAccountDeploying - } - override suspend fun getCardWalletAcceptanceData( request: VisaCardWalletDataToSignRequest, ): VisaDataToSignByCardWallet { 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 deleted file mode 100644 index b67a8484e1..0000000000 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverter.kt +++ /dev/null @@ -1,60 +0,0 @@ -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/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt new file mode 100644 index 0000000000..ff8c166266 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/converter/VisaActivationStatusConverterWithState.kt @@ -0,0 +1,81 @@ +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 + +private const val PIN_CODE_VALIDATION_ERROR = 1000 + +object VisaActivationStatusConverterWithState : + 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) + // TODO [REDACTED_TASK_KEY] + val result = value.result + if (result.status == Status.PinCodeRequired.stringValue && result.stepChangeCode == PIN_CODE_VALIDATION_ERROR) { + return if (lastUpdatedAt.value != result.updatedAt) { + lastUpdatedAt.value = result.updatedAt + + VisaActivationRemoteState.AwaitingPinCode( + activationOrderInfo = result.activationOrder!!.convert(), + status = VisaActivationRemoteState.AwaitingPinCode.Status.WasError, + ) + } else { + VisaActivationRemoteState.AwaitingPinCode( + activationOrderInfo = result.activationOrder!!.convert(), + status = VisaActivationRemoteState.AwaitingPinCode.Status.InProgress, + ) + } + } + + val status = Status.entries.find { it.stringValue == result.status } ?: error( + "Unknown status: ${result.status}", + ) + + return when (status) { + Status.Activated -> VisaActivationRemoteState.Activated + Status.Failed -> VisaActivationRemoteState.Failed + Status.BlockedForActivation -> VisaActivationRemoteState.BlockedForActivation + Status.CardWalletSignatureRequired -> + VisaActivationRemoteState.CardWalletSignatureRequired( + activationOrderInfo = result.activationOrder!!.convert(), + ) + Status.CustomerWalletSignatureRequired -> + VisaActivationRemoteState.CustomerWalletSignatureRequired( + activationOrderInfo = result.activationOrder!!.convert(), + ) + Status.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying + Status.PinCodeRequired -> VisaActivationRemoteState.AwaitingPinCode( + activationOrderInfo = result.activationOrder!!.convert(), + status = VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode, + ) + Status.WaitingForActivation -> VisaActivationRemoteState.WaitingForActivationFinishing + } + } + + private fun CardActivationRemoteStateResponse.ActivationOrder.convert(): VisaActivationOrderInfo { + return VisaActivationOrderInfo( + orderId = id, + customerId = customerId, + customerWalletAddress = customerWalletAddress, + cardWalletAddress = cardWalletAddress.takeIf { it.isNotBlank() }, + ) + } + + private enum class Status(val stringValue: String) { + Activated("activated"), + Failed("failed"), + BlockedForActivation("blocked_for_activation"), + CardWalletSignatureRequired("card_wallet_signature_required"), + CustomerWalletSignatureRequired("customer_wallet_signature_required"), + PaymentAccountDeploying("payment_account_deploying"), + PinCodeRequired("pin_code_required"), + WaitingForActivation("waiting_for_activation"), + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt index e1cc459371..a173f8c5ea 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/di/VisaDataModule.kt @@ -1,8 +1,8 @@ package com.tangem.data.visa.di +import com.tangem.data.visa.DefaultVisaActivationRepository import com.tangem.data.visa.DefaultVisaAuthRepository import com.tangem.data.visa.MockVisaRepository -import com.tangem.data.visa.MockVisaActivationRepository import com.tangem.domain.visa.repository.VisaActivationRepository import com.tangem.domain.visa.repository.VisaAuthRepository import com.tangem.domain.visa.repository.VisaRepository @@ -20,19 +20,19 @@ internal interface VisaDataModule { @Singleton fun bindVisaAuthRepository(repository: DefaultVisaAuthRepository): VisaAuthRepository - // @Binds - // @Singleton - // fun bindVisaActivationRepositoryFactory( - // repository: DefaultVisaActivationRepository.Factory, - // ): VisaActivationRepository.Factory - - // Mocked @Binds @Singleton fun bindVisaActivationRepositoryFactory( - repository: MockVisaActivationRepository.Factory, + repository: DefaultVisaActivationRepository.Factory, ): VisaActivationRepository.Factory + // Mocked + // @Binds + // @Singleton + // fun bindVisaActivationRepositoryFactory( + // repository: MockVisaActivationRepository.Factory, + // ): VisaActivationRepository.Factory + // @Binds // fun bindVisaRepository(repository: DefaultVisaRepository): VisaRepository diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt index 538c665358..2c7cae2eb4 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.visa.TangemVisaAuthApi +import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.visa.exception.RefreshTokenExpiredException @@ -79,16 +80,18 @@ internal class VisaApiRequestMaker @Inject constructor( } private suspend fun refreshAccessTokens(refreshToken: VisaAuthTokens.RefreshToken): VisaAuthTokens { - val result = visaAuthApi.refreshAccessToken(refreshToken.value).getOrThrow() + val result = visaAuthApi.refreshCardWalletAccessToken( + RefreshTokenByCardWalletRequest(refreshToken = refreshToken.value), + ).getOrThrow() return VisaAuthTokens( - accessToken = result.accessToken, - refreshToken = VisaAuthTokens.RefreshToken(result.refreshToken), + accessToken = result.result.accessToken, + refreshToken = refreshToken.copy(value = result.result.refreshToken), ) } @Throws - private suspend fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens { + private fun getAuthTokens(userWalletId: UserWalletId): VisaAuthTokens { val userWallet = findVisaUserWallet(userWalletId) val status = userWallet.scanResponse.visaCardActivationStatus ?: error("Visa card activation status not found") @@ -99,7 +102,7 @@ internal class VisaApiRequestMaker @Inject constructor( return (status as? VisaCardActivationStatus.Activated)?.visaAuthTokens ?: error("Visa card is not activated") } - private suspend fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet { + private fun findVisaUserWallet(userWalletId: UserWalletId): UserWallet { val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "No user wallet found: $userWalletId" } diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt index 2bc3ecbc4a..758ec7a589 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/visa/VisaWalletPublicKeyUtility.kt @@ -57,7 +57,7 @@ object VisaWalletPublicKeyUtility { either { val address = generateAddressOnSecp256k1(publicKey).bind() - if (address.value != targetAddress) { + if (address.value.lowercase() != targetAddress.lowercase()) { raise(VisaActivationError.AddressNotMatched) } } diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 8ce37bc6d1..dab90f1ecd 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -20,6 +20,9 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.appCurrency.models) + /** Security */ + implementation(deps.spongecastle.core) + /** Libs - Other */ implementation(deps.jodatime) implementation(deps.androidx.paging.runtime) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt index f73861c111..bf9bd888dd 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/error/VisaError.kt @@ -44,6 +44,7 @@ enum class VisaActivationError( FailedToCreateAddress(104003007), AddressNotMatched(104003008), InconsistentRemoteState(104003009), + FailedRemoteState(104003010), } object VisaAuthorizationAPIError : UniversalError { 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 76389945fd..0ee54c8be3 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 @@ -10,4 +10,5 @@ data class VisaActivationOrderInfo( @Json(name = "orderId") val orderId: String, @Json(name = "customerId") val customerId: String, @Json(name = "customerWalletAddress") val customerWalletAddress: String, + @Json(name = "cardWalletAddress") val cardWalletAddress: 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 889037bee3..b34d72d2a6 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 @@ -1,6 +1,7 @@ package com.tangem.domain.visa.model import com.squareup.moshi.* +import com.tangem.domain.visa.model.VisaActivationRemoteState.* import kotlinx.serialization.Serializable @Serializable @@ -8,7 +9,7 @@ sealed class VisaActivationRemoteState { @Serializable data class CardWalletSignatureRequired( - val request: VisaCardWalletDataToSignRequest, + val activationOrderInfo: VisaActivationOrderInfo, ) : VisaActivationRemoteState() @Serializable @@ -38,6 +39,9 @@ sealed class VisaActivationRemoteState { @Serializable data object BlockedForActivation : VisaActivationRemoteState() + @Serializable + data object Failed : VisaActivationRemoteState() + companion object { val jsonAdapter: VisaActivationRemoteState_JsonAdapter = VisaActivationRemoteState_JsonAdapter() } @@ -46,7 +50,6 @@ sealed class VisaActivationRemoteState { @Suppress("ClassNaming") 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, ) @@ -62,6 +65,7 @@ enum class VisaActivationRemoteState_Type { WaitingForActivationFinishing, Activated, BlockedForActivation, + Failed, } @Suppress("ClassNaming") @@ -77,64 +81,66 @@ class VisaActivationRemoteState_JsonAdapter { fun fromJson(value: VisaActivationRemoteState_Json): VisaActivationRemoteState { return when (value.type) { VisaActivationRemoteState_Type.CardWalletSignatureRequired -> - VisaActivationRemoteState.CardWalletSignatureRequired(value.requestCardWallet!!) + CardWalletSignatureRequired(value.activationOrderInfo!!) VisaActivationRemoteState_Type.CustomerWalletSignatureRequired -> - VisaActivationRemoteState.CustomerWalletSignatureRequired(value.activationOrderInfo!!) - VisaActivationRemoteState_Type.PaymentAccountDeploying -> VisaActivationRemoteState.PaymentAccountDeploying + CustomerWalletSignatureRequired(value.activationOrderInfo!!) + VisaActivationRemoteState_Type.PaymentAccountDeploying -> PaymentAccountDeploying VisaActivationRemoteState_Type.WaitingPinCode -> - VisaActivationRemoteState.AwaitingPinCode( + AwaitingPinCode( activationOrderInfo = value.activationOrderInfo!!, status = when (value.awaitingPinCodeStatus!!) { AwaitingPinCodeStatus_Type.WaitingForPinCode -> - VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode + AwaitingPinCode.Status.WaitingForPinCode AwaitingPinCodeStatus_Type.InProgress -> - VisaActivationRemoteState.AwaitingPinCode.Status.InProgress + AwaitingPinCode.Status.InProgress AwaitingPinCodeStatus_Type.WasError -> - VisaActivationRemoteState.AwaitingPinCode.Status.WasError + AwaitingPinCode.Status.WasError }, ) VisaActivationRemoteState_Type.WaitingForActivationFinishing -> - VisaActivationRemoteState.WaitingForActivationFinishing - VisaActivationRemoteState_Type.Activated -> VisaActivationRemoteState.Activated - VisaActivationRemoteState_Type.BlockedForActivation -> VisaActivationRemoteState.BlockedForActivation + WaitingForActivationFinishing + VisaActivationRemoteState_Type.Activated -> Activated + VisaActivationRemoteState_Type.BlockedForActivation -> BlockedForActivation + VisaActivationRemoteState_Type.Failed -> Failed } } @ToJson fun toJson(value: VisaActivationRemoteState): VisaActivationRemoteState_Json { return when (value) { - is VisaActivationRemoteState.CardWalletSignatureRequired -> + is CardWalletSignatureRequired -> VisaActivationRemoteState_Json( type = VisaActivationRemoteState_Type.CardWalletSignatureRequired, - requestCardWallet = value.request, + activationOrderInfo = value.activationOrderInfo, ) - is VisaActivationRemoteState.CustomerWalletSignatureRequired -> + is CustomerWalletSignatureRequired -> VisaActivationRemoteState_Json( type = VisaActivationRemoteState_Type.CustomerWalletSignatureRequired, activationOrderInfo = value.activationOrderInfo, ) - is VisaActivationRemoteState.PaymentAccountDeploying -> + is PaymentAccountDeploying -> VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.PaymentAccountDeploying) - is VisaActivationRemoteState.AwaitingPinCode -> + is AwaitingPinCode -> VisaActivationRemoteState_Json( VisaActivationRemoteState_Type.WaitingPinCode, activationOrderInfo = value.activationOrderInfo, awaitingPinCodeStatus = when (value.status) { - VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode -> + AwaitingPinCode.Status.WaitingForPinCode -> AwaitingPinCodeStatus_Type.WaitingForPinCode - VisaActivationRemoteState.AwaitingPinCode.Status.InProgress -> + AwaitingPinCode.Status.InProgress -> AwaitingPinCodeStatus_Type.InProgress - VisaActivationRemoteState.AwaitingPinCode.Status.WasError -> + AwaitingPinCode.Status.WasError -> AwaitingPinCodeStatus_Type.WasError }, ) - is VisaActivationRemoteState.WaitingForActivationFinishing -> + is WaitingForActivationFinishing -> VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.WaitingForActivationFinishing) - is VisaActivationRemoteState.Activated -> VisaActivationRemoteState_Json( + is Activated -> VisaActivationRemoteState_Json( VisaActivationRemoteState_Type.Activated, ) - is VisaActivationRemoteState.BlockedForActivation -> + is BlockedForActivation -> VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.BlockedForActivation) + is Failed -> VisaActivationRemoteState_Json(VisaActivationRemoteState_Type.Failed) } } } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt index b39a31f25b..32984786d7 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaAuthTokens.kt @@ -12,8 +12,15 @@ data class VisaAuthTokens( ) { @Serializable - @JvmInline - value class RefreshToken(val value: String) + @JsonClass(generateAdapter = true) + data class RefreshToken( + @Json(name = "value") val value: String, + @Json(name = "authType") val authType: Type, + ) { + enum class Type { + CardId, CardWallet, + } + } } fun VisaAuthTokens.getAuthHeader(): String { diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardWalletDataToSignRequest.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardWalletDataToSignRequest.kt index 831a4b1d6c..05f50c2d2b 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardWalletDataToSignRequest.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardWalletDataToSignRequest.kt @@ -1,3 +1,12 @@ package com.tangem.domain.visa.model -typealias VisaCardWalletDataToSignRequest = VisaActivationOrderInfo \ No newline at end of file +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.serialization.Serializable + +@Serializable +@JsonClass(generateAdapter = true) +data class VisaCardWalletDataToSignRequest( + @Json(name = "activationOrderInfo") val activationOrderInfo: VisaActivationOrderInfo, + @Json(name = "cardWalletAddress") val cardWalletAddress: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCustomerWalletDataToSignRequest.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCustomerWalletDataToSignRequest.kt index 0103e34df4..7532659cad 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCustomerWalletDataToSignRequest.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCustomerWalletDataToSignRequest.kt @@ -9,4 +9,5 @@ import kotlinx.serialization.Serializable data class VisaCustomerWalletDataToSignRequest( @Json(name = "orderId") val orderId: String, @Json(name = "card_wallet_address") val cardWalletAddress: String, + @Json(name = "customer_wallet_address") val customerWalletAddress: String, ) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCardWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCardWallet.kt index 9e95f713e1..aac9421715 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCardWallet.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaDataToSignByCardWallet.kt @@ -8,10 +8,9 @@ data class VisaDataToSignByCardWallet( val hashToSign: String, ) -fun VisaDataToSignByCardWallet.sign(cardWalletAddress: String, rootOTP: String, otpCounter: Int, signature: String) = +fun VisaDataToSignByCardWallet.sign(rootOTP: String, otpCounter: Int, signature: String) = VisaSignedActivationDataByCardWallet( dataToSign = this, - cardWalletAddress = cardWalletAddress, rootOTP = rootOTP, otpCounter = otpCounter, signature = signature, diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedActivationDataByCardWallet.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedActivationDataByCardWallet.kt index 5b37c067ec..246767c013 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedActivationDataByCardWallet.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaSignedActivationDataByCardWallet.kt @@ -2,7 +2,6 @@ package com.tangem.domain.visa.model class VisaSignedActivationDataByCardWallet( val dataToSign: VisaDataToSignByCardWallet, - val cardWalletAddress: String, val rootOTP: String, val otpCounter: Int, val signature: String, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/SetVisaPinCodeUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/SetVisaPinCodeUseCase.kt index 6fda7af7df..c15d97e074 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/SetVisaPinCodeUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/SetVisaPinCodeUseCase.kt @@ -1,4 +1,5 @@ @file:Suppress("MaximumLineLength") + package com.tangem.domain.visa import android.util.Base64 @@ -14,14 +15,12 @@ import java.security.spec.X509EncodedKeySpec import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec private const val KEY_SIZE = 256 -class SetVisaPinCodeUseCase( - private val visaActivationRepositoryFactory: VisaActivationRepository.Factory, -) { +class SetVisaPinCodeUseCase(private val visaActivationRepositoryFactory: VisaActivationRepository.Factory) { + @Suppress("MagicNumber") suspend operator fun invoke( visaCardId: VisaCardId, activationOrderId: String, @@ -29,19 +28,17 @@ class SetVisaPinCodeUseCase( ): Either = Either.catch { val visaActivationRepository = visaActivationRepositoryFactory.create(visaCardId) val rsaPublicKey = visaActivationRepository.getPinCodeRsaEncryptionPublicKey() + val formattedPin = "24$pinCode${"f".repeat(n = 8)}FF" val sessionKey = generateSessionKey() val sessionId = getSessionId(rsaPublicKey, sessionKey) - val secureRandom = SecureRandom() - val iv = ByteArray(size = 16) - secureRandom.nextBytes(iv) + val iv = generateIV() val ivParameterSpec = IvParameterSpec(iv) val ivPayload = Base64.encodeToString(iv, Base64.NO_WRAP) - val aesKey = SecretKeySpec(sessionKey.encoded, 0, sessionKey.encoded.size, "AES") - val aesCipher = Cipher.getInstance("AES/CBC/PKCS5PADDING") - aesCipher.init(Cipher.ENCRYPT_MODE, aesKey, ivParameterSpec) - val encryptedPin = Base64.encodeToString(aesCipher.doFinal(pinCode.toByteArray()), Base64.NO_WRAP) + val aesCipher = Cipher.getInstance("AES/GCM/NoPadding") + aesCipher.init(Cipher.ENCRYPT_MODE, sessionKey, ivParameterSpec) + val encryptedPin = Base64.encodeToString(aesCipher.doFinal(formattedPin.encodeToByteArray()), Base64.NO_WRAP) visaActivationRepository.sendPinCode( VisaEncryptedPinCode( @@ -59,6 +56,12 @@ class SetVisaPinCodeUseCase( return generator.generateKey() } + private fun generateIV(): ByteArray { + val iv = ByteArray(size = 16) + SecureRandom().nextBytes(iv) + return iv + } + private fun getPublicKey(rsaPublicKey: String): PublicKey { val keyBytes = Base64.decode(rsaPublicKey, Base64.NO_WRAP) val spec = X509EncodedKeySpec(keyBytes) @@ -67,8 +70,9 @@ class SetVisaPinCodeUseCase( private fun getSessionId(rsaPublicKey: String, sessionKey: Key): String { val publicKey = getPublicKey(rsaPublicKey) - val cipher = Cipher.getInstance("RSA") + val cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding") cipher.init(Cipher.ENCRYPT_MODE, publicKey) - return Base64.encodeToString(cipher.doFinal(sessionKey.encoded), Base64.NO_WRAP) + val base64SessionKey = Base64.encodeToString(sessionKey.encoded, Base64.NO_WRAP) + return Base64.encodeToString(cipher.doFinal(base64SessionKey.encodeToByteArray()), Base64.NO_WRAP) } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt index f6d451fb7b..339f8c7e9c 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt @@ -6,8 +6,6 @@ interface VisaActivationRepository { suspend fun getActivationRemoteState(): VisaActivationRemoteState - suspend fun getActivationRemoteStateLongPoll(): VisaActivationRemoteState - suspend fun getCardWalletAcceptanceData(request: VisaCardWalletDataToSignRequest): VisaDataToSignByCardWallet suspend fun getCustomerWalletAcceptanceData( 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 c18f5585e7..047c16e1d5 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,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaOtherWalletComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.pushNew( + model.stackNavigation.replaceAll( OnboardingVisaRoute.PinCode(activationOrderInfo = it, pinCodeValidationError = false), ) }, @@ -183,7 +183,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaPinCodeComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.pushNew( + model.stackNavigation.replaceAll( OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.PinCode), ) }, @@ -199,7 +199,7 @@ internal class DefaultOnboardingVisaComponent @AssistedInject constructor( params = OnboardingVisaApproveComponent.Params( childParams = childParams, onDone = { - model.stackNavigation.pushNew( + model.stackNavigation.replaceAll( OnboardingVisaRoute.InProgress(from = OnboardingVisaRoute.InProgress.From.Approve), ) }, 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 a07b1acd3a..6c1dcc74e1 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 @@ -20,7 +20,6 @@ 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 @@ -55,9 +54,12 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( cardPublicKey = params.scanResponse.card.cardPublicKey.toHexString(), ), ) - private val activationStatus = - params.scanResponse.visaCardActivationStatus as? VisaCardActivationStatus.NotStartedActivation - ?: error("Visa activation status is not set or incorrect for this step") + + private val activationInput = when (val status = params.scanResponse.visaCardActivationStatus) { + is VisaCardActivationStatus.NotStartedActivation -> status.activationInput + is VisaCardActivationStatus.ActivationStarted -> status.activationInput + else -> error("Visa activation status is not set or incorrect for this step") + } private val _uiState = MutableStateFlow(getInitialState()) @@ -153,8 +155,8 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( modelScope.launch { val challengeToSign = runCatching { visaAuthRepository.getCardAuthChallenge( - cardId = activationStatus.activationInput.cardId, - cardPublicKey = activationStatus.activationInput.cardPublicKey, + cardId = activationInput.cardId, + cardPublicKey = activationInput.cardPublicKey, ) }.getOrElse { loading(false) @@ -167,7 +169,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( accessCode = accessCode, authorizationChallenge = challengeToSign, ), - activationInput = activationStatus.activationInput, + activationInput = activationInput, ) val resultData = when (result) { @@ -175,7 +177,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( loading(false) uiMessageSender.showErrorDialog(result.error.universalError) analyticsEventsHandler.send( - VisaAnalyticsEvent.Errors(result.error.code.toString(), ONBOARDING_SOURCE), + VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError), ) return@launch } @@ -190,14 +192,16 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor( return@launch } - val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress + val targetAddress = + result.data.signedActivationData.dataToSign.request.activationOrderInfo.customerWalletAddress modelScope.launch { onDone.emit( ActivationReadyEvent( customerWalletDataToSignRequest = VisaCustomerWalletDataToSignRequest( - orderId = result.data.signedActivationData.dataToSign.request.orderId, - cardWalletAddress = result.data.signedActivationData.cardWalletAddress, + orderId = result.data.signedActivationData.dataToSign.request.activationOrderInfo.orderId, + cardWalletAddress = result.data.signedActivationData.dataToSign.request.cardWalletAddress, + customerWalletAddress = targetAddress, ), newScanResponse = params.scanResponse.copy( card = result.data.newCardDTO, 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 9f14644a5c..31f7aa82ed 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 @@ -16,7 +16,6 @@ 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 @@ -88,9 +87,7 @@ 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), - ) + analyticsEventHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError)) 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/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 a138341670..c0fdbbd57d 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 @@ -22,6 +22,7 @@ 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.child.welcome.model.analytics.VisaAnalyticsEvent import com.tangem.features.onboarding.v2.visa.impl.route.OnboardingVisaRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.delay @@ -57,10 +58,14 @@ internal class OnboardingVisaInProgressModel @Inject constructor( init { analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.ActivationInProgressScreen) + runShortPolling() + } + + private fun runShortPolling() { modelScope.launch { while (true) { val result = runCatching { - visaActivationRepository.getActivationRemoteStateLongPoll() + visaActivationRepository.getActivationRemoteState() }.getOrNull() ?: continue when (result) { @@ -68,6 +73,16 @@ internal class OnboardingVisaInProgressModel @Inject constructor( VisaActivationRemoteState.BlockedForActivation, -> { uiMessageSender.showErrorDialog(VisaActivationError.InconsistentRemoteState) + analyticsEventHandler.send( + VisaAnalyticsEvent.ErrorOnboarding(VisaActivationError.InconsistentRemoteState), + ) + return@launch + } + VisaActivationRemoteState.Failed -> { + uiMessageSender.showErrorDialog(VisaActivationError.FailedRemoteState) + analyticsEventHandler.send( + VisaAnalyticsEvent.ErrorOnboarding(VisaActivationError.FailedRemoteState), + ) return@launch } is VisaActivationRemoteState.CustomerWalletSignatureRequired, @@ -86,14 +101,14 @@ internal class OnboardingVisaInProgressModel @Inject constructor( } } - delay(timeMillis = 1000) + delay(timeMillis = 2000) } } } private suspend inline fun navigateToPinCodeIfNeeded( remoteState: VisaActivationRemoteState.AwaitingPinCode, - returnBlock: () -> Unit, + complete: () -> Unit, ) { when (remoteState.status) { VisaActivationRemoteState.AwaitingPinCode.Status.WaitingForPinCode -> { @@ -105,7 +120,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor( ), ), ) - returnBlock() + complete() } VisaActivationRemoteState.AwaitingPinCode.Status.WasError -> { onDone.emit( @@ -116,10 +131,10 @@ internal class OnboardingVisaInProgressModel @Inject constructor( ), ), ) - returnBlock() + complete() } VisaActivationRemoteState.AwaitingPinCode.Status.InProgress -> { - /** waiting for new state */ + /** waiting for the new state */ } } } 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 fa1adb5d3c..28fca46909 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 @@ -51,7 +51,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( modelScope.launch { while (true) { val result = runCatching { - visaActivationRepository.getActivationRemoteStateLongPoll() + visaActivationRepository.getActivationRemoteState() }.getOrNull() if (result is VisaActivationRemoteState.AwaitingPinCode) { @@ -59,7 +59,7 @@ internal class OnboardingVisaOtherWalletModel @Inject constructor( break } - delay(timeMillis = 1000) + delay(timeMillis = 2000) } } } 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 432a024889..3deb78230c 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 @@ -6,15 +6,12 @@ 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.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference -import com.tangem.datasource.local.visa.VisaAuthTokenStorage -import com.tangem.domain.models.scan.ScanResponse +import com.tangem.core.ui.utils.showErrorDialog import com.tangem.domain.visa.SetVisaPinCodeUseCase -import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.visa.error.VisaAuthorizationAPIError import com.tangem.domain.visa.model.VisaCardId -import com.tangem.domain.wallets.builder.UserWalletBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager -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 @@ -25,7 +22,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import javax.inject.Inject @Stable @@ -34,12 +30,9 @@ import javax.inject.Inject internal class OnboardingVisaPinCodeModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val userWalletBuilderFactory: UserWalletBuilder.Factory, - private val userWalletsListManager: UserWalletsListManager, - private val authTokenStorage: VisaAuthTokenStorage, - private val otpStorage: VisaAuthTokenStorage, private val setVisaPinCodeUseCase: SetVisaPinCodeUseCase, private val analyticsEventHandler: AnalyticsEventHandler, + private val uiMessageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -102,11 +95,11 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( activationOrderId = params.activationOrderInfo.orderId, ).onLeft { loading(false) + uiMessageSender.showErrorDialog(VisaAuthorizationAPIError) return@launch } - saveWallet() - + onDone.emit(Unit) loading(false) } } @@ -114,26 +107,4 @@ internal class OnboardingVisaPinCodeModel @Inject constructor( private fun loading(state: Boolean) { _uiState.update { it.copy(submitButtonLoading = state) } } - - private suspend fun saveWallet() { - val userWallet = createUserWallet(params.scanResponse) - userWalletsListManager.save(userWallet) - authTokenStorage.remove(params.scanResponse.card.cardId) - otpStorage.remove(params.scanResponse.card.cardId) - onDone.emit(Unit) - } - - private suspend fun createUserWallet(scanResponse: ScanResponse): UserWallet = withContext(dispatchers.io) { - val newActivationStatus = VisaCardActivationStatus.Activated( - visaAuthTokens = authTokenStorage.get(scanResponse.card.cardId) - ?: error("Impossible state. Wrong feature implementation"), - ) - - requireNotNull( - value = userWalletBuilderFactory.create( - scanResponse = scanResponse.copy(visaCardActivationStatus = newActivationStatus), - ).build(), - lazyMessage = { "User wallet not created" }, - ) - } } \ 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/PinCodeValidation.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt index 99c721134d..4e026f18ec 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/pincode/model/PinCodeValidation.kt @@ -22,10 +22,11 @@ internal object PinCodeValidation { } private fun validateNoRepeatedDigits(pinCode: String): Boolean { - return pinCode.toSet().size == PIN_CODE_LENGTH + return pinCode.toSet().size > 1 } private fun validateNoConsecutiveDigits(pinCode: String): Boolean { - return pinCode.windowed(2).all { it[0] + 1 == it[1] }.not() + return pinCode.zipWithNext().any { it.second != it.first + 1 } && + pinCode.zipWithNext().any { it.second != it.first - 1 } } } \ 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/OnboardingVisaWelcomeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/welcome/model/OnboardingVisaWelcomeModel.kt index 35869da702..7c602c0e14 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 @@ -15,7 +15,6 @@ 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 @@ -96,9 +95,7 @@ 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), - ) + analyticsEventsHandler.send(VisaAnalyticsEvent.ErrorOnboarding(result.error.universalError)) return@launch } is CompletionResult.Success -> result.data @@ -112,15 +109,17 @@ internal class OnboardingVisaWelcomeModel @Inject constructor( return@launch } - val targetAddress = result.data.signedActivationData.dataToSign.request.customerWalletAddress + val request = result.data.signedActivationData.dataToSign.request + val targetAddress = request.activationOrderInfo.customerWalletAddress modelScope.launch { onDone.emit( DoneEvent.WelcomeBackDone( ActivationReadyEvent( customerWalletDataToSignRequest = VisaCustomerWalletDataToSignRequest( - orderId = result.data.signedActivationData.dataToSign.request.orderId, - cardWalletAddress = result.data.signedActivationData.cardWalletAddress, + orderId = request.activationOrderInfo.orderId, + cardWalletAddress = request.cardWalletAddress, + customerWalletAddress = targetAddress, ), newScanResponse = params.scanResponse.copy( card = result.data.newCardDTO, 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 index f31e8c0a83..e77e0bb36f 100644 --- 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 @@ -1,6 +1,7 @@ package com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.error.UniversalError internal const val ONBOARDING_SOURCE = "Onboarding" internal const val MAIN_SOURCE = "Main" @@ -10,14 +11,11 @@ internal sealed class VisaAnalyticsEvent( params: Map = mapOf(), ) : AnalyticsEvent("Visa", event, params) { - data class Errors( - val errorCode: String, - val source: String, - ) : VisaAnalyticsEvent( + data class ErrorOnboarding(val error: UniversalError) : VisaAnalyticsEvent( event = "Errors", params = mapOf( - "Error Code" to errorCode, - "Source" to source, + "Error Code" to error.errorCode.toString(), + "Source" to ONBOARDING_SOURCE, ), ) } \ No newline at end of file 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 8bc8b14032..9961ec2459 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 @@ -1,10 +1,10 @@ package com.tangem.features.onboarding.v2.visa.impl.model import androidx.compose.runtime.Stable -import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.pop import com.arkivanov.decompose.router.stack.pushNew +import com.arkivanov.decompose.router.stack.replaceAll import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -12,6 +12,7 @@ import com.tangem.domain.common.visa.VisaUtilities import com.tangem.domain.common.visa.VisaWalletPublicKeyUtility import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus +import com.tangem.domain.visa.model.VisaCardWalletDataToSignRequest import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onboarding.v2.visa.api.OnboardingVisaComponent @@ -83,14 +84,13 @@ internal class OnboardingVisaModel @Inject constructor( ) } - @OptIn(ExperimentalDecomposeApi::class) fun navigateFromInProgress(event: OnboardingVisaInProgressComponent.Params.DoneEvent) { when (event) { OnboardingVisaInProgressComponent.Params.DoneEvent.Activated -> { modelScope.launch { onDone.emit(Unit) } } is OnboardingVisaInProgressComponent.Params.DoneEvent.NavigateTo -> { - stackNavigation.pushNew(event.route) + stackNavigation.replaceAll(event.route) } } } @@ -145,10 +145,17 @@ internal class OnboardingVisaModel @Inject constructor( is VisaCardActivationStatus.ActivationStarted -> { when (val remoteState = activationStatus.remoteState) { is VisaActivationRemoteState.CardWalletSignatureRequired -> { - OnboardingVisaRoute.WelcomeBack( - activationInput = activationStatus.activationInput, - dataToSignByCardWalletRequest = remoteState.request, - ) + if (activationStatus.activationInput.isAccessCodeSet) { + OnboardingVisaRoute.WelcomeBack( + activationInput = activationStatus.activationInput, + dataToSignByCardWalletRequest = VisaCardWalletDataToSignRequest( + activationOrderInfo = remoteState.activationOrderInfo, + cardWalletAddress = activationStatus.cardWalletAddress, + ), + ) + } else { + OnboardingVisaRoute.AccessCode + } } is VisaActivationRemoteState.CustomerWalletSignatureRequired -> { remoteState.getRoute(activationStatus) @@ -167,6 +174,7 @@ internal class OnboardingVisaModel @Inject constructor( } VisaActivationRemoteState.Activated, VisaActivationRemoteState.BlockedForActivation, + VisaActivationRemoteState.Failed, -> error("Activation status is not correct for onboarding flow") } } @@ -182,6 +190,7 @@ internal class OnboardingVisaModel @Inject constructor( val request = VisaCustomerWalletDataToSignRequest( orderId = this.activationOrderInfo.orderId, cardWalletAddress = activationStatus.cardWalletAddress, + customerWalletAddress = this.activationOrderInfo.customerWalletAddress, ) val preparationDataForApprove = PreparationDataForApprove( customerWalletAddress = this.activationOrderInfo.customerWalletAddress, From a2b440444ee0bbde5620adf999a26b92eb659926 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 16:28:48 +0700 Subject: [PATCH 083/151] Updated on 2026-08-14 --- .../DefaultWcEthMessageSignUseCase.kt | 10 +- .../DefaultWcEthSendTransactionUseCase.kt | 33 +++-- .../DefaultWcEthSignTransactionUseCase.kt | 43 +++--- .../ethereum/EthTransactionParamsConverter.kt | 25 ++++ .../network/ethereum/WcEthTxHelper.kt | 58 ++++++++ .../walletconnect/sign/BaseWcSignUseCase.kt | 15 +- .../sign/WcSignUseCaseDelegate.kt | 4 +- .../WcSignUseCaseDelegateTest.kt | 133 +++++++++++++----- .../transaction/usecase/GetFeeUseCase.kt | 41 ++++-- .../domain/walletconnect/usecase/WcUseCase.kt | 5 + .../ethereum/WcEthTransactionUseCase.kt | 32 ++++- gradle/tangem_dependencies.toml | 2 +- 12 files changed, 300 insertions(+), 101 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/EthTransactionParamsConverter.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt index 8421ccb613..bbbd62d234 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt @@ -9,7 +9,7 @@ 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.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext import com.tangem.domain.transaction.usecase.SignUseCase @@ -33,15 +33,17 @@ internal class DefaultWcEthMessageSignUseCase @AssistedInject constructor( ) : BaseWcSignUseCase(), WcEthMessageSignUseCase { - override val onSign: OnSign = collector@{ state -> + override suspend fun SignCollector.onSign( + state: WcSignState, + ) { val hashToSign = LegacySdkHelper.createMessageData(state.signModel.rawMsg) val userWallet = session.wallet val walletManager = walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) - ?: return@collector + ?: return val signedHash = signUseCase(hashToSign, userWallet, network) .onLeft { emit(state.toResult(it.left())) } - .getOrNull() ?: return@collector + .getOrNull() ?: return val respond = EthereumUtils.prepareSignedMessageData( signedHash = signedHash, 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 index 76e1b410de..c923e955fd 100644 --- 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 @@ -1,50 +1,53 @@ 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.blockchain.common.transaction.Fee +import com.tangem.blockchain.extensions.formatHex 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.SignCollector 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.ethereum.WcEthTransaction 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 +import kotlinx.coroutines.flow.emitAll +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(), +) : BaseWcSignUseCase(), WcEthSendTransactionUseCase { - override val onSign: OnSign = collector@{ state -> - val hash = sendTransaction(state.signModel, wallet, network) + private val converter = EthTransactionParamsConverter(context) + + override suspend fun SignCollector.onSign(state: WcSignState) { + val hash = sendTransaction(state.signModel.transactionData, 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) + .getOrNull() ?: return + val respondResult = respondService.respond(rawSdkRequest, hash.formatHex()) emit(state.toResult(respondResult)) } - override fun updateFee(fee: TransactionFee) { - TODO("Not yet implemented") + override fun updateFee(fee: Fee) { + middleAction(fee) } - override fun invoke(): Flow> { - method - TODO("Not yet implemented") + override fun invoke(): Flow> = flow { + val ethTransaction = converter.convert(method.transaction) ?: return@flow + emitAll(delegate.invoke(ethTransaction)) } @AssistedFactory 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 index d8a01823c7..70b1c7b3a3 100644 --- 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 @@ -1,51 +1,62 @@ 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.blockchain.common.transaction.Fee +import com.tangem.blockchain.extensions.formatHex 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.SignCollector 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.ethereum.WcEthTransaction 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 +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.emitAll +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(), +) : BaseWcSignUseCase(), WcEthSignTransactionUseCase { - override val onSign: OnSign = collector@{ state -> - val hash = prepareForSend(state.signModel, wallet, network) + private val converter = EthTransactionParamsConverter(context) + + override suspend fun SignCollector.onSign(state: WcSignState) { + val hash = prepareForSend(state.signModel.transactionData, wallet, network) + .map { it.toHexString().formatHex() } .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) + .getOrNull() + ?: return + val respondResult = respondService.respond(rawSdkRequest, hash) emit(state.toResult(respondResult)) } - override fun updateFee(fee: TransactionFee) { - TODO("Not yet implemented") + override suspend fun FlowCollector.onMiddleAction(signModel: WcEthTransaction, fee: Fee) { + val newState = signModel + .copy(transactionData = signModel.transactionData.copy(fee = fee)) + emit(newState) } - override fun invoke(): Flow> { - method - TODO("Not yet implemented") + override fun updateFee(fee: Fee) { + middleAction(fee) + } + + override fun invoke(): Flow> = flow { + val ethTransaction = converter.convert(method.transaction) ?: return@flow + emitAll(delegate.invoke(ethTransaction)) } @AssistedFactory diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/EthTransactionParamsConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/EthTransactionParamsConverter.kt new file mode 100644 index 0000000000..ac63abc8f8 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/EthTransactionParamsConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.data.walletconnect.network.ethereum + +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.walletconnect.model.WcEthTransactionParams +import com.tangem.domain.walletconnect.usecase.ethereum.WcEthTransaction +import com.tangem.utils.converter.Converter + +internal class EthTransactionParamsConverter( + private val context: WcMethodUseCaseContext, +) : Converter { + + override fun convert(value: WcEthTransactionParams): WcEthTransaction? { + val dAppFee = WcEthTxHelper.getDAppFee(context.network, value) + val transactionData = WcEthTxHelper.createTransactionData( + dAppFee = dAppFee, + network = context.network, + txParams = value, + ) + transactionData ?: return null + return WcEthTransaction( + dAppFee = dAppFee, + transactionData = transactionData, + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt new file mode 100644 index 0000000000..bc1f5e26c5 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthTxHelper.kt @@ -0,0 +1,58 @@ +package com.tangem.data.walletconnect.network.ethereum + +import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.HEX_PREFIX +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.extensions.hexToBigDecimal +import com.tangem.common.extensions.hexToBytes +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.WcEthTransactionParams +import java.math.BigDecimal + +internal object WcEthTxHelper { + private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8") + + fun getDAppFee(network: Network, txParams: WcEthTransactionParams): Fee.Ethereum.Legacy? { + val gasLimit = txParams.gas?.hexToBigDecimal() ?: return null + val gasPrice = txParams.gasPrice?.hexToBigDecimal() ?: return null + + val blockchain = Blockchain.fromId(network.id.value) + + var feeDecimal = (gasLimit * gasPrice) + .movePointLeft(blockchain.decimals()) + if (blockchain == Blockchain.Mantle) { + feeDecimal = feeDecimal.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER) + } + + val feeAmount = Amount(feeDecimal, blockchain) + return Fee.Ethereum.Legacy(feeAmount, gasLimit.toBigInteger(), gasPrice.toBigInteger()) + } + + fun createTransactionData( + dAppFee: Fee.Ethereum.Legacy?, + network: Network, + txParams: WcEthTransactionParams, + ): TransactionData.Uncompiled? { + val destinationAddress = txParams.to ?: return null + val blockchain = Blockchain.fromId(network.id.value) + val value = (txParams.value ?: "0") + .hexToBigDecimal() + .movePointLeft(blockchain.decimals()) + + val callData = CompiledSmartContractCallData(txParams.data.removePrefix(HEX_PREFIX).hexToBytes()) + return TransactionData.Uncompiled( + amount = Amount(value, blockchain), + fee = dAppFee, + sourceAddress = txParams.from, + destinationAddress = destinationAddress, + extras = EthereumTransactionExtras( + callData = callData, + nonce = txParams.nonce?.hexToBigDecimal()?.toBigInteger(), + ), + ) + } +} \ 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 d4e8696371..8967ae0706 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 @@ -31,7 +31,7 @@ internal abstract class BaseWcSignUseCase : ) } - override val onCancel: suspend (currentState: WcSignState) -> Unit = { + override suspend fun onCancel(currentState: WcSignState) { defaultReject() } @@ -46,14 +46,14 @@ internal abstract class BaseWcSignUseCase : internal interface MiddleActionCollector { - val onMiddleAction: OnMiddle get() = { _, _ -> } + suspend fun FlowCollector.onMiddleAction(signModel: SignModel, middleAction: MiddleAction) {} } internal interface FinalActionCollector { - val onSign: OnSign get() = {} + suspend fun SignCollector.onSign(state: WcSignState) {} - val onCancel: OnCancel get() = {} + suspend fun onCancel(state: WcSignState) {} } internal class WcMethodUseCaseContext( @@ -62,9 +62,4 @@ internal class WcMethodUseCaseContext( val network: Network, ) -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 +internal typealias SignCollector = FlowCollector> \ 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 index c84bc39ac7..89d8bc1097 100644 --- 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 @@ -42,7 +42,7 @@ internal class WcSignUseCaseDelegate( fun listenMiddle() = middleActionsChannel.receiveAsFlow() .buffer() - .transform { middleActions -> this.onMiddleAction(state.value, middleActions) } + .transform { middleActions -> this.onMiddleAction(state.value.signModel, middleActions) } .onEach { updatedModel -> state.update { it.toPreSign(updatedModel) } } .launchIn(this) @@ -61,7 +61,7 @@ internal class WcSignUseCaseDelegate( .transformLatest { finalAction -> when (finalAction) { Action.Cancel -> { - onCancel.invoke(state.value) + onCancel(state.value) channel.close() } Action.Sign -> { 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 index e80bd9a4f8..39aaa7b68c 100644 --- 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 @@ -5,13 +5,12 @@ 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.SignCollector 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 @@ -21,12 +20,10 @@ import org.junit.Test internal class WcSignUseCaseDelegateTest { - private val middleActionCollector = mockk>() - private val finalActionCollector = mockk>() - private val useCase = WcSignUseCaseDelegate( - finalActionCollector = finalActionCollector, - middleActionCollector = middleActionCollector, - ) + private var middleActionCollector: MiddleActionCollector = + object : MiddleActionCollector {} + private var finalActionCollector: FinalActionCollector = + object : FinalActionCollector {} private val initSignModel = TestSignModel() private val initState = WcSignState(initSignModel, WcSignStep.PreSign) @@ -51,14 +48,21 @@ internal class WcSignUseCaseDelegateTest { @Before fun setup() { - every { middleActionCollector.onMiddleAction } returns { _, _ -> } - every { finalActionCollector.onSign } returns { } - every { finalActionCollector.onCancel } returns { } + middleActionCollector = object : MiddleActionCollector {} + finalActionCollector = object : FinalActionCollector {} } @Test fun `invoke and keep flow running`() = runTest { - every { finalActionCollector.onSign } returns successSign + finalActionCollector = object : FinalActionCollector { + override suspend fun SignCollector.onSign(state: WcSignState) { + successSign(state) + } + } + val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) useCase.invoke(initSignModel).test { assertEquals(initState, awaitItem()) @@ -68,7 +72,15 @@ internal class WcSignUseCaseDelegateTest { @Test fun `success sign, keep flow running`() = runTest { - every { finalActionCollector.onSign } returns successSign + finalActionCollector = object : FinalActionCollector { + override suspend fun SignCollector.onSign(state: WcSignState) { + successSign(state) + } + } + val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) useCase.invoke(initModel = initSignModel).test { assertEquals(initState, awaitItem()) @@ -82,7 +94,15 @@ internal class WcSignUseCaseDelegateTest { @Test fun `failed sign, keep flow running`() = runTest { val failedResult = signing.toResult(testException.left()) - every { finalActionCollector.onSign } returns failedSign + finalActionCollector = object : FinalActionCollector { + override suspend fun SignCollector.onSign(state: WcSignState) { + failedSign(state) + } + } + val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) useCase.invoke(initSignModel).test { assertEquals(initState, awaitItem()) @@ -97,10 +117,16 @@ internal class WcSignUseCaseDelegateTest { 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 + finalActionCollector = object : FinalActionCollector { + override suspend fun SignCollector.onSign(state: WcSignState) { + delay(2) + throw exception + } } + val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) useCase.invoke(initSignModel).test { assertEquals(initState, awaitItem()) @@ -113,10 +139,16 @@ internal class WcSignUseCaseDelegateTest { @Test fun `interrupt signing and complete flow on cancel call`() = runTest { - every { finalActionCollector.onSign } returns { - delay(5) - emit(result) + finalActionCollector = object : FinalActionCollector { + override suspend fun SignCollector.onSign(state: WcSignState) { + delay(5) + emit(result) + } } + val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) useCase.invoke(initSignModel).test { assertEquals(initState, awaitItem()) @@ -135,13 +167,19 @@ internal class WcSignUseCaseDelegateTest { 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) + finalActionCollector = object : FinalActionCollector { + override suspend fun SignCollector.onSign(state: WcSignState) { + // should emit single time in this test + emit(if (count % 2 == 0) startLoading else startLoading2) + count = count.inc() + delay(10) + emit(expectedSignResult) + } } + val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) useCase.invoke(initSignModel).test { useCase.sign() @@ -177,14 +215,24 @@ internal class WcSignUseCaseDelegateTest { domainStep = WcSignStep.PreSign, ) - every { finalActionCollector.onSign } returns { - delay(6) - emit(failedSign) + finalActionCollector = object : FinalActionCollector { + override suspend fun SignCollector.onSign(state: WcSignState) { + delay(6) + emit(failedSign) + } } - - every { middleActionCollector.onMiddleAction } returns { currentState, middleAction -> - emit(currentState.signModel.copy(testStr = middleAction.newTestStr)) + middleActionCollector = object : MiddleActionCollector { + override suspend fun FlowCollector.onMiddleAction( + signModel: TestSignModel, + middleAction: TestMiddleAction, + ) { + emit(signModel.copy(testStr = middleAction.newTestStr)) + } } + val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) useCase.invoke(initSignModel).test { delay(2) @@ -214,11 +262,24 @@ internal class WcSignUseCaseDelegateTest { 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) + finalActionCollector = object : FinalActionCollector { + override suspend fun SignCollector.onSign(state: WcSignState) { + successSign(state) + } } + middleActionCollector = object : MiddleActionCollector { + override suspend fun FlowCollector.onMiddleAction( + signModel: TestSignModel, + middleAction: TestMiddleAction, + ) { + emit(signModel.copy(testStr = middleAction.newTestStr)) + delay(4) + } + } + val useCase = WcSignUseCaseDelegate( + finalActionCollector = finalActionCollector, + middleActionCollector = middleActionCollector, + ) useCase.invoke(initSignModel).test { assertEquals(initState, awaitItem()) 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 c370ac644d..ed841119c1 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 @@ -2,13 +2,12 @@ package com.tangem.domain.transaction.usecase import arrow.core.raise.catch import arrow.core.raise.either -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.* 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.tokens.model.Network import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.error.mapToFeeError import com.tangem.domain.walletmanager.WalletManagersFacade @@ -25,6 +24,32 @@ class GetFeeUseCase( private val walletManagersFacade: WalletManagersFacade, private val demoConfig: DemoConfig, ) { + suspend operator fun invoke(userWallet: UserWallet, network: Network, transactionData: TransactionData) = either { + catch( + block = { + val transactionSender = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { + demoTransactionSender(userWallet, network) + } else { + walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + network = network, + ) as? TransactionSender + } + val result = transactionSender?.getFee(transactionData = transactionData) + ?: 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)) + }, + ) + } + suspend operator fun invoke( amount: BigDecimal, destination: String, @@ -36,7 +61,7 @@ class GetFeeUseCase( val amountData = convertCryptoCurrencyToAmount(cryptoCurrency, amount) val result = if (demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { - demoTransactionSender(userWallet, cryptoCurrency).getFee( + demoTransactionSender(userWallet, cryptoCurrency.network).getFee( amount = amountData, destination = destination, ) @@ -61,13 +86,9 @@ class GetFeeUseCase( ) } - private suspend fun demoTransactionSender( - userWallet: UserWallet, - cryptoCurrency: CryptoCurrency, - ): DemoTransactionSender { + private suspend fun demoTransactionSender(userWallet: UserWallet, network: Network): DemoTransactionSender { return DemoTransactionSender( - walletManagersFacade - .getOrCreateWalletManager(userWallet.walletId, cryptoCurrency.network) + walletManagersFacade.getOrCreateWalletManager(userWallet.walletId, network) ?: error("WalletManager is null"), ) } 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 1aff7cea04..fbe655766a 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.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -10,4 +11,8 @@ interface WcMethodUseCase : WcUseCase { val session: WcSession val rawSdkRequest: WcSdkSessionRequest val network: Network +} + +interface WcMutableFee { + fun updateFee(fee: Fee) } \ No newline at end of file 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 index 7df50defdc..48f74153de 100644 --- 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 @@ -1,24 +1,42 @@ package com.tangem.domain.walletconnect.usecase.ethereum import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.common.transaction.Fee +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.WcMutableFee import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase interface WcEthSendTransactionUseCase : WcSignUseCase, - WcSignUseCase.SimpleRun { + WcSignUseCase.SimpleRun, + WcMutableFee { - fun updateFee(fee: TransactionFee) + override val session: WcSession + override val rawSdkRequest: WcSdkSessionRequest + override val network: Network + + override fun sign() + override fun cancel() + override fun updateFee(fee: Fee) } interface WcEthSignTransactionUseCase : WcSignUseCase, - WcSignUseCase.SimpleRun { + WcSignUseCase.SimpleRun, + WcMutableFee { - fun updateFee(fee: TransactionFee) + override val session: WcSession + override val rawSdkRequest: WcSdkSessionRequest + override val network: Network + + override fun sign() + override fun cancel() + override fun updateFee(fee: Fee) } data class WcEthTransaction( - val isFeeByDap: Boolean, - val fee: TransactionFee, + val dAppFee: Fee.Ethereum.Legacy?, + val transactionData: TransactionData.Uncompiled, ) \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index bcba22609f..0c3b257143 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-1040" +tangemBlockchainSdk = "develop-1041" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 588456cc71f6766821cad0e0441594ecafddc384 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 21 Apr 2025 15:29:10 +0300 Subject: [PATCH 084/151] Updated on 2026-08-14 --- .../fetcher/YieldBalanceFetcherImplementor.kt | 29 ++++++ .../MultiYieldBalanceFetcherImplementor.kt | 58 ++++++++++++ .../SingleYieldBalanceFetcherImplementor.kt | 91 +++++++++++++++++++ .../store/DefaultYieldsBalancesStore.kt | 53 ++++------- .../data/staking/store/YieldsBalancesStore.kt | 4 +- .../YieldsBalancesStoreUpdateMethodsTest.kt | 4 +- .../fetcher/YieldBalanceFetcherParams.kt | 40 ++++++++ 7 files changed, 240 insertions(+), 39 deletions(-) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/fetcher/YieldBalanceFetcherImplementor.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/multi/MultiYieldBalanceFetcherImplementor.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/single/SingleYieldBalanceFetcherImplementor.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt diff --git a/data/staking/src/main/java/com/tangem/data/staking/fetcher/YieldBalanceFetcherImplementor.kt b/data/staking/src/main/java/com/tangem/data/staking/fetcher/YieldBalanceFetcherImplementor.kt new file mode 100644 index 0000000000..27835a3da0 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/fetcher/YieldBalanceFetcherImplementor.kt @@ -0,0 +1,29 @@ +package com.tangem.data.staking.fetcher + +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.model.StakingID + +/** + * Implementor of internal logic of YieldBalanceFetcher + * +[REDACTED_AUTHOR] + */ +internal interface YieldBalanceFetcherImplementor { + + /** Create set of [StakingID] */ + suspend fun createStakingIds(params: @UnsafeVariance Params): Set + + /** + * Fetch yield balances + * + * @param params params + * @param stakingIds set of [StakingID] + * @param requests requests + */ + suspend fun fetch( + params: @UnsafeVariance Params, + stakingIds: Set, + requests: List, + ) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/MultiYieldBalanceFetcherImplementor.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/MultiYieldBalanceFetcherImplementor.kt new file mode 100644 index 0000000000..8edc3997bc --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/MultiYieldBalanceFetcherImplementor.kt @@ -0,0 +1,58 @@ +package com.tangem.data.staking.multi + +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.staking.fetcher.YieldBalanceFetcherImplementor +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.model.StakingID +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** + * Implementor of fetcher for refreshing multiple yield balances + * + * @property yieldsBalancesStore yields balances store + * @property stakingIdFactory factory for creating [StakingID] + * @property stakeKitApi StakeKit API + * @property dispatchers dispatchers + */ +internal class MultiYieldBalanceFetcherImplementor( + private val yieldsBalancesStore: YieldsBalancesStore, + private val stakingIdFactory: StakingIdFactory, + private val stakeKitApi: StakeKitApi, + private val dispatchers: CoroutineDispatcherProvider, +) : YieldBalanceFetcherImplementor { + + override suspend fun createStakingIds(params: YieldBalanceFetcherParams.Multi): Set { + return params.currencyIdWithNetworkMap.flatMapTo(hashSetOf()) { (currencyId, network) -> + stakingIdFactory.create(userWalletId = params.userWalletId, currencyId = currencyId, network = network) + } + } + + override suspend fun fetch( + params: YieldBalanceFetcherParams.Multi, + stakingIds: Set, + requests: List, + ) { + safeApiCall( + call = { + val yieldBalances = withContext(dispatchers.io) { + stakeKitApi.getMultipleYieldBalances(requests).bind() + } + + yieldsBalancesStore.storeActual(userWalletId = params.userWalletId, values = yieldBalances) + }, + onError = { + Timber.e(it, "Unable to fetch yield balances $params") + + yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds) + + throw it + }, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/SingleYieldBalanceFetcherImplementor.kt b/data/staking/src/main/java/com/tangem/data/staking/single/SingleYieldBalanceFetcherImplementor.kt new file mode 100644 index 0000000000..6a47800b12 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/single/SingleYieldBalanceFetcherImplementor.kt @@ -0,0 +1,91 @@ +package com.tangem.data.staking.single + +import com.tangem.data.common.api.safeApiCall +import com.tangem.data.staking.fetcher.YieldBalanceFetcherImplementor +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.model.StakingID +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import timber.log.Timber + +/** + * Implementor of fetcher for refreshing single yield balance + * + * @property yieldsBalancesStore yields balances store + * @property stakingIdFactory factory for creating [StakingID] + * @property stakeKitApi StakeKit API + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class SingleYieldBalanceFetcherImplementor( + private val yieldsBalancesStore: YieldsBalancesStore, + private val stakingIdFactory: StakingIdFactory, + private val stakeKitApi: StakeKitApi, + private val dispatchers: CoroutineDispatcherProvider, +) : YieldBalanceFetcherImplementor { + + override suspend fun createStakingIds(params: YieldBalanceFetcherParams.Single): Set { + val dataStakingId = stakingIdFactory.createForDefault( + userWalletId = params.userWalletId, + currencyId = params.currencyId, + network = params.network, + ) ?: return emptySet() + + return setOf( + StakingID(integrationId = dataStakingId.integrationId, address = dataStakingId.address), + ) + } + + override suspend fun fetch( + params: YieldBalanceFetcherParams.Single, + stakingIds: Set, + requests: List, + ) { + fetchInternal( + params = params, + stakingId = stakingIds.first(), + request = requests.first(), + ) + } + + private suspend fun fetchInternal( + params: YieldBalanceFetcherParams.Single, + stakingId: StakingID, + request: YieldBalanceRequestBody, + ) { + safeApiCall( + call = { + val result = withContext(dispatchers.io) { + stakeKitApi.getSingleYieldBalance( + integrationId = stakingId.integrationId, + body = request, + ).bind() + } + + yieldsBalancesStore.storeActual( + userWalletId = params.userWalletId, + values = setOf( + YieldBalanceWrapperDTO( + balances = result, + integrationId = request.integrationId, + addresses = request.addresses, + ), + ), + ) + }, + onError = { + Timber.e(it, "Unable to fetch yield balances $params") + + yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = setOf(stakingId)) + + throw it + }, + ) + } +} \ No newline at end of file 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 index c3a8b2a74c..18fb91c240 100644 --- 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 @@ -59,32 +59,14 @@ internal class DefaultYieldsBalancesStore( } override suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) { - updateBalanceInRuntime(userWalletId, stakingId) { + updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = setOf(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) - } + updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) { + it.copySealed(source = StatusSource.CACHE) } } @@ -95,8 +77,8 @@ internal class DefaultYieldsBalancesStore( } } - override suspend fun storeError(userWalletId: UserWalletId, stakingId: StakingID) { - updateBalanceInRuntime(userWalletId, stakingId) { + override suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) { + updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) { it.copySealed(source = StatusSource.ONLY_CACHE) } } @@ -127,26 +109,27 @@ internal class DefaultYieldsBalancesStore( } } - private suspend fun updateBalanceInRuntime( + private suspend fun updateBalancesInRuntime( userWalletId: UserWalletId, - stakingID: StakingID, + stakingIds: Set, 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 portfolioBalances = stored[userWalletId].orEmpty() - val updatedBalances = this[userWalletId].orEmpty() - .addOrReplace(item = update(balance)) { - it.integrationId == balance.integrationId && - it.address == balance.address + val balances = stakingIds.mapTo(hashSetOf()) { stakingId -> + val balance = portfolioBalances.firstOrNull { + it.integrationId == stakingId.integrationId && it.address == stakingId.address } + if (balance != null) update(balance) else createDefaultBalance(id = stakingId) + } + + val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new -> + old.integrationId == new.integrationId && old.address == new.address + } + put(key = userWalletId, value = updatedBalances) } } 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 index 3371d07f1c..326dc6fc73 100644 --- 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 @@ -25,6 +25,6 @@ interface YieldsBalancesStore { /** 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) + /** Store error by [userWalletId] and [stakingIds] */ + suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) } \ 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 index 8c51eacb19..dc05b3db34 100644 --- 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 @@ -137,7 +137,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { @Test fun `store error if runtime store is empty`() = runTest { - store.storeError(userWalletId = userWalletId, stakingId = stakingId) + store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId)) val runtimeExpected = mapOf( userWalletId to setOf( @@ -162,7 +162,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { ), ) - store.storeError(userWalletId = userWalletId, stakingId = stakingId) + store.storeError(userWalletId = userWalletId, stakingIds = setOf(stakingId)) val runtimeExpected = mapOf( userWalletId to setOf(wrapper.toDomain(source = StatusSource.ONLY_CACHE)), diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt b/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt new file mode 100644 index 0000000000..ce15ede323 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/fetcher/YieldBalanceFetcherParams.kt @@ -0,0 +1,40 @@ +package com.tangem.domain.staking.fetcher + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWalletId + +/** + * Params for fetchers of yield balance + * +[REDACTED_AUTHOR] + */ +sealed interface YieldBalanceFetcherParams { + + /** User wallet ID */ + val userWalletId: UserWalletId + + /** + * Params for fetching multiple yield balances + * + * @property userWalletId user wallet ID + * @property currencyIdWithNetworkMap map of currency ID to network + */ + data class Multi( + override val userWalletId: UserWalletId, + val currencyIdWithNetworkMap: Map, + ) : YieldBalanceFetcherParams + + /** + * Params for fetching single yield balance + * + * @property userWalletId user wallet ID + * @property currencyId currency ID + * @property network network + */ + data class Single( + override val userWalletId: UserWalletId, + val currencyId: CryptoCurrency.ID, + val network: Network, + ) : YieldBalanceFetcherParams +} \ No newline at end of file From 896da1def0451ff40e6026cbecf4bec5191e067d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 17:19:42 +0500 Subject: [PATCH 085/151] Updated on 2026-08-14 --- .../NotificationApplicationCreateBody.kt | 12 +- .../api/tangemTech/models/WalletBody.kt | 2 +- .../api/tangemTech/models/WalletResponse.kt | 4 +- .../com/tangem/datasource/di/AppInfoModule.kt | 21 ++ .../datasource/info/AndroidAppInfoProvider.kt | 25 ++ .../local/preferences/PreferencesKeys.kt | 4 + .../com/tangem/utils/info/AppInfoProvider.kt | 10 + data/notifications/.gitignore | 1 + data/notifications/build.gradle.kts | 46 ++++ .../DefaultNotificationsRepository.kt | 96 +++++++ .../NotificationsEligibleNetworkConverter.kt | 15 + .../notifications/di/NotificationsModule.kt | 18 ++ .../DefaultNotificationsRepositoryTest.kt | 256 ++++++++++++++++++ domain/notifications/.gitignore | 1 + domain/notifications/build.gradle.kts | 12 + domain/notifications/models/.gitignore | 1 + domain/notifications/models/build.gradle.kts | 5 + .../models/NotificationsEligibleNetwork.kt | 7 + .../notifications/NotificationsRepository.kt | 34 +++ settings.gradle.kts | 3 + 20 files changed, 564 insertions(+), 9 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/AppInfoModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/info/AndroidAppInfoProvider.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/info/AppInfoProvider.kt create mode 100644 data/notifications/.gitignore create mode 100644 data/notifications/build.gradle.kts create mode 100644 data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt create mode 100644 data/notifications/src/main/java/com/tangem/data/notifications/converters/NotificationsEligibleNetworkConverter.kt create mode 100644 data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt create mode 100644 data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt create mode 100644 domain/notifications/.gitignore create mode 100644 domain/notifications/build.gradle.kts create mode 100644 domain/notifications/models/.gitignore create mode 100644 domain/notifications/models/build.gradle.kts create mode 100644 domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/NotificationsEligibleNetwork.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt 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 index e28589eefa..fdc296f8c1 100644 --- 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 @@ -5,10 +5,10 @@ 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, + @Json(name = "pushToken") val pushToken: String? = null, + @Json(name = "platform") val platform: String? = null, + @Json(name = "device") val device: String? = null, + @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 index d7a5fa0b91..6ad3704bfb 100644 --- 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 @@ -5,6 +5,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class WalletBody( - @Json(name = "notifyStatus") val notifyStatus: String? = null, + @Json(name = "notifyStatus") val notifyStatus: Boolean? = 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/WalletResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/WalletResponse.kt index ad0ff4ebc6..63263f0a1d 100644 --- 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 @@ -5,7 +5,7 @@ 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 = "notifyStatus") val notifyStatus: Boolean, @Json(name = "id") val id: String, + @Json(name = "name") val name: String? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/AppInfoModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/AppInfoModule.kt new file mode 100644 index 0000000000..c67a5baa1f --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/AppInfoModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.info.AndroidAppInfoProvider +import com.tangem.utils.info.AppInfoProvider +import com.tangem.utils.version.AppVersionProvider +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 AppInfoModule { + + @Singleton + @Provides + fun provideAppInfoProvider(appVersionProvider: AppVersionProvider): AppInfoProvider { + return AndroidAppInfoProvider(appVersionProvider) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/info/AndroidAppInfoProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/info/AndroidAppInfoProvider.kt new file mode 100644 index 0000000000..08d960e333 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/info/AndroidAppInfoProvider.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.info + +import android.os.Build +import com.tangem.utils.SupportedLanguages +import com.tangem.utils.info.AppInfoProvider +import com.tangem.utils.version.AppVersionProvider +import java.util.* +import javax.inject.Inject + +internal class AndroidAppInfoProvider @Inject constructor( + private val appVersionProvider: AppVersionProvider, +) : AppInfoProvider { + override val platform: String + get() = "Android" + override val device: String + get() = "${Build.MANUFACTURER} ${Build.MODEL}" + override val osVersion: String + get() = Build.VERSION.RELEASE + override val language: String + get() = SupportedLanguages.getCurrentSupportedLanguageCode() + override val timezone: String + get() = TimeZone.getDefault().id + override val appVersion: String + get() = appVersionProvider.versionName +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 50275788dd..5177c8d4aa 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -123,6 +123,10 @@ object PreferencesKeys { val WALLETS_NFT_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "walletsNftEnabledStates") } + // region Notifications + val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") } + // endregion + fun getShouldShowStoriesKey(storyId: String) = booleanPreferencesKey("shouldShowStories_$storyId") // region Permission diff --git a/core/utils/src/main/java/com/tangem/utils/info/AppInfoProvider.kt b/core/utils/src/main/java/com/tangem/utils/info/AppInfoProvider.kt new file mode 100644 index 0000000000..0ab3e32398 --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/info/AppInfoProvider.kt @@ -0,0 +1,10 @@ +package com.tangem.utils.info + +interface AppInfoProvider { + val platform: String + val device: String + val osVersion: String + val language: String + val timezone: String + val appVersion: String +} \ No newline at end of file diff --git a/data/notifications/.gitignore b/data/notifications/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/data/notifications/.gitignore @@ -0,0 +1 @@ +/build diff --git a/data/notifications/build.gradle.kts b/data/notifications/build.gradle.kts new file mode 100644 index 0000000000..b3c107f390 --- /dev/null +++ b/data/notifications/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.data.notifications" +} + +dependencies { + + // region AndroidX libraries + implementation(deps.androidx.datastore) + implementation(deps.kotlin.coroutines) + // endregion + + // region DI + implementation(deps.hilt.core) + kapt(deps.hilt.kapt) + // endregion + + // region Arrow + implementation(deps.arrow.core) + // endregion + + // region Core modules + implementation(projects.core.datasource) + implementation(projects.core.utils) + // endregion + + // region Domain modules + implementation(projects.domain.notifications.models) + implementation(projects.domain.notifications) + // endregion + + // region tests + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) + testImplementation(deps.moshi) + testImplementation(deps.moshi.kotlin) + // endregion +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt new file mode 100644 index 0000000000..3f8c27c889 --- /dev/null +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -0,0 +1,96 @@ +package com.tangem.data.notifications + +import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.getOrThrow +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.NotificationApplicationCreateBody +import com.tangem.datasource.api.tangemTech.models.WalletBody +import com.tangem.datasource.api.tangemTech.models.WalletIdBody +import com.tangem.utils.info.AppInfoProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.* +import com.tangem.domain.notifications.NotificationsRepository +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultNotificationsRepository @Inject constructor( + private val tangemTechApi: TangemTechApi, + private val appInfoProvider: AppInfoProvider, + private val appPreferencesStore: AppPreferencesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : NotificationsRepository { + + override suspend fun createApplicationId(pushToken: String?): String = withContext(dispatchers.io) { + tangemTechApi.createApplicationId( + NotificationApplicationCreateBody( + platform = appInfoProvider.platform, + device = appInfoProvider.device, + systemVersion = appInfoProvider.osVersion, + language = appInfoProvider.language, + timezone = appInfoProvider.timezone, + pushToken = pushToken, + ), + ).getOrThrow().appId + } + + override suspend fun saveApplicationId(appId: String) { + appPreferencesStore.store(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY, appId) + } + + override suspend fun getApplicationId(): String? { + return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) + } + + override suspend fun setNotificationsEnabledForWallet(walletId: String, enabled: Boolean) = + withContext(dispatchers.io) { + tangemTechApi.setNotificationsEnabled( + walletId = walletId, + body = WalletBody(notifyStatus = enabled), + ).getOrThrow() + } + + override suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) = + withContext(dispatchers.io) { + tangemTechApi.associateApplicationIdWithWallets( + applicationId = appId, + body = wallets.map { + WalletIdBody(it) + }, + ).getOrThrow() + } + + override suspend fun isNotificationsEnabledForWallet(walletId: String): Boolean = withContext(dispatchers.io) { + tangemTechApi.getWalletById(walletId).getOrThrow().notifyStatus + } + + override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { + tangemTechApi.updateWallet( + walletId, + WalletBody(name = walletName), + ).getOrThrow() + } + + override suspend fun getWalletName(walletId: String): String? = withContext(dispatchers.io) { + tangemTechApi.getWalletById(walletId).getOrThrow().name + } + + override suspend fun sendPushToken(appId: String, pushToken: String) { + withContext(dispatchers.io) { + tangemTechApi.updatePushTokenForApplicationId( + appId, + NotificationApplicationCreateBody( + pushToken = pushToken, + ), + ).getOrThrow() + } + } + + override suspend fun getEligibleNetworks(): List = withContext(dispatchers.io) { + tangemTechApi.getEligibleNetworksForPushNotifications().getOrThrow().map { + NotificationsEligibleNetworkConverter.convert(it) + } + } +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/converters/NotificationsEligibleNetworkConverter.kt b/data/notifications/src/main/java/com/tangem/data/notifications/converters/NotificationsEligibleNetworkConverter.kt new file mode 100644 index 0000000000..471dd0b137 --- /dev/null +++ b/data/notifications/src/main/java/com/tangem/data/notifications/converters/NotificationsEligibleNetworkConverter.kt @@ -0,0 +1,15 @@ +package com.tangem.data.notifications.converters + +import com.tangem.datasource.api.tangemTech.models.CryptoNetworkResponse +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork +import com.tangem.utils.converter.Converter + +internal object NotificationsEligibleNetworkConverter : Converter { + override fun convert(value: CryptoNetworkResponse): NotificationsEligibleNetwork { + return NotificationsEligibleNetwork( + id = value.id.toString(), + networkId = value.networkId, + name = value.name, + ) + } +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt new file mode 100644 index 0000000000..9358da820e --- /dev/null +++ b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt @@ -0,0 +1,18 @@ +package com.tangem.data.notifications.di + +import com.tangem.data.notifications.DefaultNotificationsRepository +import com.tangem.domain.notifications.NotificationsRepository +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 NotificationsModule { + + @Binds + @Singleton + fun bindNotificationsRepository(repository: DefaultNotificationsRepository): NotificationsRepository +} \ No newline at end of file diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt new file mode 100644 index 0000000000..65c9a1220d --- /dev/null +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -0,0 +1,256 @@ +package com.tangem.data.notifications + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.stringPreferencesKey +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.utils.info.AppInfoProvider +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.Moshi +import com.tangem.data.notifications.converters.NotificationsEligibleNetworkConverter +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.models.* +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultNotificationsRepositoryTest { + private val tangemTechApi: TangemTechApi = mockk() + private val appInfoProvider: AppInfoProvider = mockk() + private val preferencesDataStore: DataStore = mockk() + private val appPreferencesStore = AppPreferencesStore( + moshi = Moshi.Builder().build(), + dispatchers = TestingCoroutineDispatcherProvider(), + preferencesDataStore = preferencesDataStore, + ) + private val repository = DefaultNotificationsRepository( + tangemTechApi = tangemTechApi, + appInfoProvider = appInfoProvider, + appPreferencesStore = appPreferencesStore, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `GIVEN valid push token WHEN createApplicationId THEN returns application id`() = runTest { + // GIVEN + val pushToken = "test-push-token" + val expectedAppId = "test-app-id" + val expectedAppIdResponse = NotificationApplicationIdResponse( + appId = expectedAppId, + ) + coEvery { appInfoProvider.platform } returns "android" + coEvery { appInfoProvider.device } returns "test-device" + coEvery { appInfoProvider.osVersion } returns "11" + coEvery { appInfoProvider.language } returns "en" + coEvery { appInfoProvider.timezone } returns "UTC" + coEvery { tangemTechApi.createApplicationId(any()) } returns ApiResponse.Success( + expectedAppIdResponse, + ) + + // WHEN + val result = repository.createApplicationId(pushToken) + + // THEN + assertThat(result).isEqualTo(expectedAppId) + coVerify { + tangemTechApi.createApplicationId( + NotificationApplicationCreateBody( + platform = "android", + device = "test-device", + systemVersion = "11", + language = "en", + timezone = "UTC", + pushToken = pushToken, + ), + ) + } + } + + @Test + fun `GIVEN application id WHEN saveApplicationId THEN stores it in preferences`() = runTest { + // GIVEN + val appId = "test-app-id" + val preferences = mockk(relaxed = true) + coEvery { preferencesDataStore.updateData(any()) } returns preferences + + // WHEN + repository.saveApplicationId(appId) + + // THEN + coVerify { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN stored application id WHEN getApplicationId THEN returns it`() = runTest { + // GIVEN + val expectedAppId = "test-app-id" + val preferences = mockk(relaxed = true) + val key = stringPreferencesKey(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY.name) + every { preferences[key] } returns expectedAppId + coEvery { preferencesDataStore.data } returns flowOf(preferences) + + // WHEN + val result = repository.getApplicationId() + + // THEN + assertThat(result).isEqualTo(expectedAppId) + } + + @Test + fun `GIVEN wallet id and enabled status WHEN setNotificationsEnabledForWallet THEN updates notification status`() = + runTest { + // GIVEN + val walletId = "test-wallet-id" + val enabled = true + coEvery { + tangemTechApi.setNotificationsEnabled( + walletId, + WalletBody(notifyStatus = enabled), + ) + } returns ApiResponse.Success(Unit) + + // WHEN + repository.setNotificationsEnabledForWallet(walletId, enabled) + + // THEN + coVerify { tangemTechApi.setNotificationsEnabled(walletId, WalletBody(notifyStatus = enabled)) } + } + + @Test + fun `GIVEN application id and wallet list WHEN associateApplicationIdWithWallets THEN associates them`() = runTest { + // GIVEN + val appId = "test-app-id" + val wallets = listOf("wallet1", "wallet2") + coEvery { + tangemTechApi.associateApplicationIdWithWallets( + appId, + wallets.map { WalletIdBody(it) }, + ) + } returns ApiResponse.Success(Unit) + + // WHEN + repository.associateApplicationIdWithWallets(appId, wallets) + + // THEN + coVerify { tangemTechApi.associateApplicationIdWithWallets(appId, wallets.map { WalletIdBody(it) }) } + } + + @Test + fun `GIVEN wallet id WHEN isNotificationsEnabledForWallet THEN returns notification status`() = runTest { + // GIVEN + val walletId = "test-wallet-id" + val expectedStatus = true + coEvery { tangemTechApi.getWalletById(walletId) } returns ApiResponse.Success( + WalletResponse( + notifyStatus = expectedStatus, + name = "Test Wallet", + id = walletId, + ), + ) + + // WHEN + val result = repository.isNotificationsEnabledForWallet(walletId) + + // THEN + assertThat(result).isEqualTo(expectedStatus) + } + + @Test + fun `GIVEN wallet id and name WHEN setWalletName THEN updates wallet name`() = runTest { + // GIVEN + val walletId = "test-wallet-id" + val walletName = "Test Wallet" + coEvery { + tangemTechApi.updateWallet( + walletId, + WalletBody(name = walletName), + ) + } returns ApiResponse.Success(Unit) + + // WHEN + repository.setWalletName(walletId, walletName) + + // THEN + coVerify { tangemTechApi.updateWallet(walletId, WalletBody(name = walletName)) } + } + + @Test + fun `GIVEN wallet id WHEN getWalletName THEN returns wallet name`() = runTest { + // GIVEN + val walletId = "test-wallet-id" + val expectedName = "Test Wallet" + coEvery { tangemTechApi.getWalletById(walletId) } returns ApiResponse.Success( + WalletResponse( + notifyStatus = false, + name = expectedName, + id = walletId, + ), + ) + + // WHEN + val result = repository.getWalletName(walletId) + + // THEN + assertThat(result).isEqualTo(expectedName) + } + + @Test + fun `GIVEN application id and push token WHEN sendPushToken THEN updates push token`() = runTest { + // GIVEN + val appId = "test-app-id" + val pushToken = "test-push-token" + coEvery { + tangemTechApi.updatePushTokenForApplicationId( + appId, + NotificationApplicationCreateBody(pushToken = pushToken), + ) + } returns ApiResponse.Success(appId) + + // WHEN + repository.sendPushToken(appId, pushToken) + + // THEN + coVerify { + tangemTechApi.updatePushTokenForApplicationId( + appId, + NotificationApplicationCreateBody(pushToken = pushToken), + ) + } + } + + @Test + fun `GIVEN eligible networks WHEN getEligibleNetworks THEN returns converted networks`() = runTest { + // GIVEN + val expectedNetworks = listOf( + CryptoNetworkResponse( + id = 1, + name = "Ethereum", + networkId = "Ethereum", + ), + CryptoNetworkResponse( + id = 2, + name = "Bitcoin", + networkId = "bitcoin", + ), + ) + coEvery { tangemTechApi.getEligibleNetworksForPushNotifications() } returns ApiResponse.Success( + expectedNetworks, + ) + + // WHEN + val result = repository.getEligibleNetworks() + + // THEN + assertThat(result).hasSize(2) + assertThat(result[0]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[0])) + assertThat(result[1]).isEqualTo(NotificationsEligibleNetworkConverter.convert(expectedNetworks[1])) + coVerify { tangemTechApi.getEligibleNetworksForPushNotifications() } + } +} \ No newline at end of file diff --git a/domain/notifications/.gitignore b/domain/notifications/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/notifications/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/notifications/build.gradle.kts b/domain/notifications/build.gradle.kts new file mode 100644 index 0000000000..485c40d9ab --- /dev/null +++ b/domain/notifications/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(deps.arrow.core) + implementation(deps.kotlin.coroutines) + + implementation(projects.domain.notifications.models) +} \ No newline at end of file diff --git a/domain/notifications/models/.gitignore b/domain/notifications/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/notifications/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/notifications/models/build.gradle.kts b/domain/notifications/models/build.gradle.kts new file mode 100644 index 0000000000..7dd715c617 --- /dev/null +++ b/domain/notifications/models/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} \ No newline at end of file diff --git a/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/NotificationsEligibleNetwork.kt b/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/NotificationsEligibleNetwork.kt new file mode 100644 index 0000000000..7cd89c6754 --- /dev/null +++ b/domain/notifications/models/src/main/java/com/tangem/domain/notifications/models/NotificationsEligibleNetwork.kt @@ -0,0 +1,7 @@ +package com.tangem.domain.notifications.models + +data class NotificationsEligibleNetwork( + val id: String, + val networkId: String, + val name: String, +) \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt new file mode 100644 index 0000000000..bb42e3efab --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt @@ -0,0 +1,34 @@ +package com.tangem.domain.notifications + +import com.tangem.domain.notifications.models.NotificationsEligibleNetwork + +interface NotificationsRepository { + + @Throws + suspend fun createApplicationId(pushToken: String? = null): String + + suspend fun saveApplicationId(appId: String) + + suspend fun getApplicationId(): String? + + @Throws + suspend fun setNotificationsEnabledForWallet(walletId: String, enabled: Boolean) + + @Throws + suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) + + @Throws + suspend fun isNotificationsEnabledForWallet(walletId: String): Boolean + + @Throws + suspend fun setWalletName(walletId: String, walletName: String) + + @Throws + suspend fun getWalletName(walletId: String): String? + + @Throws + suspend fun sendPushToken(appId: String, pushToken: String) + + @Throws + suspend fun getEligibleNetworks(): List +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 01e4240001..5aa3d2ba61 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -291,6 +291,8 @@ include(":domain:networks") include(":domain:quotes") include(":domain:blockaid") include(":domain:blockaid:models") +include(":domain:notifications") +include(":domain:notifications:models") // endregion Domain modules // region Data modules @@ -318,5 +320,6 @@ include(":data:networks") include(":data:nft") include(":data:onramp") include(":data:quotes") +include(":data:notifications") include(":data:blockaid") // endregion Data modules \ No newline at end of file From c5a7662d01b81c22d5edb0f361621ac5765d299c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 17:20:32 +0500 Subject: [PATCH 086/151] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../main/java/com/tangem/tap/common/images/Coil.kt | 12 ++++++++++++ gradle/dependencies.toml | 1 + 3 files changed, 14 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 700a9a7754..8120e8c86e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -275,6 +275,7 @@ dependencies { implementation(deps.reKotlin) implementation(deps.zxing.qrCore) implementation(deps.coil) + implementation(deps.coil.gif) implementation(deps.amplitude) implementation(deps.kotsonGson) implementation(deps.spongecastle.core) diff --git a/app/src/main/java/com/tangem/tap/common/images/Coil.kt b/app/src/main/java/com/tangem/tap/common/images/Coil.kt index 4b269bdca4..28336b233f 100644 --- a/app/src/main/java/com/tangem/tap/common/images/Coil.kt +++ b/app/src/main/java/com/tangem/tap/common/images/Coil.kt @@ -1,8 +1,11 @@ package com.tangem.tap.common.images import android.content.Context +import android.os.Build import android.util.Log import coil.ImageLoader +import coil.decode.GifDecoder +import coil.decode.ImageDecoderDecoder import coil.memory.MemoryCache import coil.request.CachePolicy import coil.util.Logger @@ -25,6 +28,15 @@ fun createCoilImageLoader(context: Context, logEnabled: Boolean = false): ImageL .build() } } + .components { + // According to Coil ImageDecoder API is faster and supports animated WebP and HEIF + // https://coil-kt.github.io/coil/gifs/ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + add(ImageDecoderDecoder.Factory()) + } else { + add(GifDecoder.Factory()) + } + } .memoryCachePolicy(CachePolicy.ENABLED) .memoryCache { MemoryCache.Builder(context) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 3f1defd17d..2353d61b82 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -214,6 +214,7 @@ test-hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", versi amplitude = { module = "com.amplitude:android-sdk", version.ref = "amplitude" } armadillo = { module = "at.favre.lib:armadillo", version.ref = "armadillo" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } +coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" } kotlin-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutine" } kotlin-coroutines-rx2 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-rx2", version.ref = "coroutine" } kotlin-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutine" } From 736ef3e45e3e8aa3cfe290e44ab97a8aedf522f5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 15:16:49 +0500 Subject: [PATCH 087/151] Updated on 2026-08-14 --- .../collections/ui/NFTCollectionsContent.kt | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) 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 bb30235ff8..d774219ff8 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 @@ -1,8 +1,7 @@ package com.tangem.features.nft.collections.ui import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -150,7 +149,11 @@ private fun LazyListScope.assetsListLoading( item( key = "loading_${collectionId}_$rowIndex", ) { - AnimatedVisibility(visible = expanded) { + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { val paddingValues = PaddingValues( start = TangemTheme.dimens.spacing6, top = TangemTheme.dimens.spacing6, @@ -160,8 +163,7 @@ private fun LazyListScope.assetsListLoading( Row( modifier = Modifier .fillMaxWidth() - .padding(TangemTheme.dimens.spacing6) - .animateContentSize(), + .padding(TangemTheme.dimens.spacing6), ) { NFTCollectionAssetLoading( modifier = Modifier @@ -195,12 +197,15 @@ private fun LazyListScope.assetsListFailed( item( key = "failed_$collectionId", ) { - AnimatedVisibility(visible = expanded) { + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { Box( modifier = Modifier .fillMaxWidth() - .height(TangemTheme.dimens.size142) - .animateContentSize(), + .height(TangemTheme.dimens.size142), contentAlignment = Alignment.Center, ) { UnableToLoadData( @@ -225,7 +230,11 @@ private fun LazyListScope.assetsListContent( item( key = "content_${collectionId}_${item1.id}_${item2?.id}", ) { - AnimatedVisibility(visible = expanded) { + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { val paddingValues = PaddingValues( start = TangemTheme.dimens.spacing6, top = TangemTheme.dimens.spacing6, @@ -235,8 +244,7 @@ private fun LazyListScope.assetsListContent( Row( modifier = Modifier .fillMaxWidth() - .padding(TangemTheme.dimens.spacing6) - .animateContentSize(), + .padding(TangemTheme.dimens.spacing6), ) { NFTCollectionAsset( modifier = Modifier From e87d946e4091c0a0fb873d56102b21b7f14b740b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 15:17:24 +0700 Subject: [PATCH 088/151] Updated on 2026-08-14 --- .../DefaultTransactionRepository.kt | 39 +++++++--- .../di/WalletConnectDataModule.kt | 11 ++- ...efaultWcSolanaSignAllTransactionUseCase.kt | 78 +++++++++++++++++++ .../DefaultWcSolanaSignTransactionUseCase.kt | 57 ++++++++++++++ .../walletconnect/network/solana/Model.kt | 19 +++++ .../network/solana/WcSolanaNetwork.kt | 54 ++++++++++++- .../transaction/TransactionRepository.kt | 7 ++ .../usecase/PrepareForSendUseCase.kt | 34 ++++++-- .../walletconnect/model/WcSolanaMethod.kt | 17 ++++ .../solana/WcSolanaSignTransactionUseCase.kt | 35 +++++++++ 10 files changed, 332 insertions(+), 19 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt 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 da6951cafd..7c4b516b71 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 @@ -277,20 +277,39 @@ internal class DefaultTransactionRepository( 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") - } + val preparer = getPreparer(network, userWalletId) 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) } } + + override suspend fun prepareForSendMultiple( + transactionData: List, + signer: TransactionSigner, + userWalletId: UserWalletId, + network: Network, + ): Result> = withContext(coroutineDispatcherProvider.io) { + val preparer = getPreparer(network, userWalletId) + + when (val prepareForSend = preparer.prepareForSendMultiple(transactionData, signer)) { + is com.tangem.blockchain.extensions.Result.Failure -> Result.failure(prepareForSend.error) + is com.tangem.blockchain.extensions.Result.Success -> Result.success(prepareForSend.data) + } + } + + private suspend fun getPreparer(network: Network, userWalletId: UserWalletId): TransactionPreparer { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + val preparer = walletManager as? TransactionPreparer ?: run { + Timber.e("${walletManager?.wallet?.blockchain} does not support TransactionBuilder") + error("Wallet manager does not support TransactionPreparer") + } + return preparer + } } \ 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 385089f187..3c2b20e4f5 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 @@ -144,7 +144,15 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun wcSolanaNetwork(excludedBlockchains: ExcludedBlockchains): WcSolanaNetwork = WcSolanaNetwork( + fun wcSolanaNetwork( + @SdkMoshi moshi: Moshi, + excludedBlockchains: ExcludedBlockchains, + sessionsManager: WcSessionsManager, + factories: WcSolanaNetwork.Factories, + ): WcSolanaNetwork = WcSolanaNetwork( + moshi = moshi, + sessionsManager = sessionsManager, + factories = factories, excludedBlockchains = excludedBlockchains, ) @@ -175,6 +183,7 @@ internal object WalletConnectDataModule { fun diHelperBox(ethNetwork: WcEthNetwork, solanaNetwork: WcSolanaNetwork) = DiHelperBox( handlers = setOf( ethNetwork, + solanaNetwork, ), converters = setOf( ethNetwork, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt new file mode 100644 index 0000000000..4724192a80 --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt @@ -0,0 +1,78 @@ +package com.tangem.data.walletconnect.network.solana + +import arrow.core.left +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.extensions.encodeBase64 +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.sign.BaseWcSignUseCase +import com.tangem.data.walletconnect.sign.SignCollector +import com.tangem.data.walletconnect.sign.SignStateConverter.toResult +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import com.tangem.domain.walletconnect.usecase.solana.WcSolanaSignAllTransactionUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import okio.ByteString.Companion.decodeBase64 +import org.json.JSONArray +import org.json.JSONObject + +internal class DefaultWcSolanaSignAllTransactionUseCase @AssistedInject constructor( + override val respondService: WcRespondService, + private val prepareForSend: PrepareForSendUseCase, + @Assisted override val context: WcMethodUseCaseContext, + @Assisted private val method: WcSolanaMethod.SignAllTransaction, +) : BaseWcSignUseCase>(), + WcSolanaSignAllTransactionUseCase { + + override suspend fun SignCollector>.onSign( + state: WcSignState>, + ) { + val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) + .onLeft { error -> + emit(state.toResult(error.left())) + } + .getOrNull() + ?: return + val respond = getSolanaResultTxHashesString(hash) + val respondResult = respondService.respond(rawSdkRequest, respond) + emit(state.toResult(respondResult)) + } + + override fun invoke(): Flow>> { + val transactionData = method.transaction + .map { it.decodeBase64()?.toByteArray() ?: ByteArray(0) } + .map { TransactionData.Compiled(value = TransactionData.Compiled.Data.Bytes(it)) } + + return delegate.invoke(transactionData) + } + + /** + * Build json object + * { + * "transactions": [ + * "signed_tx_hash" + * ] + * } + */ + private fun getSolanaResultTxHashesString(signedHashes: List): String { + val result = JSONObject() + val transactions = JSONArray() + signedHashes.forEach { + transactions.put(it.encodeBase64()) + } + result.put("transactions", transactions) + return result.toString() + } + + @AssistedFactory + interface Factory { + fun create( + context: WcMethodUseCaseContext, + method: WcSolanaMethod.SignAllTransaction, + ): DefaultWcSolanaSignAllTransactionUseCase + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt new file mode 100644 index 0000000000..4a497421ff --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt @@ -0,0 +1,57 @@ +package com.tangem.data.walletconnect.network.solana + +import arrow.core.left +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.extensions.encodeBase58 +import com.tangem.data.walletconnect.respond.WcRespondService +import com.tangem.data.walletconnect.sign.BaseWcSignUseCase +import com.tangem.data.walletconnect.sign.SignCollector +import com.tangem.data.walletconnect.sign.SignStateConverter.toResult +import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.domain.transaction.usecase.PrepareForSendUseCase +import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.usecase.sign.WcSignState +import com.tangem.domain.walletconnect.usecase.solana.WcSolanaSignTransactionUseCase +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import okio.ByteString.Companion.decodeBase64 + +internal class DefaultWcSolanaSignTransactionUseCase @AssistedInject constructor( + override val respondService: WcRespondService, + private val prepareForSend: PrepareForSendUseCase, + @Assisted override val context: WcMethodUseCaseContext, + @Assisted private val method: WcSolanaMethod.SignTransaction, +) : BaseWcSignUseCase(), + WcSolanaSignTransactionUseCase { + + override suspend fun SignCollector.onSign(state: WcSignState) { + val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) + .onLeft { error -> + emit(state.toResult(error.left())) + } + .getOrNull() + ?: return + val respond = "{ signature: \"${hash.encodeBase58()}\" }" + val respondResult = respondService.respond(rawSdkRequest, respond) + emit(state.toResult(respondResult)) + } + + override fun invoke(): Flow> { + val data = method.transaction.decodeBase64()?.toByteArray() ?: ByteArray(0) + + val transactionData = TransactionData.Compiled( + value = TransactionData.Compiled.Data.Bytes(data), + ) + return delegate.invoke(transactionData) + } + + @AssistedFactory + interface Factory { + fun create( + context: WcMethodUseCaseContext, + method: WcSolanaMethod.SignTransaction, + ): DefaultWcSolanaSignTransactionUseCase + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt new file mode 100644 index 0000000000..fe252ca79c --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/Model.kt @@ -0,0 +1,19 @@ +package com.tangem.data.walletconnect.network.solana + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +internal data class WcSolanaSignMessageRequest( + @Json(name = "pubkey") + val publicKey: String, + + @Json(name = "message") + val message: String, +) + +@JsonClass(generateAdapter = true) +internal data class WcSolanaSignTransactionRequest( + @Json(name = "transaction") + val transaction: String, +) \ No newline at end of file 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 d4c72c3ee2..d376a58417 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,19 +1,45 @@ package com.tangem.data.walletconnect.network.solana +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.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.WcSolanaMethod +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 WcSolanaNetwork constructor( + private val moshi: Moshi, + private val sessionsManager: WcSessionsManager, + private val factories: Factories, private val excludedBlockchains: ExcludedBlockchains, -) : WcNamespaceConverter { +) : WcNamespaceConverter, WcRequestToUseCaseConverter { override val namespaceKey: NamespaceKey = NamespaceKey("solana") + override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { + val methodKey = request.request.method + val name = Name.entries.find { it.raw == methodKey } ?: return null + val method: WcSolanaMethod = 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 WcSolanaMethod.SignMessage -> TODO() + is WcSolanaMethod.SignTransaction -> factories.signTransaction.create(context, method) + is WcSolanaMethod.SignAllTransaction -> factories.signAllTransaction.create(context, method) + } + } + override fun toBlockchain(chainId: CAIP2): Blockchain? { if (chainId.namespace != namespaceKey.key) return null return when (chainId.reference) { @@ -41,6 +67,32 @@ internal class WcSolanaNetwork constructor( ) } + private fun Name.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? { + val rawParams = request.request.params + return when (this) { + Name.SignMessage -> moshi.fromJson(rawParams)?.let { request -> + WcSolanaMethod.SignMessage(pubKey = request.publicKey, message = request.message) + } + Name.SignTransaction -> moshi.fromJson(rawParams)?.let { request -> + WcSolanaMethod.SignTransaction(request.transaction) + } + Name.SendAllTransaction -> moshi.fromJson>(rawParams)?.let { list -> + WcSolanaMethod.SignAllTransaction(list) + } + } + } + + private enum class Name(val raw: String) { + SignMessage("solana_signMessage"), + SignTransaction("solana_signTransaction"), + SendAllTransaction("solana_signAllTransactions"), + } + + internal class Factories @Inject constructor( + val signTransaction: DefaultWcSolanaSignTransactionUseCase.Factory, + val signAllTransaction: DefaultWcSolanaSignAllTransactionUseCase.Factory, + ) + companion object { private const val MAINNET_CHAIN_ID = "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" private const val TESTNET_CHAIN_ID = "4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z" 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 9ef573a666..30ab51adf6 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 @@ -89,4 +89,11 @@ interface TransactionRepository { userWalletId: UserWalletId, network: Network, ): Result + + suspend fun prepareForSendMultiple( + transactionData: List, + 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 index 58d073be30..b2e29146b2 100644 --- 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 @@ -4,6 +4,7 @@ import arrow.core.Either import arrow.core.left import arrow.core.right import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.TransactionSigner import com.tangem.domain.card.models.TwinKey import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.TapWorkarounds.isTangemTwins @@ -20,13 +21,7 @@ class PrepareForSendUseCase( 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 signer = createSigner(userWallet) return transactionRepository.prepareForSend( transactionData = transactionData, userWalletId = userWallet.walletId, @@ -35,4 +30,29 @@ class PrepareForSendUseCase( ) .fold(onSuccess = { it.right() }, onFailure = { it.left() }) } + suspend operator fun invoke( + transactionData: List, + userWallet: UserWallet, + network: Network, + ): Either> { + val signer = createSigner(userWallet) + return transactionRepository.prepareForSendMultiple( + transactionData = transactionData, + userWalletId = userWallet.walletId, + network = network, + signer = signer, + ) + .fold(onSuccess = { it.right() }, onFailure = { it.left() }) + } + + private fun createSigner(userWallet: UserWallet): TransactionSigner { + 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 signer + } } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt new file mode 100644 index 0000000000..3bc1f2dfbc --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSolanaMethod.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.walletconnect.model + +sealed interface WcSolanaMethod : WcMethod { + + data class SignMessage( + val pubKey: String, + val message: String, + ) : WcSolanaMethod + + data class SignTransaction( + val transaction: String, + ) : WcSolanaMethod + + data class SignAllTransaction( + val transaction: List, + ) : WcSolanaMethod +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt new file mode 100644 index 0000000000..92eb17320f --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt @@ -0,0 +1,35 @@ +package com.tangem.domain.walletconnect.usecase.solana + +import com.tangem.blockchain.common.TransactionData +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.sign.WcSignState +import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase +import kotlinx.coroutines.flow.Flow + +interface WcSolanaSignTransactionUseCase : + WcSignUseCase, + WcSignUseCase.SimpleRun { + + override val network: Network + override val rawSdkRequest: WcSdkSessionRequest + override val session: WcSession + + override fun sign() + override fun cancel() + override fun invoke(): Flow> +} + +interface WcSolanaSignAllTransactionUseCase : + WcSignUseCase, + WcSignUseCase.SimpleRun> { + + override val network: Network + override val rawSdkRequest: WcSdkSessionRequest + override val session: WcSession + + override fun sign() + override fun cancel() + override fun invoke(): Flow>> +} \ No newline at end of file From a3a18854c8a37abb35115df5d7a462c65c05d705 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 13:28:11 +0500 Subject: [PATCH 089/151] Updated on 2026-08-14 --- .../auth/DefaultExpressAuthProvider.kt | 56 +--- .../tangem/tap/network/auth/di/AuthModule.kt | 12 +- .../datasource/api/common/config/Express.kt | 2 - .../api/express/TangemExpressApi.kt | 38 ++- .../tangem/datasource/api/onramp/OnrampApi.kt | 43 ++- .../swap/DefaultExpressServiceLoader.kt | 16 +- .../swap/ExpressServiceLoader.kt | 5 +- .../exchangeservice/swap/ExpressUtils.kt | 46 +++ .../managers/ProdApiConfigsManagerTest.kt | 4 - .../data/onramp/DefaultOnrampRepository.kt | 265 +++++++++++------- .../repository/DefaultCurrenciesRepository.kt | 8 +- .../onramp/CheckOnrampAvailabilityUseCase.kt | 16 +- .../onramp/FetchOnrampCountriesUseCase.kt | 5 +- .../onramp/FetchOnrampCurrenciesUseCase.kt | 5 +- .../domain/onramp/GetOnrampCountryUseCase.kt | 5 +- .../onramp/GetOnrampRedirectUrlUseCase.kt | 6 +- .../domain/onramp/GetOnrampStatusUseCase.kt | 5 +- .../domain/onramp/OnrampFetchPairsUseCase.kt | 10 +- .../domain/onramp/OnrampFetchQuotesUseCase.kt | 4 +- .../onramp/repositories/OnrampRepository.kt | 24 +- .../ConfirmResidencyComponent.kt | 2 + .../DefaultConfirmResidencyComponent.kt | 1 + .../onramp/main/DefaultOnrampMainComponent.kt | 2 + .../main/model/OnrampMainComponentModel.kt | 11 +- .../redirect/model/OnrampRedirectModel.kt | 6 +- .../onramp/root/DefaultOnrampComponent.kt | 5 +- .../selectcountry/SelectCountryComponent.kt | 7 +- .../model/OnrampSelectCountryModel.kt | 9 +- .../selectcurrency/SelectCurrencyComponent.kt | 7 +- .../model/OnrampSelectCurrencyModel.kt | 2 +- .../DefaultOnrampSettingsComponent.kt | 3 +- .../settings/OnrampSettingsComponent.kt | 7 +- .../model/OnrampSuccessComponentModel.kt | 2 +- .../model/AvailableSwapPairsModel.kt | 4 + .../feature/swap/DefaultSwapRepository.kt | 62 +++- .../swap/DefaultSwapTransactionRepository.kt | 9 +- .../tangem/feature/swap/di/SwapDataModule.kt | 2 + .../feature/swap/domain/api/SwapRepository.kt | 19 +- .../swap/domain/GetAvailablePairsUseCase.kt | 8 +- .../feature/swap/domain/SwapInteractorImpl.kt | 16 +- .../swap/domain/SwapTransactionRepository.kt | 5 +- .../tokendetails/model/TokenDetailsModel.kt | 6 +- .../factory/express/ExchangeStatusFactory.kt | 22 +- .../factory/express/ExpressStatusFactory.kt | 12 +- .../factory/express/OnrampStatusFactory.kt | 10 +- .../wallet/child/wallet/model/WalletModel.kt | 44 +-- .../wallet/domain/OnrampStatusFactory.kt | 9 +- .../tangem/lib/auth/ExpressAuthProvider.kt | 5 - 48 files changed, 544 insertions(+), 328 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressUtils.kt diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt index 5087ef7019..15e74c5498 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt @@ -1,68 +1,14 @@ package com.tangem.tap.network.auth -import com.tangem.common.CardIdRangeDec -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.datasource.local.preferences.utils.getSyncOrDefault -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.wallets.models.UserWallet import com.tangem.lib.auth.ExpressAuthProvider -import kotlinx.coroutines.runBlocking import java.util.UUID import java.util.concurrent.atomic.AtomicReference -internal class DefaultExpressAuthProvider( - private val userWalletsStore: UserWalletsStore, - private val appPreferencesStore: AppPreferencesStore, -) : ExpressAuthProvider { +internal class DefaultExpressAuthProvider : ExpressAuthProvider { private var uuid = AtomicReference(UUID.randomUUID()) - override fun getUserId(): String { - return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: error("No user id provided") - } - override fun getSessionId(): String { return uuid.get().toString() } - - override fun getRefCode(): String { - val selectedUserWallet = userWalletsStore.selectedUserWalletOrNull ?: error("Can not get selected user wallet") - - return when { - isRing(selectedUserWallet) -> "ring" - isChangeNow(selectedUserWallet) -> "ChangeNow" - isPartner(selectedUserWallet) -> "partner" - else -> "" - } - } - - private fun isRing(selectedUserWallet: UserWallet): Boolean { - val addedWalletsWithRings = runBlocking { - appPreferencesStore.getSyncOrDefault( - key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY, - default = emptySet(), - ) - } - - return addedWalletsWithRings.contains(selectedUserWallet.walletId.stringValue) - } - - private fun isChangeNow(selectedUserWallet: UserWallet): Boolean { - val changeNowRange = CardIdRangeDec( - start = "AF99001800554008", - end = "AF99001800559994", - ) - val card = selectedUserWallet.scanResponse.card - return card.batchId == BATCH_ID_CHANGENOW || changeNowRange?.contains(card.cardId) == true - } - - private fun isPartner(selectedUserWallet: UserWallet): Boolean { - return selectedUserWallet.scanResponse.card.batchId == BATCH_ID_PARTNER - } - - private companion object { - const val BATCH_ID_CHANGENOW = "BB000013" - const val BATCH_ID_PARTNER = "AF990015" - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index c72a9c145c..5f724624af 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -2,8 +2,6 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -30,14 +28,8 @@ internal class AuthModule { @Provides @Singleton - fun provideExpressAuthProvider( - userWalletsStore: UserWalletsStore, - appPreferencesStore: AppPreferencesStore, - ): ExpressAuthProvider { - return DefaultExpressAuthProvider( - userWalletsStore = userWalletsStore, - appPreferencesStore = appPreferencesStore, - ) + fun provideExpressAuthProvider(): ExpressAuthProvider { + return DefaultExpressAuthProvider() } @Provides 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 3babcef973..db32d40409 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 @@ -48,10 +48,8 @@ internal class Express( private fun createHeaders(isProd: Boolean) = buildMap { put(key = "api-key", value = ProviderSuspend { getApiKey(isProd) }) - put(key = "user-id", value = ProviderSuspend(expressAuthProvider::getUserId)) put(key = "session-id", value = ProviderSuspend(expressAuthProvider::getSessionId)) putAll(from = RequestHeader.AppVersionPlatformHeaders(appVersionProvider).values) - put(key = "refcode", value = ProviderSuspend(expressAuthProvider::getRefCode)) } private fun getApiKey(isProd: Boolean): String { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index 54a7935d66..469ab8a516 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -5,10 +5,7 @@ import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.ExchangeSentRequestBody import com.tangem.datasource.api.express.models.request.PairsRequestBody import com.tangem.datasource.api.express.models.response.* -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Query +import retrofit2.http.* /** * Interface of Tangem Express API (new swap mechanism) @@ -17,16 +14,29 @@ import retrofit2.http.Query interface TangemExpressApi { @POST("assets") - suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse> + suspend fun getAssets( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + @Body body: AssetsRequestBody, + ): ApiResponse> @POST("pairs") - suspend fun getPairs(@Body body: PairsRequestBody): ApiResponse> + suspend fun getPairs( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + @Body body: PairsRequestBody, + ): ApiResponse> @GET("providers") - suspend fun getProviders(): ApiResponse> + suspend fun getProviders( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + ): ApiResponse> @GET("exchange-quote") suspend fun getExchangeQuote( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, @Query("fromContractAddress") fromContractAddress: String, @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, @@ -40,6 +50,8 @@ interface TangemExpressApi { @GET("exchange-data") suspend fun getExchangeData( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, @Query("fromContractAddress") fromContractAddress: String, @Query("fromNetwork") fromNetwork: String, @Query("toContractAddress") toContractAddress: String, @@ -57,8 +69,16 @@ interface TangemExpressApi { ): ApiResponse @GET("exchange-status") - suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse + suspend fun getExchangeStatus( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + @Query("txId") txId: String, + ): ApiResponse @POST("exchange-sent") - suspend fun exchangeSent(@Body body: ExchangeSentRequestBody): ApiResponse + suspend fun exchangeSent( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + @Body body: ExchangeSentRequestBody, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt index 2a8034b8b1..433cf8b3eb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/OnrampApi.kt @@ -3,37 +3,52 @@ package com.tangem.datasource.api.onramp import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.onramp.models.request.OnrampPairsRequest import com.tangem.datasource.api.onramp.models.response.OnrampDataResponse -import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO import com.tangem.datasource.api.onramp.models.response.OnrampQuoteResponse import com.tangem.datasource.api.onramp.models.response.OnrampStatusResponse import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampCurrencyDTO +import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO -import retrofit2.http.Body -import retrofit2.http.GET -import retrofit2.http.POST -import retrofit2.http.Query +import retrofit2.http.* @Suppress("LongParameterList", "LargeClass", "TooManyFunctions") interface OnrampApi { @GET("currencies") - suspend fun getCurrencies(): ApiResponse> + suspend fun getCurrencies( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + ): ApiResponse> @GET("countries") - suspend fun getCountries(): ApiResponse> + suspend fun getCountries( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + ): ApiResponse> @GET("country-by-ip") - suspend fun getCountryByIp(): ApiResponse + suspend fun getCountryByIp( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + ): ApiResponse @GET("payment-methods") - suspend fun getPaymentMethods(): ApiResponse> + suspend fun getPaymentMethods( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + ): ApiResponse> @POST("onramp-pairs") - suspend fun getPairs(@Body body: OnrampPairsRequest): ApiResponse> + suspend fun getPairs( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + @Body body: OnrampPairsRequest, + ): ApiResponse> @GET("onramp-quote") suspend fun getQuote( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, @Query("fromCurrencyCode") fromCurrencyCode: String, @Query("fromPrecision") fromPrecision: Int, @Query("toContractAddress") toContractAddress: String, @@ -47,6 +62,8 @@ interface OnrampApi { @GET("onramp-data") suspend fun getData( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, @Query("fromCurrencyCode") fromCurrencyCode: String, @Query("fromPrecision") fromPrecision: Int, @Query("toContractAddress") toContractAddress: String, @@ -64,5 +81,9 @@ interface OnrampApi { ): ApiResponse @GET("onramp-status") - suspend fun getStatus(@Query("txId") txId: String): ApiResponse + suspend fun getStatus( + @Header("user-id") userWalletId: String, + @Header("refcode") refCode: String, + @Query("txId") txId: String, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultExpressServiceLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultExpressServiceLoader.kt index b966a12e0e..c89a7f0d1d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultExpressServiceLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/DefaultExpressServiceLoader.kt @@ -5,11 +5,14 @@ import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.AssetsRequestBody import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.express.models.response.Asset +import com.tangem.datasource.exchangeservice.swap.ExpressUtils.getRefCode +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.ExpressAssetsStore import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow @@ -33,31 +36,34 @@ typealias InitializationStatusFlow = MutableStateFlow internal class DefaultExpressServiceLoader @Inject constructor( private val tangemExpressApi: TangemExpressApi, private val expressAssetsStore: ExpressAssetsStore, + private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, ) : ExpressServiceLoader { private val initializationStatuses = MutableStateFlow>(value = emptyMap()) - override suspend fun update(userWalletId: UserWalletId, userTokens: List) { + override suspend fun update(userWallet: UserWallet, userTokens: List) { withContext(dispatchers.io) { - val initializationStatus = getInitializationStatusInternal(userWalletId) + val initializationStatus = getInitializationStatusInternal(userWallet.walletId) try { if (userTokens.isNotEmpty()) { val response = tangemExpressApi.getAssets( + userWalletId = userWallet.walletId.stringValue, + refCode = getRefCode(userWallet, appPreferencesStore), body = AssetsRequestBody(tokensList = userTokens), ).getOrThrow() - expressAssetsStore.store(userWalletId, response) + expressAssetsStore.store(userWallet.walletId, response) initializationStatus.update { response.lceContent() } } } catch (e: Throwable) { - if (expressAssetsStore.getSyncOrNull(userWalletId) == null) { + if (expressAssetsStore.getSyncOrNull(userWallet.walletId) == null) { initializationStatus.update { e.lceError() } } - Timber.e(e, "Unable to fetch assets for: ${userWalletId.stringValue}") + Timber.e(e, "Unable to fetch assets for: ${userWallet.walletId.stringValue}") } } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressServiceLoader.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressServiceLoader.kt index f73ee82700..38d07ae3f1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressServiceLoader.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressServiceLoader.kt @@ -3,6 +3,7 @@ package com.tangem.datasource.exchangeservice.swap import com.tangem.datasource.api.express.models.request.LeastTokenInfo import com.tangem.datasource.api.express.models.response.Asset import com.tangem.domain.core.lce.Lce +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -13,8 +14,8 @@ import kotlinx.coroutines.flow.Flow */ interface ExpressServiceLoader { - /** Update service using [userWalletId] and [userTokens] */ - suspend fun update(userWalletId: UserWalletId, userTokens: List) + /** Update service using [userWallet] and [userTokens] */ + suspend fun update(userWallet: UserWallet, userTokens: List) /** Get initialization status by [userWalletId] */ fun getInitializationStatus(userWalletId: UserWalletId): Flow>> diff --git a/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressUtils.kt b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressUtils.kt new file mode 100644 index 0000000000..50d0877467 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/exchangeservice/swap/ExpressUtils.kt @@ -0,0 +1,46 @@ +package com.tangem.datasource.exchangeservice.swap + +import com.tangem.common.CardIdRangeDec +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.utils.getSyncOrDefault +import com.tangem.domain.wallets.models.UserWallet +import kotlinx.coroutines.runBlocking + +object ExpressUtils { + private const val BATCH_ID_CHANGENOW = "BB000013" + private const val BATCH_ID_PARTNER = "AF990015" + + fun getRefCode(userWallet: UserWallet, appPreferencesStore: AppPreferencesStore): String { + return when { + isRing(userWallet, appPreferencesStore) -> "ring" + isChangeNow(userWallet) -> "ChangeNow" + isPartner(userWallet) -> "partner" + else -> "" + } + } + + private fun isRing(selectedUserWallet: UserWallet, appPreferencesStore: AppPreferencesStore): Boolean { + val addedWalletsWithRings = runBlocking { + appPreferencesStore.getSyncOrDefault( + key = PreferencesKeys.ADDED_WALLETS_WITH_RING_KEY, + default = emptySet(), + ) + } + + return addedWalletsWithRings.contains(selectedUserWallet.walletId.stringValue) + } + + private fun isChangeNow(selectedUserWallet: UserWallet): Boolean { + val changeNowRange = CardIdRangeDec( + start = "AF99001800554008", + end = "AF99001800559994", + ) + val card = selectedUserWallet.scanResponse.card + return card.batchId == BATCH_ID_CHANGENOW || changeNowRange?.contains(card.cardId) == true + } + + private fun isPartner(selectedUserWallet: UserWallet): Boolean { + return selectedUserWallet.scanResponse.card.batchId == BATCH_ID_PARTNER + } +} \ No newline at end of file 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 6537f31453..2624ca1707 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 @@ -50,9 +50,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { @Before fun setup() { every { appVersionProvider.versionName } returns VERSION_NAME - every { expressAuthProvider.getUserId() } returns EXPRESS_USER_ID every { expressAuthProvider.getSessionId() } returns EXPRESS_SESSION_ID - every { expressAuthProvider.getRefCode() } returns EXPRESS_REF_CODE every { stakeKitAuthProvider.getApiKey() } returns STAKE_KIT_API_KEY every { appAuthProvider.getCardId() } returns APP_CARD_ID every { appAuthProvider.getCardPublicKey() } returns APP_CARD_PUBLIC_KEY @@ -131,9 +129,7 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY } }, - "user-id" to ProviderSuspend { EXPRESS_USER_ID }, "session-id" to ProviderSuspend { EXPRESS_SESSION_ID }, - "refcode" to ProviderSuspend { EXPRESS_REF_CODE }, "version" to ProviderSuspend { VERSION_NAME }, "platform" to ProviderSuspend { "android" }, "language" to ProviderSuspend { Locale.getDefault().language.checkHeaderValueOrEmpty() }, diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index 578611fb04..479cb8c590 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -24,6 +24,7 @@ import com.tangem.datasource.api.onramp.models.response.model.OnrampCountryDTO import com.tangem.datasource.api.onramp.models.response.model.OnrampPairDTO import com.tangem.datasource.api.onramp.models.response.model.PaymentMethodDTO import com.tangem.datasource.crypto.DataSignatureVerifier +import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore @@ -43,6 +44,7 @@ import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.NonCancellable @@ -82,10 +84,16 @@ internal class DefaultOnrampRepository( override fun getCurrencies(): Flow> = currenciesStore.get(CURRENCIES_KEY) - override suspend fun fetchCurrencies() = withContext(dispatchers.io) { + override suspend fun fetchCurrencies(userWallet: UserWallet) = withContext(dispatchers.io) { if (!currenciesStore.getSyncOrNull(CURRENCIES_KEY).isNullOrEmpty()) return@withContext - val result = onrampApi.getCurrencies() + val result = onrampApi.getCurrencies( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ) .getOrThrow() .map(currencyConverter::convert) @@ -98,10 +106,16 @@ internal class DefaultOnrampRepository( return countriesStore.getSyncOrNull(COUNTRIES_KEY) } - override suspend fun fetchCountries(): List = withContext(dispatchers.io) { + override suspend fun fetchCountries(userWallet: UserWallet): List = withContext(dispatchers.io) { if (!countriesStore.getSyncOrNull(COUNTRIES_KEY).isNullOrEmpty()) return@withContext emptyList() - val result = onrampApi.getCountries() + val result = onrampApi.getCountries( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ) .getOrThrow() .map(countryConverter::convert) @@ -110,14 +124,27 @@ internal class DefaultOnrampRepository( result } - override suspend fun getCountryByIp(): OnrampCountry = withContext(dispatchers.io) { - onrampApi.getCountryByIp() + override suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry = withContext(dispatchers.io) { + onrampApi.getCountryByIp( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ) .getOrThrow() .let(countryConverter::convert) } - override suspend fun getStatus(txId: String): OnrampStatus = withContext(dispatchers.io) { - onrampApi.getStatus(txId) + override suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus = withContext(dispatchers.io) { + onrampApi.getStatus( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + txId = txId, + ) .getOrThrow() .let(statusConverter::convert) } @@ -161,11 +188,19 @@ internal class DefaultOnrampRepository( .map { it?.let(countryConverter::convert) } } - override suspend fun fetchPaymentMethodsIfAbsent() = withContext(dispatchers.io) { + override suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet) = withContext(dispatchers.io) { if (paymentMethodsStore.contains(PAYMENT_METHODS_KEY)) return@withContext val response = safeApiCall( - call = { onrampApi.getPaymentMethods().bind() }, + call = { + onrampApi.getPaymentMethods( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ).bind() + }, onError = { Timber.w(it, "Unable to fetch onramp payment methods") throw it @@ -174,100 +209,123 @@ internal class DefaultOnrampRepository( paymentMethodsStore.store(PAYMENT_METHODS_KEY, response.removeApplePay()) } - override suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency) = - withContext(dispatchers.io) { - val onrampPairs = async { - safeApiCall( - call = { - onrampApi.getPairs( - body = OnrampPairsRequest( - fromCurrencyCode = currency.code, - countryCode = country.code, - to = listOf( - OnrampDestinationDTO( - contractAddress = cryptoCurrency.getContractAddress(), - network = cryptoCurrency.network.backendId, - ), + override suspend fun fetchPairs( + userWallet: UserWallet, + currency: OnrampCurrency, + country: OnrampCountry, + cryptoCurrency: CryptoCurrency, + ) = withContext(dispatchers.io) { + val onrampPairs = async { + safeApiCall( + call = { + onrampApi.getPairs( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = OnrampPairsRequest( + fromCurrencyCode = currency.code, + countryCode = country.code, + to = listOf( + OnrampDestinationDTO( + contractAddress = cryptoCurrency.getContractAddress(), + network = cryptoCurrency.network.backendId, ), ), - ).bind() - }, - onError = { - Timber.w(it, "Unable to fetch onramp pairs") - throw it - }, - ) - } - val providers = async { - safeApiCall( - call = { expressApi.getProviders().bind() }, - onError = { - Timber.w(it, "Unable to fetch express providers") - throw it - }, - ) - } - storeOnrampPairs(pairs = onrampPairs.await(), providers = providers.await()) + ), + ).bind() + }, + onError = { + Timber.w(it, "Unable to fetch onramp pairs") + throw it + }, + ) } + val providers = async { + safeApiCall( + call = { + expressApi.getProviders( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ).bind() + }, + onError = { + Timber.w(it, "Unable to fetch express providers") + throw it + }, + ) + } + storeOnrampPairs(pairs = onrampPairs.await(), providers = providers.await()) + } - override suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount) = withContext(dispatchers.io) { - val pairs = requireNotNull(pairsStore.getSyncOrNull(PAIRS_KEY)) { - "Unable to get pairs. At this point they must not be null." - } - val amountValue = requireNotNull(amount.value) { "Amount value must not be null" } - val fromAmount = amountValue.movePointRight(amount.decimals).toString() - val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" } - val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" } - val fromOnrampAmount = OnrampAmount( - value = fromAmount - .toBigDecimalOrDefault() - .movePointLeft(currency.precision), - decimals = currency.precision, - symbol = amount.currencySymbol, - ) - val quotes: List = pairs.flatMap { pair -> - pair.providers.flatMap { provider -> - provider.paymentMethods.map { paymentMethod -> - async { - safeApiCall( - call = { - val response = onrampApi.getQuote( - fromCurrencyCode = currency.code, - fromPrecision = currency.precision, - toContractAddress = cryptoCurrency.getContractAddress(), - toNetwork = cryptoCurrency.network.backendId, - paymentMethod = paymentMethod.id, - countryCode = country.code, - fromAmount = fromAmount, - toDecimals = cryptoCurrency.decimals, - providerId = provider.id, - ).bind() - OnrampQuote.Data( - fromAmount = fromOnrampAmount, - toAmount = convertToAmount(response.toAmount, cryptoCurrency), - minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency), - maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency), - paymentMethod = paymentMethod, - provider = provider, - countryCode = response.countryCode, - ) - }, - onError = { error -> - convertQuoteError( - error = error, - paymentMethod = paymentMethod, - provider = provider, - fromOnrampAmount = fromOnrampAmount, - countryCode = country.code, - ) - }, - ) + override suspend fun fetchQuotes(userWallet: UserWallet, cryptoCurrency: CryptoCurrency, amount: Amount) = + withContext(dispatchers.io) { + val pairs = requireNotNull(pairsStore.getSyncOrNull(PAIRS_KEY)) { + "Unable to get pairs. At this point they must not be null." + } + val amountValue = requireNotNull(amount.value) { "Amount value must not be null" } + val fromAmount = amountValue.movePointRight(amount.decimals).toString() + val currency = requireNotNull(getDefaultCurrencySync()) { "Default currency must not be null" } + val country = requireNotNull(getDefaultCountrySync()) { "Default country must not be null" } + val fromOnrampAmount = OnrampAmount( + value = fromAmount + .toBigDecimalOrDefault() + .movePointLeft(currency.precision), + decimals = currency.precision, + symbol = amount.currencySymbol, + ) + val quotes: List = pairs.flatMap { pair -> + pair.providers.flatMap { provider -> + provider.paymentMethods.map { paymentMethod -> + async { + safeApiCall( + call = { + val response = onrampApi.getQuote( + fromCurrencyCode = currency.code, + fromPrecision = currency.precision, + toContractAddress = cryptoCurrency.getContractAddress(), + toNetwork = cryptoCurrency.network.backendId, + paymentMethod = paymentMethod.id, + countryCode = country.code, + fromAmount = fromAmount, + toDecimals = cryptoCurrency.decimals, + providerId = provider.id, + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ).bind() + OnrampQuote.Data( + fromAmount = fromOnrampAmount, + toAmount = convertToAmount(response.toAmount, cryptoCurrency), + minFromAmount = convertToAmount(response.minFromAmount, cryptoCurrency), + maxFromAmount = convertToAmount(response.maxFromAmount, cryptoCurrency), + paymentMethod = paymentMethod, + provider = provider, + countryCode = response.countryCode, + ) + }, + onError = { error -> + convertQuoteError( + error = error, + paymentMethod = paymentMethod, + provider = provider, + fromOnrampAmount = fromOnrampAmount, + countryCode = country.code, + ) + }, + ) + } } } - } - }.awaitAll().filterNotNull() - quotesStore.store(QUOTES_KEY, quotes) - } + }.awaitAll().filterNotNull() + quotesStore.store(QUOTES_KEY, quotes) + } override fun getQuotes(): Flow> { return quotesStore.get(QUOTES_KEY) @@ -278,14 +336,14 @@ internal class DefaultOnrampRepository( } override suspend fun getOnrampData( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrency: CryptoCurrency, quote: OnrampProviderWithQuote.Data, isDarkTheme: Boolean, ): OnrampTransaction = withContext(dispatchers.io) { try { val address = requireNotNull( - value = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network), + value = walletManagersFacade.getDefaultAddress(userWallet.walletId, cryptoCurrency.network), lazyMessage = { "Address must not be null" }, ) val fromAmountString = quote.fromAmount.value.movePointRight(quote.fromAmount.decimals).toString() @@ -309,6 +367,11 @@ internal class DefaultOnrampRepository( language = null, theme = getTheme(isDarkTheme), requestId = requestId, + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), ).bind() }, onError = { e -> @@ -326,7 +389,7 @@ internal class DefaultOnrampRepository( txId = data.txId, quote = quote, onrampDataJson = dataJson, - userWalletId = userWalletId, + userWalletId = userWallet.walletId, currency = currency, cryptoCurrency = cryptoCurrency, residency = country.name, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 6e893fe151..9454dd4a87 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -618,7 +618,7 @@ internal class DefaultCurrenciesRepository( } coroutineScope { - launch { expressServiceLoader.update(userWalletId, tokens) } + launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) } } } @@ -639,7 +639,7 @@ internal class DefaultCurrenciesRepository( skipCache = refresh, block = { coroutineScope { - launch { expressServiceLoader.update(userWalletId, tokens) } + launch { expressServiceLoader.update(getUserWallet(userWalletId), tokens) } } }, ) @@ -677,13 +677,13 @@ internal class DefaultCurrenciesRepository( isSortedByBalance = false, ) - private suspend fun getUserWallet(userWalletId: UserWalletId): UserWallet { + private fun getUserWallet(userWalletId: UserWalletId): UserWallet { return requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { "Unable to find a user wallet with provided ID: $userWalletId" } } - private suspend fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) { + private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) { val userWallet = getUserWallet(userWalletId) ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt index 60cb522991..911ca71568 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/CheckOnrampAvailabilityUseCase.kt @@ -6,27 +6,31 @@ import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.wallets.models.UserWallet class CheckOnrampAvailabilityUseCase( private val repository: OnrampRepository, private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(): Either { + suspend operator fun invoke(userWallet: UserWallet): Either { return Either.catch { - repository.fetchPaymentMethodsIfAbsent() + repository.fetchPaymentMethodsIfAbsent(userWallet) val savedCountry = repository.getDefaultCountrySync() if (savedCountry != null) { - proceedWithSavedCountry(savedCountry = savedCountry) + proceedWithSavedCountry(userWallet = userWallet, savedCountry = savedCountry) } else { - val detectedCountry = repository.getCountryByIp() + val detectedCountry = repository.getCountryByIp(userWallet) OnrampAvailability.ConfirmResidency(detectedCountry) } }.mapLeft(errorResolver::resolve) } - private suspend fun proceedWithSavedCountry(savedCountry: OnrampCountry): OnrampAvailability { - val countries = repository.fetchCountries() + private suspend fun proceedWithSavedCountry( + userWallet: UserWallet, + savedCountry: OnrampCountry, + ): OnrampAvailability { + val countries = repository.fetchCountries(userWallet) val updatedCountry = countries.find { it.id == savedCountry.id } ?: savedCountry return if (updatedCountry.onrampAvailable) { val currency = repository.getDefaultCurrencySync() ?: run { diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCountriesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCountriesUseCase.kt index 1920178751..5c9cd186d9 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCountriesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCountriesUseCase.kt @@ -4,13 +4,14 @@ import arrow.core.Either import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.wallets.models.UserWallet class FetchOnrampCountriesUseCase( private val repository: OnrampRepository, private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(): Either { - return Either.catch { repository.fetchCountries() }.mapLeft(errorResolver::resolve) + suspend operator fun invoke(userWallet: UserWallet): Either { + return Either.catch { repository.fetchCountries(userWallet) }.mapLeft(errorResolver::resolve) } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCurrenciesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCurrenciesUseCase.kt index d1a15ca673..facbee7dab 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCurrenciesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/FetchOnrampCurrenciesUseCase.kt @@ -4,13 +4,14 @@ import arrow.core.Either import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.wallets.models.UserWallet class FetchOnrampCurrenciesUseCase( private val repository: OnrampRepository, private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(): Either { - return Either.catch { repository.fetchCurrencies() }.mapLeft(errorResolver::resolve) + suspend operator fun invoke(userWallet: UserWallet): Either { + return Either.catch { repository.fetchCurrencies(userWallet) }.mapLeft(errorResolver::resolve) } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountryUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountryUseCase.kt index 65c945e7b5..9802404640 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountryUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampCountryUseCase.kt @@ -7,6 +7,7 @@ import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.map @@ -24,8 +25,8 @@ class GetOnrampCountryUseCase( } } - suspend fun invokeSync(): Either { - return Either.catch { repository.getDefaultCountrySync() ?: repository.getCountryByIp() } + suspend fun invokeSync(userWallet: UserWallet): Either { + return Either.catch { repository.getDefaultCountrySync() ?: repository.getCountryByIp(userWallet) } .mapLeft(errorResolver::resolve) } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt index 55ba74a5bf..dae9cfdb2a 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampRedirectUrlUseCase.kt @@ -7,7 +7,7 @@ import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.onramp.repositories.OnrampTransactionRepository import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet class GetOnrampRedirectUrlUseCase( private val repository: OnrampRepository, @@ -16,14 +16,14 @@ class GetOnrampRedirectUrlUseCase( ) { suspend operator fun invoke( - userWalletId: UserWalletId, + userWallet: UserWallet, quote: OnrampProviderWithQuote.Data, cryptoCurrency: CryptoCurrency, isDarkTheme: Boolean, ): Either { return Either.catch { val transaction = repository.getOnrampData( - userWalletId = userWalletId, + userWallet = userWallet, cryptoCurrency = cryptoCurrency, quote = quote, isDarkTheme = isDarkTheme, diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt index 20effbbeeb..92afbf647c 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/GetOnrampStatusUseCase.kt @@ -5,15 +5,16 @@ import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository +import com.tangem.domain.wallets.models.UserWallet class GetOnrampStatusUseCase( private val onrampRepository: OnrampRepository, private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(txId: String): Either { + suspend operator fun invoke(userWallet: UserWallet, txId: String): Either { return Either.catch { - onrampRepository.getStatus(txId) + onrampRepository.getStatus(userWallet = userWallet, txId = txId) }.mapLeft(errorResolver::resolve) } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampFetchPairsUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampFetchPairsUseCase.kt index 1bbaf78853..2cc183e142 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampFetchPairsUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampFetchPairsUseCase.kt @@ -5,17 +5,23 @@ import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet class OnrampFetchPairsUseCase( private val repository: OnrampRepository, private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(cryptoCurrency: CryptoCurrency): Either { + suspend operator fun invoke(userWallet: UserWallet, cryptoCurrency: CryptoCurrency): Either { return Either.catch { val country = requireNotNull(repository.getDefaultCountrySync()) { "Country must not be null" } val currency = requireNotNull(repository.getDefaultCurrencySync()) { "Currency must not be null" } - repository.fetchPairs(currency = currency, country = country, cryptoCurrency = cryptoCurrency) + repository.fetchPairs( + userWallet = userWallet, + currency = currency, + country = country, + cryptoCurrency = cryptoCurrency, + ) }.mapLeft(errorResolver::resolve) } } \ No newline at end of file diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampFetchQuotesUseCase.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampFetchQuotesUseCase.kt index 1842f2a819..7135dbddb0 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampFetchQuotesUseCase.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/OnrampFetchQuotesUseCase.kt @@ -5,14 +5,16 @@ import com.tangem.domain.onramp.repositories.OnrampErrorResolver import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet class OnrampFetchQuotesUseCase( private val repository: OnrampRepository, private val errorResolver: OnrampErrorResolver, ) { - suspend operator fun invoke(amount: Amount, cryptoCurrency: CryptoCurrency) = Either.catch { + suspend operator fun invoke(userWallet: UserWallet, amount: Amount, cryptoCurrency: CryptoCurrency) = Either.catch { repository.fetchQuotes( + userWallet = userWallet, cryptoCurrency = cryptoCurrency, amount = amount, ) diff --git a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt index 302a563b12..4e1dc066ae 100644 --- a/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt +++ b/domain/onramp/src/main/java/com/tangem/domain/onramp/repositories/OnrampRepository.kt @@ -4,7 +4,7 @@ import com.tangem.domain.onramp.model.* import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") @@ -13,15 +13,21 @@ interface OnrampRepository { fun getCurrencies(): Flow> fun getCountries(): Flow> suspend fun getCountriesSync(): List? - suspend fun getCountryByIp(): OnrampCountry - suspend fun getStatus(txId: String): OnrampStatus - suspend fun fetchCurrencies() - suspend fun fetchCountries(): List - suspend fun fetchPaymentMethodsIfAbsent() - suspend fun fetchPairs(currency: OnrampCurrency, country: OnrampCountry, cryptoCurrency: CryptoCurrency) - suspend fun fetchQuotes(cryptoCurrency: CryptoCurrency, amount: Amount) + suspend fun getCountryByIp(userWallet: UserWallet): OnrampCountry + suspend fun getStatus(userWallet: UserWallet, txId: String): OnrampStatus + suspend fun fetchCurrencies(userWallet: UserWallet) + suspend fun fetchCountries(userWallet: UserWallet): List + suspend fun fetchPaymentMethodsIfAbsent(userWallet: UserWallet) + suspend fun fetchPairs( + userWallet: UserWallet, + currency: OnrampCurrency, + country: OnrampCountry, + cryptoCurrency: CryptoCurrency, + ) + + suspend fun fetchQuotes(userWallet: UserWallet, cryptoCurrency: CryptoCurrency, amount: Amount) suspend fun getOnrampData( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrency: CryptoCurrency, quote: OnrampProviderWithQuote.Data, isDarkTheme: Boolean, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt index 7dda243f37..f948424bfa 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/ConfirmResidencyComponent.kt @@ -4,10 +4,12 @@ import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId internal interface ConfirmResidencyComponent : ComposableBottomSheetComponent { data class Params( + val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency, val country: OnrampCountry, val onDismiss: () -> Unit, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt index 2ff2e6a708..053fa306c7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/confirmresidency/DefaultConfirmResidencyComponent.kt @@ -69,6 +69,7 @@ internal class DefaultConfirmResidencyComponent @AssistedInject constructor( is ConfirmResidencyBottomSheetConfig.SelectCountry -> selectCountryComponentFactory.create( context = childByContext(componentContext), params = SelectCountryComponent.Params( + userWalletId = params.userWalletId, cryptoCurrency = params.cryptoCurrency, onDismiss = { isCountrySelected -> // Dismiss country select sheet diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt index f6c03525c1..0f959bb2c2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -54,6 +54,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( is OnrampMainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( context = childByContext(componentContext), params = ConfirmResidencyComponent.Params( + userWalletId = params.userWalletId, cryptoCurrency = params.cryptoCurrency, country = config.country, onDismiss = { @@ -65,6 +66,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( is OnrampMainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create( context = childByContext(componentContext), params = SelectCurrencyComponent.Params( + userWallet = model.userWallet, cryptoCurrency = params.cryptoCurrency, onDismiss = model.bottomSheetNavigation::dismiss, ), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index a3afe0f26f..09feb75a0d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -57,6 +57,9 @@ internal class OnrampMainComponentModel @Inject constructor( ) : Model(), OnrampIntents { private val params: OnrampMainComponent.Params = paramsContainer.require() + + val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + private val stateFactory = OnrampStateFactory( currentStateProvider = Provider { _state.value }, cryptoCurrency = params.cryptoCurrency, @@ -68,7 +71,6 @@ internal class OnrampMainComponentModel @Inject constructor( onrampIntents = this, cryptoCurrency = params.cryptoCurrency, ) - private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } private val _state: MutableStateFlow = MutableStateFlow( value = stateFactory.getInitialState( currency = params.cryptoCurrency.name, @@ -107,7 +109,7 @@ internal class OnrampMainComponentModel @Inject constructor( private fun checkResidenceCountry() { modelScope.launch { - checkOnrampAvailabilityUseCase() + checkOnrampAvailabilityUseCase(userWallet) .onRight(::handleOnrampAvailability) .onLeft(::handleOnrampError) } @@ -158,7 +160,7 @@ internal class OnrampMainComponentModel @Inject constructor( if (!state?.amountBlockState?.amountFieldModel?.fiatValue.isNullOrEmpty()) { _state.update { amountStateFactory.getAmountSecondaryLoadingState() } } - fetchPairsUseCase.invoke(params.cryptoCurrency).fold( + fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( ifLeft = ::handleOnrampError, ifRight = { _state.update { amountStateFactory.getAmountSecondaryResetState() } }, ) @@ -184,6 +186,7 @@ internal class OnrampMainComponentModel @Inject constructor( val content = state.value as? OnrampMainComponentUM.Content ?: return@runCatching if (content.amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) return@runCatching fetchQuotesUseCase.invoke( + userWallet = userWallet, amount = content.amountBlockState.amountFieldModel.fiatAmount, cryptoCurrency = params.cryptoCurrency, ).onLeft(::handleOnrampError) @@ -216,7 +219,7 @@ internal class OnrampMainComponentModel @Inject constructor( } override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { - if (isDemoCardUseCase.invoke(selectedUserWallet.cardId)) { + if (isDemoCardUseCase.invoke(userWallet.cardId)) { showDemoWarning() } else { val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt index cc6426d771..7d8207b969 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/redirect/model/OnrampRedirectModel.kt @@ -14,6 +14,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.onramp.GetOnrampRedirectUrlUseCase import com.tangem.domain.onramp.model.error.OnrampError +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.redirect.OnrampRedirectComponent import com.tangem.features.onramp.redirect.entity.OnrampRedirectTopBarUM @@ -29,6 +30,7 @@ internal class OnrampRedirectModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val urlOpener: UrlOpener, private val getOnrampRedirectUrlUseCase: GetOnrampRedirectUrlUseCase, + private val getWalletsUseCase: GetWalletsUseCase, private val messageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, router: Router, @@ -36,6 +38,8 @@ internal class OnrampRedirectModel @Inject constructor( ) : Model() { private val params: OnrampRedirectComponent.Params = paramsContainer.require() + private val selectedUserWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + val state = OnrampRedirectUM( topBarConfig = OnrampRedirectTopBarUM( title = combinedReference( @@ -62,7 +66,7 @@ internal class OnrampRedirectModel @Inject constructor( fun getRedirectUrl(isDarkTheme: Boolean) { modelScope.launch { getOnrampRedirectUrlUseCase.invoke( - userWalletId = params.userWalletId, + userWallet = selectedUserWallet, quote = params.onrampProviderWithQuote, cryptoCurrency = params.cryptoCurrency, isDarkTheme = isDarkTheme, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt index 48804d46be..e08b4a4477 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -61,8 +61,9 @@ internal class DefaultOnrampComponent @AssistedInject constructor( OnrampChild.Settings -> settingsComponentFactory.create( context = childByContext(componentContext), params = OnrampSettingsComponent.Params( - params.cryptoCurrency, - navigation::pop, + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + onBack = navigation::pop, ), ) OnrampChild.Main -> onrampMainComponentFactory.create( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt index 8abf849546..869dc95993 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/SelectCountryComponent.kt @@ -3,10 +3,15 @@ package com.tangem.features.onramp.selectcountry import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId internal interface SelectCountryComponent : ComposableBottomSheetComponent { - data class Params(val cryptoCurrency: CryptoCurrency, val onDismiss: (Boolean) -> Unit) + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrency: CryptoCurrency, + val onDismiss: (Boolean) -> Unit, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt index 8d0835efff..a30217c087 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcountry/model/OnrampSelectCountryModel.kt @@ -13,6 +13,7 @@ import com.tangem.domain.onramp.GetOnrampCountryUseCase import com.tangem.domain.onramp.OnrampSaveDefaultCountryUseCase import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampCountry +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selectcountry.SelectCountryComponent import com.tangem.features.onramp.selectcountry.entity.CountryItemState @@ -44,11 +45,15 @@ internal class OnrampSelectCountryModel @Inject constructor( private val saveDefaultCountryUseCase: OnrampSaveDefaultCountryUseCase, private val getOnrampCountryUseCase: GetOnrampCountryUseCase, private val fetchOnrampCountriesUseCase: FetchOnrampCountriesUseCase, + getWalletsUseCase: GetWalletsUseCase, paramsContainer: ParamsContainer, ) : Model() { - val state: StateFlow get() = controller.state private val params: SelectCountryComponent.Params = paramsContainer.require() + private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + + val state: StateFlow get() = controller.state + private val controller = CountryListUMController( searchBarUM = createSearchBarUM(), loadingItems = loadingItems, @@ -103,7 +108,7 @@ internal class OnrampSelectCountryModel @Inject constructor( private fun updateCountriesList() { modelScope.launch { - fetchOnrampCountriesUseCase().onLeft { + fetchOnrampCountriesUseCase(userWallet).onLeft { controller.update(UpdateCountryItemsErrorTransformer(onRetry = ::onRetry)) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/SelectCurrencyComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/SelectCurrencyComponent.kt index f8283864dd..e06fdec8ac 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/SelectCurrencyComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/SelectCurrencyComponent.kt @@ -3,10 +3,15 @@ package com.tangem.features.onramp.selectcurrency import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet internal interface SelectCurrencyComponent : ComposableBottomSheetComponent { - data class Params(val cryptoCurrency: CryptoCurrency, val onDismiss: () -> Unit) + data class Params( + val userWallet: UserWallet, + val cryptoCurrency: CryptoCurrency, + val onDismiss: () -> Unit, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt index 1162910ed0..c7899de0de 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selectcurrency/model/OnrampSelectCurrencyModel.kt @@ -97,7 +97,7 @@ internal class OnrampSelectCurrencyModel @Inject constructor( private fun updateCurrenciesList() { modelScope.launch { - fetchOnrampCurrenciesUseCase().onLeft { + fetchOnrampCurrenciesUseCase(userWallet = params.userWallet).onLeft { controller.update(UpdateCurrencyItemsErrorTransformer(onRetry = ::onRetry)) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt index 941d543d82..2d61aa6571 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/DefaultOnrampSettingsComponent.kt @@ -52,7 +52,8 @@ internal class DefaultOnrampSettingsComponent @AssistedInject constructor( OnrampSettingsConfig.SelectCountry -> selectCountryComponentFactory.create( context = childByContext(componentContext), params = SelectCountryComponent.Params( - params.cryptoCurrency, + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, onDismiss = { model.bottomSheetNavigation.dismiss() }, ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/OnrampSettingsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/OnrampSettingsComponent.kt index 4b3b8d1455..c6fca3fb80 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/OnrampSettingsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/settings/OnrampSettingsComponent.kt @@ -3,10 +3,15 @@ package com.tangem.features.onramp.settings import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWalletId internal interface OnrampSettingsComponent : ComposableContentComponent { - data class Params(val cryptoCurrency: CryptoCurrency, val onBack: () -> Unit) + data class Params( + val userWalletId: UserWalletId, + val cryptoCurrency: CryptoCurrency, + val onBack: () -> Unit, + ) interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt index 4803e7f98e..fa74f49da7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/model/OnrampSuccessComponentModel.kt @@ -97,7 +97,7 @@ internal class OnrampSuccessComponentModel @Inject constructor( return } - getOnrampStatusUseCase(txId = transaction.txId) + getOnrampStatusUseCase(userWallet = userWallet, txId = transaction.txId) .fold( ifLeft = { error -> analyticsEventHandler.sendOnrampErrorEvent( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt index 09f41bc29d..ca5940bfa9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsModel.kt @@ -19,6 +19,7 @@ import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast @@ -52,11 +53,13 @@ internal class AvailableSwapPairsModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getAvailablePairsUseCase: GetAvailablePairsUseCase, + private val getWalletsUseCase: GetWalletsUseCase, ) : Model() { val state: StateFlow = tokenListUMController.state private var params: AvailableSwapPairsComponent.Params = paramsContainer.require() + private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } private val tokenListFlow = getTokenListUseCaseFlow() @@ -212,6 +215,7 @@ internal class AvailableSwapPairsModel @Inject constructor( availablePairsByNetworkFlow.update(networkInfo = networkInfo, state = lceLoading()) getAvailablePairsUseCase( + userWallet = userWallet, initialCurrency = networkInfo, currencies = statuses.map(CryptoCurrencyStatus::currency), ) 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 f872cd9c2c..ac037efc96 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 @@ -21,9 +21,12 @@ import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.crypto.DataSignatureVerifier +import com.tangem.datasource.exchangeservice.swap.ExpressUtils +import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository @@ -48,6 +51,7 @@ internal class DefaultSwapRepository( private val userWalletsListManager: UserWalletsListManager, private val errorsDataConverter: ErrorsDataConverter, private val dataSignatureVerifier: DataSignatureVerifier, + private val appPreferencesStore: AppPreferencesStore, moshi: Moshi, excludedBlockchains: ExcludedBlockchains, ) : SwapRepository { @@ -60,6 +64,7 @@ internal class DefaultSwapRepository( private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java) override suspend fun getPairs( + userWallet: UserWallet, initialCurrency: LeastTokenInfo, currencyList: List, ): PairsWithProviders { @@ -73,6 +78,7 @@ internal class DefaultSwapRepository( val pairsDeferred = async { getPairsInternal( + userWallet = userWallet, from = arrayListOf(initial), to = currenciesList, ) @@ -80,6 +86,7 @@ internal class DefaultSwapRepository( val reversedPairsDeferred = async { getPairsInternal( + userWallet = userWallet, from = currenciesList, to = arrayListOf(initial), ) @@ -90,7 +97,13 @@ internal class DefaultSwapRepository( val allPairs = pairs + reversedPairs - val providers = tangemExpressApi.getProviders().getOrThrow() + val providers = tangemExpressApi.getProviders( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + ).getOrThrow() return@withContext swapPairInfoConverter.convert( SwapPairsWithProviders( @@ -109,6 +122,7 @@ internal class DefaultSwapRepository( } override suspend fun getPairsOnly( + userWallet: UserWallet, initialCurrency: LeastTokenInfo, currencyList: List, ): PairsWithProviders { @@ -122,6 +136,7 @@ internal class DefaultSwapRepository( val pairsDeferred = async { getPairsInternal( + userWallet = userWallet, from = arrayListOf(initial), to = currenciesList, ) @@ -129,6 +144,7 @@ internal class DefaultSwapRepository( val reversedPairsDeferred = async { getPairsInternal( + userWallet = userWallet, from = currenciesList, to = arrayListOf(initial), ) @@ -156,25 +172,42 @@ internal class DefaultSwapRepository( } private suspend fun getPairsInternal( + userWallet: UserWallet, from: List, to: List, ): ApiResponse> { return tangemExpressApi.getPairs( - PairsRequestBody( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = PairsRequestBody( from = from, to = to, ), ) } - override suspend fun getExchangeStatus(txId: String): Either { + override suspend fun getExchangeStatus( + userWallet: UserWallet, + txId: String, + ): Either { return withContext(coroutineDispatcher.io) { either { catch( block = { exchangeStatusConverter.convert( tangemExpressApi - .getExchangeStatus(txId) + .getExchangeStatus( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + txId = txId, + ) .getOrThrow(), ) }, @@ -188,7 +221,7 @@ internal class DefaultSwapRepository( } override suspend fun findBestQuote( - userWalletId: UserWalletId, + userWallet: UserWallet, fromContractAddress: String, fromNetwork: String, toContractAddress: String, @@ -211,6 +244,11 @@ internal class DefaultSwapRepository( toDecimals = toDecimals, providerId = providerId, rateType = rateType.name.lowercase(), + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), ).getOrThrow() QuoteModel( toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), @@ -223,6 +261,7 @@ internal class DefaultSwapRepository( } override suspend fun getExchangeData( + userWallet: UserWallet, fromContractAddress: String, fromNetwork: String, toContractAddress: String, @@ -255,6 +294,11 @@ internal class DefaultSwapRepository( requestId = requestId, refundAddress = refundAddress, refundExtraId = refundExtraId, + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), ).getOrThrow() if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { val txDetails = parseTxDetails(response.txDetailsJson) @@ -281,6 +325,7 @@ internal class DefaultSwapRepository( } override suspend fun exchangeSent( + userWallet: UserWallet, txId: String, fromNetwork: String, fromAddress: String, @@ -290,7 +335,12 @@ internal class DefaultSwapRepository( ): Either = withContext(coroutineDispatcher.io) { try { tangemExpressApi.exchangeSent( - ExchangeSentRequestBody( + userWalletId = userWallet.walletId.stringValue, + refCode = ExpressUtils.getRefCode( + userWallet = userWallet, + appPreferencesStore = appPreferencesStore, + ), + body = ExchangeSentRequestBody( txId = txId, fromNetwork = fromNetwork, fromAddress = fromAddress, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index 082f1b8ec9..87e009b03a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -7,8 +7,8 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.datasource.local.preferences.utils.getObjectMapSync -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.SavedSwapTransactionListConverter import com.tangem.feature.swap.domain.SwapTransactionRepository @@ -80,9 +80,8 @@ internal class DefaultSwapTransactionRepository( } override suspend fun getTransactions( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrencyId: CryptoCurrency.ID, - scanResponse: ScanResponse, ): Flow?> { return withContext(dispatchers.io) { val txStatuses = appPreferencesStore.getObjectMapSync( @@ -93,7 +92,7 @@ internal class DefaultSwapTransactionRepository( ).map { savedTransactions -> val currencyTxs = savedTransactions ?.filter { - it.userWalletId == userWalletId.stringValue && + it.userWalletId == userWallet.walletId.stringValue && ( it.toCryptoCurrencyId == cryptoCurrencyId.value || it.fromCryptoCurrencyId == cryptoCurrencyId.value @@ -103,7 +102,7 @@ internal class DefaultSwapTransactionRepository( currencyTxs?.mapNotNull { converter.convertBack( value = it, - scanResponse = scanResponse, + scanResponse = userWallet.scanResponse, txStatuses = txStatuses, ) } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 5a1b3dcf85..eb0e016c58 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -36,6 +36,7 @@ internal class SwapDataModule { errorsDataConverter: ErrorsDataConverter, @NetworkMoshi moshi: Moshi, excludedBlockchains: ExcludedBlockchains, + appPreferencesStore: AppPreferencesStore, ): SwapRepository { return DefaultSwapRepository( tangemExpressApi = tangemExpressApi, @@ -46,6 +47,7 @@ internal class SwapDataModule { dataSignatureVerifier = dataSignature, moshi = moshi, excludedBlockchains = excludedBlockchains, + appPreferencesStore = appPreferencesStore, ) } 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 bacf1b8df5..69b15ea0b7 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 @@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain.api import arrow.core.Either import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.domain.* @@ -9,16 +10,24 @@ import java.math.BigDecimal interface SwapRepository { - suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List): PairsWithProviders + suspend fun getPairs( + userWallet: UserWallet, + initialCurrency: LeastTokenInfo, + currencyList: List, + ): PairsWithProviders /** Express getPairs request variant without providers request */ - suspend fun getPairsOnly(initialCurrency: LeastTokenInfo, currencyList: List): PairsWithProviders + suspend fun getPairsOnly( + userWallet: UserWallet, + initialCurrency: LeastTokenInfo, + currencyList: List, + ): PairsWithProviders - suspend fun getExchangeStatus(txId: String): Either + suspend fun getExchangeStatus(userWallet: UserWallet, txId: String): Either @Suppress("LongParameterList") suspend fun findBestQuote( - userWalletId: UserWalletId, + userWallet: UserWallet, fromContractAddress: String, fromNetwork: String, toContractAddress: String, @@ -43,6 +52,7 @@ interface SwapRepository { @Suppress("LongParameterList") suspend fun getExchangeData( + userWallet: UserWallet, fromContractAddress: String, fromNetwork: String, toContractAddress: String, @@ -61,6 +71,7 @@ interface SwapRepository { // TODO: Add target error handling, remove either ([REDACTED_JIRA]) @Suppress("LongParameterList") suspend fun exchangeSent( + userWallet: UserWallet, txId: String, fromNetwork: String, fromAddress: String, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt index 113c0ca915..28d0e06898 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/GetAvailablePairsUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast @@ -10,9 +11,14 @@ class GetAvailablePairsUseCase( ) { suspend operator fun invoke( + userWallet: UserWallet, initialCurrency: LeastTokenInfo, currencies: List, ): List { - return swapRepository.getPairsOnly(initialCurrency, currencies).pairs + return swapRepository.getPairsOnly( + userWallet = userWallet, + initialCurrency = initialCurrency, + currencyList = currencies, + ).pairs } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index d5b93c4276..34954d7dc3 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 @@ -114,6 +114,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } val pairsLeast = getPairs( + userWallet = userWallet, initialCurrency = LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", network = currency.network.backendId, @@ -197,10 +198,15 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private suspend fun getPairs( + userWallet: UserWallet, initialCurrency: LeastTokenInfo, currenciesList: List, ): PairsWithProviders { - return repository.getPairs(initialCurrency, currenciesList) + return repository.getPairs( + userWallet = userWallet, + initialCurrency = initialCurrency, + currencyList = currenciesList, + ) } override suspend fun givePermissionToSwap( @@ -308,7 +314,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( isBalanceWithoutFeeEnough: Boolean, ): Pair { val maybeQuotes = repository.findBestQuote( - userWalletId = userWalletId, + userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), @@ -619,6 +625,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( return result.fold( ifRight = { txHash -> repository.exchangeSent( + userWallet = userWallet, txId = swapData.transaction.txId, fromNetwork = currencyToSendStatus.currency.network.backendId, fromAddress = currencyToSendStatus.value.networkAddress?.defaultAddress?.value.orEmpty(), @@ -674,6 +681,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapProvider: SwapProvider, ): SwapTransactionState { val exchangeData = repository.getExchangeData( + userWallet = userWallet, fromContractAddress = currencyToSend.currency.getContractAddress(), fromNetwork = currencyToSend.currency.network.backendId, toContractAddress = currencyToGet.currency.getContractAddress(), @@ -726,6 +734,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ifLeft = { SwapTransactionState.Error.TransactionError(it) }, ifRight = { txHash -> repository.exchangeSent( + userWallet = userWallet, txId = exchangeDataCex.txId, fromNetwork = currencyToSend.currency.network.backendId, fromAddress = currencyToSend.value.networkAddress?.defaultAddress?.value.orEmpty(), @@ -1132,7 +1141,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } val quotes = repository.findBestQuote( - userWalletId = userWalletId, + userWallet = userWallet, fromContractAddress = fromToken.getContractAddress(), fromNetwork = fromToken.network.backendId, toContractAddress = toToken.getContractAddress(), @@ -1388,6 +1397,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( selectedFee: FeeType, ): SwapState { return repository.getExchangeData( + userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt index 812499d47f..d0d49d2adf 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.domain -import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel @@ -18,9 +18,8 @@ interface SwapTransactionRepository { ) suspend fun getTransactions( - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrencyId: CryptoCurrency.ID, - scanResponse: ScanResponse, ): Flow?> suspend fun removeTransaction( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index d70de8f364..86c1687693 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -6,6 +6,8 @@ import arrow.core.getOrElse import com.tangem.blockchain.common.address.AddressType import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter +import com.tangem.common.ui.bottomsheet.receive.AddressModel +import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -17,8 +19,6 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.common.ui.bottomsheet.receive.AddressModel -import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -166,7 +166,7 @@ internal class TokenDetailsModel @Inject constructor( appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, currentStateProvider = Provider { uiState.value }, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - userWalletId = userWalletId, + userWallet = userWallet, cryptoCurrency = cryptoCurrency, ) } 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 6890405a1d..1fbea94122 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 @@ -12,8 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.repository.QuotesRepository -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.* @@ -29,7 +28,6 @@ import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.map @Suppress("LongParameterList") @@ -39,14 +37,13 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private val quotesRepository: QuotesRepository, private val quotesRepositoryV2: QuotesRepositoryV2, private val tokensFeatureToggles: TokensFeatureToggles, - private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val swapTransactionStatusStore: SwapTransactionStatusStore, private val analyticsEventsHandler: AnalyticsEventHandler, @Assisted private val clickIntents: TokenDetailsClickIntents, @Assisted private val appCurrencyProvider: Provider, @Assisted private val currentStateProvider: Provider, - @Assisted private val userWalletId: UserWalletId, + @Assisted private val userWallet: UserWallet, @Assisted private val cryptoCurrency: CryptoCurrency, ) { @@ -60,14 +57,9 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } suspend operator fun invoke(): Flow> { - val selectedWallet = getSelectedWalletSyncUseCase().fold( - ifLeft = { return emptyFlow() }, - ifRight = { it }, - ) return swapTransactionRepository.getTransactions( - userWalletId = userWalletId, + userWallet = userWallet, cryptoCurrencyId = cryptoCurrency.id, - scanResponse = selectedWallet.scanResponse, ).conflate() .map { savedTransactions -> val quotes = savedTransactions @@ -91,7 +83,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( val shouldDispose = selectedTx.activeStatus?.isAutoDisposable == true || isForceDispose if (shouldDispose) { swapTransactionRepository.removeTransaction( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, fromCryptoCurrency = selectedTx.fromCryptoCurrency, toCryptoCurrency = selectedTx.toCryptoCurrency, txId = selectedTx.info.txId, @@ -117,7 +109,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } private suspend fun getExchangeStatus(txId: String, provider: SwapProvider): ExchangeStatusModel? { - return swapRepository.getExchangeStatus(txId) + return swapRepository.getExchangeStatus(userWallet = userWallet, txId = txId) .fold( ifLeft = { null }, ifRight = { statusModel -> @@ -157,7 +149,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( val refundContractAddress = status.refundContractAddress if (refundNetwork != null && refundContractAddress != null) { return addCryptoCurrenciesUseCase( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, contractAddress = refundContractAddress, networkId = refundNetwork, ).getOrNull() @@ -219,7 +211,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( clickIntents: TokenDetailsClickIntents, appCurrencyProvider: Provider, currentStateProvider: Provider, - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrency: CryptoCurrency, ): ExchangeStatusFactory } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt index 77c9dc1404..7d8902e653 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExpressStatusFactory.kt @@ -10,12 +10,12 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent import com.tangem.domain.tokens.model.analytics.TokenScreenAnalyticsEvent -import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.models.domain.ExchangeStatus +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.components.ExchangeStatusNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.express.ExchangeUM -import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted @@ -36,7 +36,7 @@ internal class ExpressStatusFactory @AssistedInject constructor( @Assisted private val clickIntents: TokenDetailsClickIntents, @Assisted private val cryptoCurrency: CryptoCurrency, @Assisted appCurrencyProvider: Provider, - @Assisted userWalletId: UserWalletId, + @Assisted userWallet: UserWallet, @Assisted cryptoCurrencyStatusProvider: Provider, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventsHandler: AnalyticsEventHandler, @@ -49,7 +49,7 @@ internal class ExpressStatusFactory @AssistedInject constructor( clickIntents = clickIntents, appCurrencyProvider = appCurrencyProvider, currentStateProvider = currentStateProvider, - userWalletId = userWalletId, + userWallet = userWallet, cryptoCurrency = cryptoCurrency, ) } @@ -61,7 +61,7 @@ internal class ExpressStatusFactory @AssistedInject constructor( appCurrencyProvider = appCurrencyProvider, clickIntents = clickIntents, cryptoCurrency = cryptoCurrency, - userWalletId = userWalletId, + userWallet = userWallet, ) } @@ -210,7 +210,7 @@ internal class ExpressStatusFactory @AssistedInject constructor( clickIntents: TokenDetailsClickIntents, appCurrencyProvider: Provider, currentStateProvider: Provider, - userWalletId: UserWalletId, + userWallet: UserWallet, cryptoCurrency: CryptoCurrency, cryptoCurrencyStatusProvider: Provider, ): ExpressStatusFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index bcf2f0b1b2..b790b61b5d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -14,8 +14,8 @@ import com.tangem.domain.onramp.model.OnrampStatus.Status.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsOnrampTransactionStateConverter import com.tangem.utils.Provider @@ -39,7 +39,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( @Assisted private val appCurrencyProvider: Provider, @Assisted private val clickIntents: TokenDetailsClickIntents, @Assisted private val cryptoCurrency: CryptoCurrency, - @Assisted private val userWalletId: UserWalletId, + @Assisted private val userWallet: UserWallet, ) { private val onrampTransactionStateConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -54,7 +54,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( operator fun invoke(): Flow> { return getOnrampTransactionsUseCase( - userWalletId = userWalletId, + userWalletId = userWallet.walletId, cryptoCurrencyId = cryptoCurrency.id, ).map { maybeTransaction -> maybeTransaction.fold( @@ -82,7 +82,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( return if (onrampTx.activeStatus.isTerminal) { onrampTx } else { - getOnrampStatusUseCase(onrampTx.info.txId).fold( + getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( ifLeft = { Timber.e("Couldn't update onramp status. $it") onrampTx @@ -150,7 +150,7 @@ internal class OnrampStatusFactory @AssistedInject constructor( appCurrencyProvider: Provider, clickIntents: TokenDetailsClickIntents, cryptoCurrency: CryptoCurrency, - userWalletId: UserWalletId, + userWallet: UserWallet, ): OnrampStatusFactory } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 5359f89098..270806b3a2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -15,6 +15,7 @@ import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase @@ -107,9 +108,7 @@ internal class WalletModel @Inject constructor( subscribeToUserWalletsUpdates() subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() - subscribeToScreenBackgroundState() subscribeOnPushNotificationsPermission() - subscribeOnExpressTransactionsUpdates() subscribeOnNFTUpdates() clickIntents.initialize(innerWalletRouter, modelScope) @@ -223,6 +222,8 @@ internal class WalletModel @Inject constructor( } walletDeepLinksHandler.registerForWallet(scope = modelScope, userWallet = selectedWallet) + subscribeOnExpressTransactionsUpdates(selectedWallet) + subscribeToScreenBackgroundState(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -231,7 +232,7 @@ internal class WalletModel @Inject constructor( // We need to update the current wallet quotes if the application was in the background for more than 10 seconds // and then returned to the foreground - private fun subscribeToScreenBackgroundState() { + private fun subscribeToScreenBackgroundState(userWallet: UserWallet) { screenLifecycleProvider.isBackgroundState .onEach { isBackground -> expressTxStatusTaskScheduler.cancelTask() @@ -241,30 +242,31 @@ internal class WalletModel @Inject constructor( isBackground -> needToRefreshTimer() needToRefreshWallet && !isBackground -> { triggerRefreshWalletQuotes() - subscribeOnExpressTransactionsUpdates() + subscribeOnExpressTransactionsUpdates(userWallet) } - !isBackground -> subscribeOnExpressTransactionsUpdates() + !isBackground -> subscribeOnExpressTransactionsUpdates(userWallet) } } .launchIn(modelScope) + .saveIn(expressStatusJobHolder) } - private fun subscribeOnExpressTransactionsUpdates() { - modelScope.launch(dispatchers.main) { - expressTxStatusTaskScheduler.cancelTask() - expressTxStatusTaskScheduler.scheduleTask( - modelScope, - PeriodicTask( - isDelayFirst = false, - delay = EXPRESS_STATUS_UPDATE_DELAY, - task = { - runCatching { onrampStatusFactory.updateOnrmapTransactionStatuses() } - }, - onSuccess = { /* no-op */ }, - onError = { /* no-op */ }, - ), - ) - }.saveIn(expressStatusJobHolder) + private fun subscribeOnExpressTransactionsUpdates(userWallet: UserWallet) { + expressTxStatusTaskScheduler.cancelTask() + expressTxStatusTaskScheduler.scheduleTask( + modelScope, + PeriodicTask( + isDelayFirst = false, + delay = EXPRESS_STATUS_UPDATE_DELAY, + task = { + runCatching { + onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) + } + }, + onSuccess = { /* no-op */ }, + onError = { /* no-op */ }, + ), + ) } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index aeb4c7ad66..e60f42ad67 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -11,6 +11,7 @@ import com.tangem.domain.onramp.OnrampUpdateTransactionStatusUseCase import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.OnrampStatus.Status.* import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -40,22 +41,22 @@ internal class OnrampStatusFactory @Inject constructor( } } - suspend fun updateOnrmapTransactionStatuses() = withContext(dispatchers.io) { + suspend fun updateOnrmapTransactionStatuses(userWallet: UserWallet) = withContext(dispatchers.io) { val singleWalletState = stateHolder.getSelectedWallet() as? WalletState.SingleCurrency.Content ?: return@withContext singleWalletState.expressTxs.map { tx -> async { if (tx is ExpressTransactionStateUM.OnrampUM) { - updateOnrampTxStatus(tx) + updateOnrampTxStatus(userWallet, tx) } } }.awaitAll() } - private suspend fun updateOnrampTxStatus(onrampTx: ExpressTransactionStateUM.OnrampUM) { + private suspend fun updateOnrampTxStatus(userWallet: UserWallet, onrampTx: ExpressTransactionStateUM.OnrampUM) { if (!onrampTx.activeStatus.isTerminal) { - getOnrampStatusUseCase(onrampTx.info.txId).fold( + getOnrampStatusUseCase(userWallet = userWallet, onrampTx.info.txId).fold( ifLeft = { Timber.e("Couldn't update onramp status. $it") }, diff --git a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt index ebca655e53..fa5fc99d83 100644 --- a/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt +++ b/libs/auth/src/main/java/com/tangem/lib/auth/ExpressAuthProvider.kt @@ -1,10 +1,5 @@ package com.tangem.lib.auth interface ExpressAuthProvider { - - fun getUserId(): String - fun getSessionId(): String - - fun getRefCode(): String } \ No newline at end of file From 92794cb2f61e1af1ef8f4ff78ce7de6de06a488a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 13:39:33 +0500 Subject: [PATCH 090/151] Updated on 2026-08-14 --- .../java/com/tangem/tap/routing/utils/ChildFactory.kt | 10 ++++++++-- .../main/kotlin/com/tangem/common/routing/AppRoute.kt | 1 + core/res/src/main/res/values-ja/strings.xml | 1 - core/res/src/main/res/values/strings.xml | 1 - .../com/tangem/features/nft/component/NFTComponent.kt | 1 + .../tangem/features/nft/common/DefaultNFTComponent.kt | 1 + .../tangem/features/nft/receive/NFTReceiveComponent.kt | 1 + .../tangem/features/nft/receive/entity/NFTReceiveUM.kt | 2 ++ .../features/nft/receive/model/NFTReceiveModel.kt | 5 +++++ .../com/tangem/features/nft/receive/ui/NFTReceive.kt | 3 ++- .../wallet/model/intents/WalletContentClickIntents.kt | 7 +++---- .../wallet/presentation/router/DefaultWalletRouter.kt | 10 ++++++++-- .../wallet/presentation/router/InnerWalletRouter.kt | 3 ++- .../wallet/subscribers/WalletNFTListSubscriber.kt | 2 +- 14 files changed, 35 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 3eebff1081..189f0f09e5 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 @@ -406,7 +406,10 @@ internal class ChildFactory @Inject constructor( is AppRoute.NFT -> createComponentChild( context = context, - params = NFTComponent.Params(userWalletId = route.userWalletId), + params = NFTComponent.Params( + userWalletId = route.userWalletId, + walletName = route.walletName, + ), componentFactory = nftComponentFactory, ) is AppRoute.NFTSend -> { @@ -757,7 +760,10 @@ internal class ChildFactory @Inject constructor( is AppRoute.NFT -> route.asComponentChild( contextProvider = contextProvider(route, contextFactory), - params = NFTComponent.Params(userWalletId = route.userWalletId), + params = NFTComponent.Params( + userWalletId = route.userWalletId, + walletName = route.walletName, + ), componentFactory = nftComponentFactory, ) is AppRoute.NFTSend -> { 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 2074653f5f..ca72fdcd9a 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 @@ -283,6 +283,7 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class NFT( val userWalletId: UserWalletId, + val walletName: String, ) : AppRoute(path = "/nft/${userWalletId.stringValue}") @Serializable diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index b59492d1ed..df67256173 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -518,7 +518,6 @@ 特徴 結果がありません。別のリクエストをお試しください。 ネットワークを選択 - 私のウォレットへ NFTを受け取る %1$dコレクションの%2$dNFT ここをタップして最初のNFTを受け取ります diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 42b9c6762c..074a5c4164 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -527,7 +527,6 @@ Traits No results. Please try another request. Choose network - To My wallet Receive NFT Traits %1$d NFTs in %2$d collection diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTComponent.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTComponent.kt index f2f8d3f9e5..f49edd1b5b 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTComponent.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/component/NFTComponent.kt @@ -8,6 +8,7 @@ interface NFTComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, + val walletName: String, ) interface Factory : ComponentFactory 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 95e4a2ad8f..5b60f0c848 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 @@ -122,6 +122,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( context = factoryContext, params = NFTReceiveComponent.Params( userWalletId = route.userWalletId, + walletName = params.walletName, onBackClick = ::onChildBack, ), ) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt index 0be6d0b5f7..e52f42d20d 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/NFTReceiveComponent.kt @@ -29,6 +29,7 @@ internal class NFTReceiveComponent @AssistedInject constructor( data class Params( val userWalletId: UserWalletId, + val walletName: String, val onBackClick: () -> Unit, ) } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt index 0764b2f074..e31c80c425 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt @@ -3,10 +3,12 @@ package com.tangem.features.nft.receive.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList internal data class NFTReceiveUM( val onBackClick: () -> Unit, + val appBarSubtitle: TextReference, val search: SearchBarUM, val networks: Networks, val bottomSheetConfig: TangemBottomSheetConfig?, 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 3d62d4c7e2..88a9b57851 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 @@ -9,6 +9,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager 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.core.ui.extensions.wrappedList import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase import com.tangem.domain.nft.GetNFTAvailableNetworksUseCase import com.tangem.domain.nft.GetNFTNetworkStatusUseCase @@ -47,6 +48,10 @@ internal class NFTReceiveModel @Inject constructor( private val _state = MutableStateFlow( value = NFTReceiveUM( onBackClick = params.onBackClick, + appBarSubtitle = resourceReference( + R.string.hot_crypto_add_token_subtitle, + formatArgs = wrappedList(params.walletName), + ), search = getInitialSearchBar(), networks = NFTReceiveUM.Networks.Content(persistentListOf()), bottomSheetConfig = null, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt index 69acaeef22..47a338514f 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.nft.impl.R @@ -33,7 +34,7 @@ internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) { onIconClicked = state.onBackClick, ), title = stringResourceSafe(id = R.string.nft_receive_title), - subtitle = stringResourceSafe(id = R.string.nft_receive_subtitle), + subtitle = state.appBarSubtitle.resolveReference(), ) }, content = { innerPadding -> 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 9bb3ac509c..e4b51cf352 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 @@ -14,7 +14,6 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap @@ -59,7 +58,7 @@ internal interface WalletContentClickIntents { fun onDisposeExpressStatus() - fun onNFTClick(userWalletId: UserWalletId) + fun onNFTClick(userWallet: UserWallet) } @Suppress("LongParameterList") @@ -242,7 +241,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( stateHolder.update(CloseBottomSheetTransformer(userWalletId)) } - override fun onNFTClick(userWalletId: UserWalletId) { + override fun onNFTClick(userWallet: UserWallet) { 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) @@ -256,6 +255,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(NFTAnalyticsEvent.NFTListScreenOpened(analyticsState)) } - router.openNFT(userWalletId) + router.openNFT(userWallet) } } \ 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 8028924122..2b865d1f61 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 @@ -10,6 +10,7 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig @@ -116,7 +117,12 @@ internal class DefaultWalletRouter @Inject constructor( reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) } - override fun openNFT(userWalletId: UserWalletId) { - router.push(AppRoute.NFT(userWalletId)) + override fun openNFT(userWallet: UserWallet) { + router.push( + AppRoute.NFT( + userWalletId = userWallet.walletId, + walletName = userWallet.name, + ), + ) } } \ 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 7d9d61c701..393c930e9c 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 @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig @@ -58,5 +59,5 @@ internal interface InnerWalletRouter { fun openScanFailedDialog(onTryAgain: () -> Unit) /** Open NFT collections screen */ - fun openNFT(userWalletId: UserWalletId) + fun openNFT(userWallet: UserWallet) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt index 67a36a2935..57ed2a8b6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -37,7 +37,7 @@ internal class WalletNFTListSubscriber( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, nftCollections = it, - onItemClick = { clickIntents.onNFTClick(userWallet.walletId) }, + onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) } From 55a740c941565f8a6529d3c7d21de93aa4fcb2ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 12:37:05 +0500 Subject: [PATCH 091/151] Updated on 2026-08-14 --- .../DefaultTransactionRepository.kt | 30 +++++++++++++------ .../transaction/TransactionRepository.kt | 4 +-- .../CreateApprovalTransactionUseCase.kt | 2 +- .../transaction/usecase/GetFeeUseCase.kt | 2 +- .../helpers/StakingFeeTransactionLoader.kt | 20 +++++++++++-- 5 files changed, 42 insertions(+), 16 deletions(-) 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 da6951cafd..69af643654 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 @@ -38,7 +38,7 @@ internal class DefaultTransactionRepository( override suspend fun createTransaction( amount: Amount, - fee: Fee, + fee: Fee?, memo: String?, destination: String, userWalletId: UserWalletId, @@ -52,13 +52,25 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) ?: error("Wallet manager not found") - return@withContext walletManager.createTransaction( - amount = amount, - fee = fee, - destination = destination, - ).copy( - extras = txExtras ?: getMemoExtras(networkId = network.id.value, memo), - ) + val extras = txExtras ?: getMemoExtras(networkId = network.id.value, memo) + + return@withContext if (fee != null) { + walletManager.createTransaction( + amount = amount, + fee = fee, + destination = destination, + ).copy( + extras = extras, + ) + } else { + TransactionData.Uncompiled( + amount = amount, + sourceAddress = walletManager.wallet.address, + destinationAddress = destination, + extras = extras, + fee = null, + ) + } } override suspend fun createTransferTransaction( @@ -102,7 +114,7 @@ internal class DefaultTransactionRepository( override suspend fun createApprovalTransaction( amount: Amount, approvalAmount: Amount?, - fee: Fee, + fee: Fee?, contractAddress: String, spenderAddress: String, userWalletId: UserWalletId, 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 9ef573a666..01d37e849d 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 @@ -16,7 +16,7 @@ interface TransactionRepository { @Suppress("LongParameterList") suspend fun createTransaction( amount: Amount, - fee: Fee, + fee: Fee?, memo: String?, destination: String, userWalletId: UserWalletId, @@ -38,7 +38,7 @@ interface TransactionRepository { suspend fun createApprovalTransaction( amount: Amount, approvalAmount: Amount?, - fee: Fee, + fee: Fee?, contractAddress: String, spenderAddress: String, userWalletId: UserWalletId, 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 d3b136ff3e..903025cb16 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 @@ -20,7 +20,7 @@ class CreateApprovalTransactionUseCase( cryptoCurrency: CryptoCurrency.Token, userWalletId: UserWalletId, amount: BigDecimal?, - fee: Fee, + fee: Fee?, contractAddress: String, spenderAddress: String, ) = Either.catch { 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 ed841119c1..0e427fb8c0 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 @@ -33,7 +33,7 @@ class GetFeeUseCase( walletManagersFacade.getOrCreateWalletManager( userWalletId = userWallet.walletId, network = network, - ) as? TransactionSender + ) } val result = transactionSender?.getFee(transactionData = transactionData) ?: error("Fee is null") 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 8043052fe7..3bbd7b2f6e 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 @@ -14,9 +14,11 @@ import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.ActionParams import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate +import com.tangem.domain.tokens.model.CryptoCurrency 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.CreateApprovalTransactionUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.wallets.models.UserWallet @@ -40,6 +42,7 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( private val getFeeUseCase: GetFeeUseCase, private val estimateGasUseCase: EstimateGasUseCase, private val isFeeApproximateUseCase: IsFeeApproximateUseCase, + private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @Assisted private val userWallet: UserWallet, @Assisted private val yield: Yield, @@ -183,11 +186,22 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( onApprovalFee: (TransactionFee) -> Unit, onApprovalFeeError: (GetFeeError) -> Unit, ) { - getFeeUseCase( + val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token + ?: return onApprovalFeeError(GetFeeError.UnknownError) + val approvalTransactionData = createApprovalTransactionUseCase( + cryptoCurrency = tokenCurrency, + userWalletId = userWallet.walletId, amount = amount, - destination = validatorAddress, + fee = null, + contractAddress = tokenCurrency.contractAddress, + spenderAddress = validatorAddress, + ).getOrElse { + return onApprovalFeeError(GetFeeError.DataError(it)) + } + getFeeUseCase( userWallet = userWallet, - cryptoCurrency = cryptoCurrencyStatus.currency, + network = tokenCurrency.network, + transactionData = approvalTransactionData, ).fold( ifRight = { fee -> onApprovalFee(fee) From 51ce41ee51e2bcce9a0c9d61db50c95cfeb0d5e8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 14:06:01 +0300 Subject: [PATCH 092/151] Updated on 2026-08-14 --- .../test/data/staking/MockYieldDTOFactory.kt | 88 +++++ .../CommonYieldBalanceFetcherDelegate.kt | 114 ++++++ .../fetcher/YieldBalanceFetcherImplementor.kt | 10 +- .../multi/DefaultMultiYieldBalanceFetcher.kt | 51 +++ .../DefaultSingleYieldBalanceFetcher.kt | 52 +++ .../utils/YieldBalanceRequestBodyFactory.kt | 27 ++ .../DefaultMultiYieldBalanceFetcherTest.kt | 323 ++++++++++++++++ .../DefaultSingleYieldBalanceFetcherTest.kt | 363 ++++++++++++++++++ .../staking/multi/MultiYieldBalanceFetcher.kt | 11 + .../single/SingleYieldBalanceFetcher.kt | 11 + 10 files changed, 1043 insertions(+), 7 deletions(-) create mode 100644 common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/fetcher/CommonYieldBalanceFetcherDelegate.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyFactory.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt create mode 100644 domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt diff --git a/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt new file mode 100644 index 0000000000..189705d948 --- /dev/null +++ b/common/test/src/main/java/com/tangem/common/test/data/staking/MockYieldDTOFactory.kt @@ -0,0 +1,88 @@ +package com.tangem.common.test.data.staking + +import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentDTO +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.YieldDTO +import com.tangem.domain.staking.model.StakingID +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +object MockYieldDTOFactory { + + val defaultStakingID = StakingID( + integrationId = "ton-ton-chorus-one-pools-staking", + address = "0x1", + ) + + fun create(stakingID: StakingID = defaultStakingID): YieldDTO { + return YieldDTO( + id = stakingID.integrationId, + token = TokenDTO( + name = "Miguel Estes", + network = NetworkTypeDTO.POLYGON, + symbol = "splendide", + decimals = 1323, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + tokens = listOf(), + args = YieldDTO.ArgsDTO( + enter = YieldDTO.ArgsDTO.Enter( + addresses = YieldDTO.ArgsDTO.Enter.Addresses( + address = AddressArgumentDTO(required = false), + ), + args = mapOf(), + ), + exit = null, + ), + status = YieldDTO.StatusDTO(enter = true, exit = true), + apy = BigDecimal.ONE, + rewardRate = 1.0, + rewardType = YieldDTO.RewardTypeDTO.UNKNOWN, + metadata = YieldDTO.MetadataDTO( + name = "name", + logoUri = "logoUri", + description = "description", + documentation = null, + gasFeeTokenDTO = TokenDTO( + name = "Johnnie Mullen", + network = NetworkTypeDTO.POLYGON, + symbol = "fuisset", + decimals = 2957, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + tokenDTO = TokenDTO( + name = "Lazaro Wood", + network = NetworkTypeDTO.POLYGON, + symbol = "vocent", + decimals = 1602, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + tokensDTO = listOf(), + type = "type", + rewardSchedule = YieldDTO.MetadataDTO.RewardScheduleDTO.DAY, + cooldownPeriod = null, + warmupPeriod = YieldDTO.MetadataDTO.PeriodDTO(1), + rewardClaiming = YieldDTO.MetadataDTO.RewardClaimingDTO.AUTO, + defaultValidator = null, + minimumStake = null, + supportsMultipleValidators = null, + revshare = YieldDTO.MetadataDTO.EnabledDTO(enabled = true), + fee = YieldDTO.MetadataDTO.EnabledDTO(enabled = true), + ), + validators = listOf(), + isAvailable = true, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/fetcher/CommonYieldBalanceFetcherDelegate.kt b/data/staking/src/main/java/com/tangem/data/staking/fetcher/CommonYieldBalanceFetcherDelegate.kt new file mode 100644 index 0000000000..7204495961 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/fetcher/CommonYieldBalanceFetcherDelegate.kt @@ -0,0 +1,114 @@ +package com.tangem.data.staking.fetcher + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.left +import arrow.core.raise.catch +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO +import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.core.utils.catchOn +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import timber.log.Timber + +internal fun commonFetcher( + implementor: YieldBalanceFetcherImplementor, + stakingYieldsStore: StakingYieldsStore, + yieldsBalancesStore: YieldsBalancesStore, + dispatchers: CoroutineDispatcherProvider, +): FlowFetcher { + return CommonYieldBalanceFetcher( + implementor = implementor, + stakingYieldsStore = stakingYieldsStore, + yieldsBalancesStore = yieldsBalancesStore, + dispatchers = dispatchers, + ) +} + +/** + * Common implementation of YieldBalanceFetcher + * + * @param implementor fetcher implementor + * @property stakingYieldsStore staking yields store + * @property yieldsBalancesStore yields balances store + * @property dispatchers dispatchers + */ +private class CommonYieldBalanceFetcher( + private val implementor: YieldBalanceFetcherImplementor, + private val stakingYieldsStore: StakingYieldsStore, + private val yieldsBalancesStore: YieldsBalancesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : FlowFetcher { + + override suspend fun invoke(params: Params): Either { + val stakingIds = getStakingIds(params).getOrElse { + return it.left() + } + + return Either.catchOn(dispatchers.default) { + val requests = prefetch(userWalletId = params.userWalletId, stakingIds = stakingIds) + + implementor.fetch(params = params, stakingIds = stakingIds, requests) + } + .onLeft { yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds) } + } + + private suspend fun getStakingIds(params: Params): Either> = either { + val stakingIds = catch( + block = { implementor.createStakingIds(params = params) }, + catch = { raise(it) }, + ) + + ensure(stakingIds.isNotEmpty()) { + val exception = IllegalStateException("Unable to create staking ids for $params: list is empty") + Timber.e(exception) + + raise(exception) + } + + stakingIds + } + + private suspend fun prefetch( + userWalletId: UserWalletId, + stakingIds: Set, + ): List { + yieldsBalancesStore.refresh(userWalletId = userWalletId, stakingIds = stakingIds) + + val yieldDTOs = stakingYieldsStore.getSyncWithTimeout() + + if (yieldDTOs.isNullOrEmpty()) { + val exception = IllegalStateException("No enabled yields for $userWalletId") + Timber.e(exception) + throw exception + } + + val yieldIds = yieldDTOs.mapNotNullTo(destination = hashSetOf(), transform = YieldDTO::id) + + val requests = stakingIds + .filter { stakingId -> yieldIds.any { it == stakingId.integrationId } } + .map(YieldBalanceRequestBodyFactory::create) + + if (requests.isEmpty()) { + val exception = IllegalStateException( + """ + No available yields to fetch yield balances: + – userWalletId: $userWalletId + – stakingIds: ${stakingIds.joinToString()} + """.trimIndent(), + ) + Timber.d(exception) + throw exception + } + + return requests + } +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/fetcher/YieldBalanceFetcherImplementor.kt b/data/staking/src/main/java/com/tangem/data/staking/fetcher/YieldBalanceFetcherImplementor.kt index 27835a3da0..be258fc5fe 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/fetcher/YieldBalanceFetcherImplementor.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/fetcher/YieldBalanceFetcherImplementor.kt @@ -9,10 +9,10 @@ import com.tangem.domain.staking.model.StakingID * [REDACTED_AUTHOR] */ -internal interface YieldBalanceFetcherImplementor { +internal interface YieldBalanceFetcherImplementor { /** Create set of [StakingID] */ - suspend fun createStakingIds(params: @UnsafeVariance Params): Set + suspend fun createStakingIds(params: Params): Set /** * Fetch yield balances @@ -21,9 +21,5 @@ internal interface YieldBalanceFetcherImplementor, - requests: List, - ) + suspend fun fetch(params: Params, stakingIds: Set, requests: List) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt new file mode 100644 index 0000000000..45cccaea38 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt @@ -0,0 +1,51 @@ +package com.tangem.data.staking.multi + +import com.tangem.data.staking.fetcher.YieldBalanceFetcherImplementor +import com.tangem.data.staking.fetcher.commonFetcher +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +/** + * Default implementation of [MultiYieldBalanceFetcher] + * + * @property stakingYieldsStore staking yields store + * @property yieldsBalancesStore yields balances store + * @property stakingIdFactory factory for creating StakingID + * @property stakeKitApi stake kit API + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultMultiYieldBalanceFetcher( + private val stakingYieldsStore: StakingYieldsStore, + private val yieldsBalancesStore: YieldsBalancesStore, + private val stakingIdFactory: StakingIdFactory, + private val stakeKitApi: StakeKitApi, + private val dispatchers: CoroutineDispatcherProvider, +) : MultiYieldBalanceFetcher, + FlowFetcher by commonFetcher( + implementor = createMultiFetcherImplementor(yieldsBalancesStore, stakingIdFactory, stakeKitApi, dispatchers), + stakingYieldsStore = stakingYieldsStore, + yieldsBalancesStore = yieldsBalancesStore, + dispatchers = dispatchers, + ) + +private fun createMultiFetcherImplementor( + yieldsBalancesStore: YieldsBalancesStore, + stakingIdFactory: StakingIdFactory, + stakeKitApi: StakeKitApi, + dispatchers: CoroutineDispatcherProvider, +): YieldBalanceFetcherImplementor { + return MultiYieldBalanceFetcherImplementor( + yieldsBalancesStore = yieldsBalancesStore, + stakingIdFactory = stakingIdFactory, + stakeKitApi = stakeKitApi, + dispatchers = dispatchers, + ) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt new file mode 100644 index 0000000000..37d48ac768 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt @@ -0,0 +1,52 @@ +package com.tangem.data.staking.single + +import com.tangem.data.staking.fetcher.YieldBalanceFetcherImplementor +import com.tangem.data.staking.fetcher.commonFetcher +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +import com.tangem.utils.coroutines.CoroutineDispatcherProvider + +/** + * Default implementation of [MultiYieldBalanceFetcher] + * + * @property stakingYieldsStore staking yields store + * @property yieldsBalancesStore yields balances store + * @property stakingIdFactory factory for creating StakingID + * @property stakeKitApi stake kit API + * @property dispatchers dispatchers + * +[REDACTED_AUTHOR] + */ +internal class DefaultSingleYieldBalanceFetcher( + private val stakingYieldsStore: StakingYieldsStore, + private val yieldsBalancesStore: YieldsBalancesStore, + private val stakingIdFactory: StakingIdFactory, + private val stakeKitApi: StakeKitApi, + private val dispatchers: CoroutineDispatcherProvider, +) : SingleYieldBalanceFetcher, + FlowFetcher by commonFetcher( + implementor = createSingleFetcherImplementor(yieldsBalancesStore, stakingIdFactory, stakeKitApi, dispatchers), + stakingYieldsStore = stakingYieldsStore, + yieldsBalancesStore = yieldsBalancesStore, + dispatchers = dispatchers, + ) + +private fun createSingleFetcherImplementor( + yieldsBalancesStore: YieldsBalancesStore, + stakingIdFactory: StakingIdFactory, + stakeKitApi: StakeKitApi, + dispatchers: CoroutineDispatcherProvider, +): YieldBalanceFetcherImplementor { + return SingleYieldBalanceFetcherImplementor( + yieldsBalancesStore = yieldsBalancesStore, + stakingIdFactory = stakingIdFactory, + stakeKitApi = stakeKitApi, + dispatchers = dispatchers, + ) +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyFactory.kt b/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyFactory.kt new file mode 100644 index 0000000000..64d63950a9 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/utils/YieldBalanceRequestBodyFactory.kt @@ -0,0 +1,27 @@ +package com.tangem.data.staking.utils + +import com.tangem.datasource.api.stakekit.models.request.Address +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +import com.tangem.domain.staking.model.StakingID + +/** + * Factory for creating [YieldBalanceRequestBody] + * +[REDACTED_AUTHOR] + */ +internal object YieldBalanceRequestBodyFactory { + + fun create(stakingID: StakingID): YieldBalanceRequestBody { + return YieldBalanceRequestBody( + addresses = Address( + address = stakingID.address, + additionalAddresses = null, // todo fill additional addresses metadata if needed + explorerUrl = "", // todo fill exporer url [REDACTED_JIRA] + ), + args = YieldBalanceRequestBody.YieldBalanceRequestArgs( + validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA] + ), + integrationId = stakingID.integrationId, + ) + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt new file mode 100644 index 0000000000..5f57b9d20d --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt @@ -0,0 +1,323 @@ +package com.tangem.data.staking.multi + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.data.staking.MockYieldDTOFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.Test + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultMultiYieldBalanceFetcherTest { + + private val stakingYieldsStore: StakingYieldsStore = mockk() + private val yieldsBalancesStore: YieldsBalancesStore = mockk() + private val stakingIdFactory: StakingIdFactory = mockk() + private val stakeKitApi: StakeKitApi = mockk() + + private val fetcher = DefaultMultiYieldBalanceFetcher( + stakingYieldsStore = stakingYieldsStore, + yieldsBalancesStore = yieldsBalancesStore, + stakingIdFactory = stakingIdFactory, + stakeKitApi = stakeKitApi, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `fetch yields balances successfully`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs + + val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields + + val requests = tonAndSolanaIds.map(YieldBalanceRequestBodyFactory::create).sortedBy { it.integrationId } + val result = setOf( + MockYieldBalanceWrapperDTOFactory.createWithBalance(solanaId), + MockYieldBalanceWrapperDTOFactory.createWithBalance(tonId), + ) + coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns ApiResponse.Success(result) + coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, solana.id, solana.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getMultipleYieldBalances(requests) + yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = result) + } + + coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch yields balances failure if stakingIdFactory returns empty list`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns emptySet() + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns emptySet() + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, solana.id, solana.network) + } + + coVerify(inverse = true) { + yieldsBalancesStore.refresh(any(), any>()) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getMultipleYieldBalances(any()) + yieldsBalancesStore.storeActual(any(), any()) + yieldsBalancesStore.storeError(any(), any()) + } + + val expected = IllegalStateException("Unable to create staking ids for $params: list is empty") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null + coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, solana.id, solana.network) + yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) + stakingYieldsStore.getSyncWithTimeout() + yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + } + + coVerify(inverse = true) { + stakeKitApi.getMultipleYieldBalances(any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + } + + val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() + coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, solana.id, solana.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingYieldsStore.getSyncWithTimeout() + yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + } + + coVerify(inverse = true) { + stakeKitApi.getMultipleYieldBalances(any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + } + + val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if yields converting is failed`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs + + val yields = listOf( + MockYieldDTOFactory.create(tonId).copy(id = null), + MockYieldDTOFactory.create(solanaId).copy(id = null), + ) + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields + coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, solana.id, solana.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingYieldsStore.getSyncWithTimeout() + yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + } + + coVerify(inverse = true) { + stakeKitApi.getMultipleYieldBalances(any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + } + + val expected = IllegalStateException( + """ + No available yields to fetch yield balances: + – userWalletId: $userWalletId + – stakingIds: ${setOf(solanaId, tonId).joinToString()} + """.trimIndent(), + ) + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs + + val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1"))) + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields + coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, solana.id, solana.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingYieldsStore.getSyncWithTimeout() + yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) + } + + coVerify(inverse = true) { + stakeKitApi.getMultipleYieldBalances(any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + } + + val expected = IllegalStateException( + """ + No available yields to fetch yield balances: + – userWalletId: $userWalletId + – stakingIds: ${setOf(solanaId, tonId).joinToString()} + """.trimIndent(), + ) + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) + coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) + coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs + + val yields = listOf(MockYieldDTOFactory.create(tonId), MockYieldDTOFactory.create(solanaId)) + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields + + val requests = setOf(solanaId, tonId).map(YieldBalanceRequestBodyFactory::create) + + @Suppress("UNCHECKED_CAST") + val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) + as ApiResponse> + + coEvery { stakeKitApi.getMultipleYieldBalances(requests) } returns errorResponse + coEvery { yieldsBalancesStore.storeError(userWalletId, tonAndSolanaIds) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.create(params.userWalletId, ton.id, ton.network) + stakingIdFactory.create(params.userWalletId, solana.id, solana.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getMultipleYieldBalances(requests) + yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = tonAndSolanaIds) + } + + coVerify(inverse = true) { yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) } + + val expected = ApiResponseError.NetworkException + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + private companion object { + val userWalletId = UserWalletId("011") + + val mocks = MockCryptoCurrencyFactory() + + val ton = mocks.createCoin(Blockchain.TON) + val solana = mocks.createCoin(Blockchain.Solana) + + val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId + val solanaId = StakingID( + integrationId = "solana-sol-native-multivalidator-staking", + address = "0x1", + ) + + val tonAndSolanaIds = setOf(tonId, solanaId) + } +} \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt new file mode 100644 index 0000000000..16e2049744 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt @@ -0,0 +1,363 @@ +package com.tangem.data.staking.single + +import com.google.common.truth.Truth +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory +import com.tangem.common.test.data.staking.MockYieldDTOFactory +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.utils.StakingIdFactory +import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.datasource.api.stakekit.StakeKitApi +import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody +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 com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.math.BigDecimal + +/** +[REDACTED_AUTHOR] + */ +internal class DefaultSingleYieldBalanceFetcherTest { + + private val stakingYieldsStore: StakingYieldsStore = mockk() + private val yieldsBalancesStore: YieldsBalancesStore = mockk() + private val stakingIdFactory: StakingIdFactory = mockk() + private val stakeKitApi: StakeKitApi = mockk() + + private val fetcher = DefaultSingleYieldBalanceFetcher( + stakingYieldsStore = stakingYieldsStore, + yieldsBalancesStore = yieldsBalancesStore, + stakingIdFactory = stakingIdFactory, + stakeKitApi = stakeKitApi, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + @Test + fun `fetch yields balances successfully`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + } just Runs + + val yields = listOf(MockYieldDTOFactory.create(tonId)) + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields + + val request = YieldBalanceRequestBodyFactory.create(tonId) + val result = listOf(createBalanceDTO()) + coEvery { stakeKitApi.getSingleYieldBalance(tonId.integrationId, request) } returns ApiResponse.Success(result) + + val values = result.mapTo(hashSetOf()) { it.toWrapper(request) } + coEvery { yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = values) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getSingleYieldBalance(integrationId = tonId.integrationId, body = request) + yieldsBalancesStore.storeActual(userWalletId = userWalletId, values = values) + } + + coVerify(inverse = true) { yieldsBalancesStore.storeError(any(), any()) } + + Truth.assertThat(actual.isRight()).isTrue() + } + + @Test + fun `fetch yields balances failure if stakingIdFactory createForDefault returns null`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns null + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.createForDefault(userWalletId = userWalletId, currencyId = ton.id, network = ton.network) + } + + coVerify(inverse = true) { + yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + } + + val expected = IllegalStateException("Unable to create staking ids for $params: list is empty") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns null`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + } just Runs + + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns null + coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + stakingYieldsStore.getSyncWithTimeout() + yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) + } + + coVerify(inverse = true) { + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + } + + val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if stakingYieldsStore getSyncWithTimeout returns empty list`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + } just Runs + + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns emptyList() + coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + stakingYieldsStore.getSyncWithTimeout() + yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) + } + + coVerify(inverse = true) { + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + } + + val expected = IllegalStateException("No enabled yields for ${params.userWalletId}") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if yields converting is failed`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + } just Runs + + val yields = listOf(MockYieldDTOFactory.create(tonId).copy(id = null)) + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields + coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + stakingYieldsStore.getSyncWithTimeout() + yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) + } + + coVerify(inverse = true) { + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + } + + val expected = IllegalStateException( + """ + No available yields to fetch yield balances: + – userWalletId: $userWalletId + – stakingIds: ${setOf(tonId).joinToString()} + """.trimIndent(), + ) + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if available yields does not contain ids from params`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + } just Runs + + val yields = listOf(MockYieldDTOFactory.create(StakingID(integrationId = "polygon", address = "0x1"))) + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields + coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + stakingYieldsStore.getSyncWithTimeout() + yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) + } + + coVerify(inverse = true) { + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + } + + val expected = IllegalStateException( + """ + No available yields to fetch yield balances: + – userWalletId: $userWalletId + – stakingIds: ${setOf(tonId).joinToString()} + """.trimIndent(), + ) + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if stakeKitApi getMultipleYieldBalances is failed`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId + coEvery { + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + } just Runs + + val yields = listOf(MockYieldDTOFactory.create(tonId)) + coEvery { stakingYieldsStore.getSyncWithTimeout() } returns yields + + val request = YieldBalanceRequestBodyFactory.create(tonId) + + @Suppress("UNCHECKED_CAST") + val errorResponse = ApiResponse.Error(ApiResponseError.NetworkException) as ApiResponse> + + coEvery { stakeKitApi.getSingleYieldBalance(tonId.integrationId, request) } returns errorResponse + coEvery { yieldsBalancesStore.storeError(userWalletId, setOf(tonId)) } just Runs + + val actual = fetcher(params) + + coVerify { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getSingleYieldBalance(tonId.integrationId, request) + yieldsBalancesStore.storeError(userWalletId = userWalletId, stakingIds = setOf(tonId)) + } + + coVerify(inverse = true) { yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) } + + val expected = ApiResponseError.NetworkException + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + private fun createBalanceDTO(): BalanceDTO { + return BalanceDTO( + groupId = "dictas", + type = BalanceDTO.BalanceTypeDTO.REWARDS, + amount = BigDecimal.ONE, + date = null, + pricePerShare = BigDecimal.ONE, + pendingActions = listOf(), + pendingActionConstraints = listOf(), + tokenDTO = TokenDTO( + name = "Casandra Paul", + network = NetworkTypeDTO.POLYGON, + symbol = "vim", + decimals = 3994, + address = null, + coinGeckoId = null, + logoURI = null, + isPoints = null, + ), + validatorAddress = null, + validatorAddresses = listOf(), + providerId = null, + ) + } + + private fun BalanceDTO.toWrapper(request: YieldBalanceRequestBody): YieldBalanceWrapperDTO { + return YieldBalanceWrapperDTO( + balances = listOf(this), + integrationId = request.integrationId, + addresses = request.addresses, + ) + } + + private companion object { + val userWalletId = UserWalletId("011") + + val mocks = MockCryptoCurrencyFactory() + + val ton = mocks.createCoin(Blockchain.TON) + + val tonId = MockYieldBalanceWrapperDTOFactory.defaultStakingId + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt new file mode 100644 index 0000000000..a189e8bcc9 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiYieldBalanceFetcher.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.staking.multi + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams + +/** + * Fetcher of yields balances + * +[REDACTED_AUTHOR] + */ +interface MultiYieldBalanceFetcher : FlowFetcher \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt new file mode 100644 index 0000000000..1b81d24be3 --- /dev/null +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleYieldBalanceFetcher.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.staking.single + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams + +/** + * Fetcher of yield balance + * +[REDACTED_AUTHOR] + */ +interface SingleYieldBalanceFetcher : FlowFetcher \ No newline at end of file From c300a5eaf337e5bcc439f09cb2c9d9b6a03e93b2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 13:58:07 +0500 Subject: [PATCH 093/151] Updated on 2026-08-14 --- .../wallet/child/wallet/model/WalletModel.kt | 30 ------------------- .../model/intents/WalletClickIntents.kt | 8 ----- .../SetNFTCollectionsTransformer.kt | 15 ++-------- 3 files changed, 2 insertions(+), 51 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 270806b3a2..56879b602c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -11,13 +11,11 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -45,7 +43,6 @@ import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.utils.Provider import com.tangem.utils.coroutines.* -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -80,8 +77,6 @@ internal class WalletModel @Inject constructor( private val onrampStatusFactory: OnrampStatusFactory, private val biometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventsHandler: AnalyticsEventHandler, - private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, - private val walletsRepository: WalletsRepository, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, @@ -93,7 +88,6 @@ internal class WalletModel @Inject constructor( private val walletsUpdateJobHolder = JobHolder() private val refreshWalletJobHolder = JobHolder() private val expressStatusJobHolder = JobHolder() - private val walletsNFTsUpdateJobHolder = JobHolder() private var needToRefreshWallet = false private var expressTxStatusTaskScheduler = SingleTaskScheduler() @@ -109,7 +103,6 @@ internal class WalletModel @Inject constructor( subscribeOnBalanceHiding() subscribeOnSelectedWalletFlow() subscribeOnPushNotificationsPermission() - subscribeOnNFTUpdates() clickIntents.initialize(innerWalletRouter, modelScope) } @@ -269,29 +262,6 @@ internal class WalletModel @Inject constructor( ) } - @OptIn(ExperimentalCoroutinesApi::class) - private fun subscribeOnNFTUpdates() { - getWalletsUseCase() - .conflate() - .distinctUntilChanged() - .flatMapLatest { wallets -> - wallets - .map { wallet -> - walletsRepository.nftEnabledStatus(wallet.walletId) - .distinctUntilChanged() - .onEach { nftEnabled -> - if (nftEnabled) { - fetchNFTCollectionsUseCase.invoke(wallet.walletId) - } - } - } - .merge() - } - .flowOn(dispatchers.main) - .launchIn(modelScope) - .saveIn(walletsNFTsUpdateJobHolder) - } - private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index ee324b252e..e39efc204f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -5,7 +5,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager -import com.tangem.domain.nft.FetchNFTCollectionsUseCase import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.settings.NeverToShowWalletsScrollPreview import com.tangem.domain.tokens.FetchCardTokenListUseCase @@ -15,7 +14,6 @@ import com.tangem.domain.tokens.FetchTokenListUseCase.RefreshMode import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase import com.tangem.feature.wallet.presentation.router.InnerWalletRouter -import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController @@ -52,8 +50,6 @@ internal class WalletClickIntents @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val onrampFeatureToggles: OnrampFeatureToggles, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, - private val fetchNFTCollectionsUseCase: FetchNFTCollectionsUseCase, - private val isWalletNFTEnabledSyncUseCase: IsWalletNFTEnabledSyncUseCase, ) : BaseWalletClickIntents(), WalletCardClickIntents by walletCardClickIntentsImplementor, WalletWarningsClickIntents by warningsClickIntentsImplementer, @@ -142,10 +138,6 @@ internal class WalletClickIntents @Inject constructor( async { rampStateManager.fetchSellServiceData() }.let(::add) async { fetchHotCryptoUseCase() }.let(::add) - - if (isWalletNFTEnabledSyncUseCase.invoke(userWallet.walletId)) { - async { fetchNFTCollectionsUseCase(userWalletId = userWallet.walletId) }.let(::add) - } } .awaitAll() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt index 9c5fcb1479..b749ffccf3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers -import com.tangem.domain.models.StatusSource import com.tangem.domain.nft.models.* import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM @@ -16,14 +15,8 @@ internal class SetNFTCollectionsTransformer( override fun transform(prevState: WalletState): WalletState = when (prevState) { is WalletState.MultiCurrency.Content -> prevState.copy( nftState = when { - nftCollections.allCollectionsFailed() -> - WalletNFTItemUM.Failed - nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() -> - WalletNFTItemUM.Failed - nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> + nftCollections.allLoadedCollectionsEmpty() -> WalletNFTItemUM.Empty(onItemClick) - !nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> - WalletNFTItemUM.Loading else -> createContentNFTItemUM(onItemClick) }, ) @@ -41,10 +34,6 @@ internal class SetNFTCollectionsTransformer( .map { it.content } .filterIsInstance() - val isFlickering = collectionsContent - .map { it.source } - .any { it == StatusSource.CACHE } - val collections = collectionsContent .mapNotNull { it.collections } .flatten() @@ -65,7 +54,7 @@ internal class SetNFTCollectionsTransformer( collectionsCount = collections.size, assetsCount = collections .sumOf { it.count }, - isFlickering = isFlickering, + isFlickering = false, onItemClick = onItemClick, ) } From d99c4e1405df396a2b50ef6a8b3bcf3fe50d3d0c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 14:31:44 +0500 Subject: [PATCH 094/151] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 10 ++ .../core/ui/components/fields/SearchBar.kt | 2 + .../tangem/data/nft/DefaultNFTRepository.kt | 83 ++++++---- .../tangem/domain/nft/RefreshAllNFTUseCase.kt | 16 ++ .../domain/nft/repository/NFTRepository.kt | 2 + .../entity/NFTCollectionsStateUM.kt | 3 + .../collections/entity/NFTCollectionsUM.kt | 1 + .../ChangeRefreshingStateTransformer.kt | 15 ++ .../transformer/UpdateDataStateTransformer.kt | 29 +++- .../collections/model/NFTCollectionsModel.kt | 37 ++++- .../nft/collections/ui/NFTCollections.kt | 24 ++- .../nft/collections/ui/NFTCollectionsEmpty.kt | 3 + .../collections/ui/NFTCollectionsFailed.kt | 3 + .../collections/ui/NFTCollectionsLoading.kt | 142 ++++++++++++++++++ gradle/tangem_dependencies.toml | 2 +- 15 files changed, 316 insertions(+), 56 deletions(-) create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeRefreshingStateTransformer.kt create mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsLoading.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 de24b448ec..697065bb97 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 @@ -35,6 +35,16 @@ internal object NFTDomainModule { nftRepository = nftRepository, ) + @Provides + @Singleton + fun providesRefreshAllUseCase( + currenciesRepository: CurrenciesRepository, + nftRepository: NFTRepository, + ): RefreshAllNFTUseCase = RefreshAllNFTUseCase( + currenciesRepository = currenciesRepository, + nftRepository = nftRepository, + ) + @Provides @Singleton fun providesFetchNFTCollectionAssetsUseCase(nftRepository: NFTRepository): FetchNFTCollectionAssetsUseCase = diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt index d69616e5d6..a302980668 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/SearchBar.kt @@ -41,6 +41,7 @@ fun SearchBar( state: SearchBarUM, modifier: Modifier = Modifier, colors: TextFieldColors = TangemSearchBarDefaults.defaultTextFieldColors, + enabled: Boolean = true, ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current @@ -57,6 +58,7 @@ fun SearchBar( state.onActiveChange(false) } }, + enabled = enabled, value = state.query, onValueChange = state.onQueryChange, interactionSource = interactionSource, 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 7b806c688a..7fe4317d15 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 @@ -64,38 +64,11 @@ internal class DefaultNFTRepository @Inject constructor( flowOf(NFTCollections.empty(network)) } - override suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) = coroutineScope { - networks.mapNotNull { network -> - if (network.canHandleNFTs()) { - launch(dispatchers.io) { - Either.catch { - expireCollections(userWalletId, network) + override suspend fun refreshCollections(userWalletId: UserWalletId, networks: List) = + refreshCollectionsInternal(userWalletId, networks, refreshAssets = false) - val collections = walletManagersFacade.getNFTCollections(userWalletId, network) - val mergedCollections = collections.mergeWithStoredAssets(userWalletId, network) - - saveCollectionsInRuntime( - userWalletId = userWalletId, - network = network, - collections = mergedCollections, - ) - saveCollectionsInPersistence( - userWalletId = userWalletId, - network = network, - collections = mergedCollections, - ) - }.onLeft { - saveFailedStateInRuntime( - userWalletId = userWalletId, - network = network, - error = it, - ) - } - }.saveIn(getNetworkJobHolder(network)) - } else { - null - } - }.joinAll() + override suspend fun refreshAll(userWalletId: UserWalletId, networks: List) { + refreshCollectionsInternal(userWalletId, networks, refreshAssets = true) } override suspend fun refreshAssets( @@ -162,6 +135,54 @@ internal class DefaultNFTRepository @Inject constructor( assetIdentifier = NFTSdkAssetIdentifierConverter.convertBack(assetIdentifier), ) + private suspend fun refreshCollectionsInternal( + userWalletId: UserWalletId, + networks: List, + refreshAssets: Boolean, + ) = coroutineScope { + networks.mapNotNull { network -> + if (network.canHandleNFTs()) { + launch(dispatchers.io) { + Either.catch { + expireCollections(userWalletId, network) + + val collections = walletManagersFacade.getNFTCollections(userWalletId, network) + val mergedCollections = collections.mergeWithStoredAssets(userWalletId, network) + + saveCollectionsInRuntime( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + saveCollectionsInPersistence( + userWalletId = userWalletId, + network = network, + collections = mergedCollections, + ) + + if (refreshAssets) { + mergedCollections.forEach { collection -> + refreshAssets( + userWalletId = userWalletId, + network = network, + collectionId = NFTSdkCollectionIdentifierConverter.convert(collection.identifier), + ) + } + } + }.onLeft { + saveFailedStateInRuntime( + userWalletId = userWalletId, + network = network, + error = it, + ) + } + }.saveIn(getNetworkJobHolder(network)) + } else { + null + } + }.joinAll() + } + private suspend fun refreshSalePrice( userWalletId: UserWalletId, network: Network, diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt new file mode 100644 index 0000000000..2e365e88a8 --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt @@ -0,0 +1,16 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId + +class RefreshAllNFTUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId) { + val currencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) + nftRepository.refreshAll(userWalletId, currencies.map { it.network }.distinct()) + } +} \ 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 36ca075957..ea99cbca2a 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 @@ -14,6 +14,8 @@ interface NFTRepository { suspend fun refreshAssets(userWalletId: UserWalletId, network: Network, collectionId: NFTCollection.Identifier) + suspend fun refreshAll(userWalletId: UserWalletId, networks: List) + suspend fun isNFTSupported(network: Network): Boolean suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsStateUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsStateUM.kt index 14267e3120..11a6d37285 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsStateUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsStateUM.kt @@ -1,6 +1,9 @@ package com.tangem.features.nft.collections.entity +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig + internal data class NFTCollectionsStateUM( val onBackClick: () -> Unit, + val pullToRefreshConfig: PullToRefreshConfig, val content: NFTCollectionsUM, ) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt index 394144b65d..a8287ca423 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/NFTCollectionsUM.kt @@ -12,6 +12,7 @@ internal sealed class NFTCollectionsUM { ) : NFTCollectionsUM() data class Loading( + val search: SearchBarUM, val onReceiveClick: () -> Unit, ) : NFTCollectionsUM() diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeRefreshingStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeRefreshingStateTransformer.kt new file mode 100644 index 0000000000..7a2face48c --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeRefreshingStateTransformer.kt @@ -0,0 +1,15 @@ +package com.tangem.features.nft.collections.entity.transformer + +import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM +import com.tangem.utils.transformer.Transformer + +internal class ChangeRefreshingStateTransformer( + private val isRefreshing: Boolean, +) : Transformer { + + override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy( + pullToRefreshConfig = prevState.pullToRefreshConfig.copy( + isRefreshing = isRefreshing, + ), + ) +} \ No newline at end of file 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 a0cfab6956..bb684cf18b 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 @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.nft.models.* import com.tangem.features.nft.collections.entity.* @@ -24,8 +25,8 @@ internal class UpdateDataStateTransformer( private val initialSearchBarFactory: () -> SearchBarUM, ) : Transformer { - override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM = prevState.copy( - content = when { + override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM { + val content = when { nftCollections.allCollectionsFailed() -> NFTCollectionsUM.Failed(onRetryClick, onReceiveClick) nftCollections.anyCollectionFailed() && nftCollections.allLoadedCollectionsEmpty() -> @@ -33,7 +34,16 @@ internal class UpdateDataStateTransformer( nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> NFTCollectionsUM.Empty(onReceiveClick) !nftCollections.allCollectionsLoaded() && nftCollections.allCollectionsEmpty() -> - NFTCollectionsUM.Loading(onReceiveClick) + NFTCollectionsUM.Loading( + onReceiveClick = onReceiveClick, + search = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + isActive = false, + onQueryChange = { }, + onActiveChange = { }, + ), + ) else -> { NFTCollectionsUM.Content( search = if (prevState.content is NFTCollectionsUM.Content) { @@ -54,8 +64,17 @@ internal class UpdateDataStateTransformer( onReceiveClick = onReceiveClick, ) } - }, - ) + } + return prevState.copy( + content = content, + pullToRefreshConfig = when (content) { + is NFTCollectionsUM.Loading -> prevState.pullToRefreshConfig.copy( + isRefreshing = false, + ) + else -> prevState.pullToRefreshConfig + }, + ) + } private fun List.transform( state: NFTCollectionsStateUM, 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 2e1ddc7a19..c35c9fc81f 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 @@ -3,15 +3,18 @@ package com.tangem.features.nft.collections.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.ui.components.containers.pullToRefresh.PullToRefreshConfig 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.RefreshAllNFTUseCase 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.* 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 @@ -29,6 +32,7 @@ internal class NFTCollectionsModel @Inject constructor( private val searchManager: InputManager, private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase, + private val refreshAllNFTUseCase: RefreshAllNFTUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -37,7 +41,20 @@ internal class NFTCollectionsModel @Inject constructor( private val _state = MutableStateFlow( value = NFTCollectionsStateUM( onBackClick = params.onBackClick, - content = NFTCollectionsUM.Loading(params.onBackClick), + content = NFTCollectionsUM.Loading( + search = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + isActive = false, + onQueryChange = { }, + onActiveChange = { }, + ), + onReceiveClick = params.onReceiveClick, + ), + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = { onRefresh() }, + ), ), ) @@ -59,7 +76,7 @@ internal class NFTCollectionsModel @Inject constructor( onReceiveClick = { params.onReceiveClick() }, - onRetryClick = ::onRetryClick, + onRetryClick = ::onRefresh, onExpandCollectionClick = ::onExpandCollectionClick, onRetryAssetsClick = ::onRetryAssetsClick, onAssetClick = { asset, collectionName -> @@ -69,9 +86,21 @@ internal class NFTCollectionsModel @Inject constructor( ).transform(it) } } + .onStart { onRefresh() } .launchIn(modelScope) } + private fun onRefresh() { + modelScope.launch { + _state.update { ChangeRefreshingStateTransformer(true).transform(it) } + try { + refreshAllNFTUseCase(params.userWalletId) + } finally { + _state.update { ChangeRefreshingStateTransformer(false).transform(it) } + } + } + } + private fun onSearchQueryChange(newQuery: String) { modelScope.launch { _state.update { UpdateSearchQueryTransformer(newQuery).transform(it) } @@ -111,10 +140,6 @@ internal class NFTCollectionsModel @Inject constructor( loadCollectionAssets(collection) } - private fun onRetryClick() { - // TODO refresh all - } - private fun loadCollectionAssets(collection: NFTCollection) { modelScope.launch { fetchNFTCollectionAssetsUseCase( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt index 9dd7e06ffc..2d42caaf85 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollections.kt @@ -1,12 +1,12 @@ package com.tangem.features.nft.collections.ui import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.* import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.components.containers.pullToRefresh.TangemPullToRefreshContainer import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.nft.collections.entity.NFTCollectionsStateUM @@ -29,19 +29,17 @@ internal fun NFTCollections(state: NFTCollectionsStateUM, modifier: Modifier = M ) }, content = { innerPadding -> - AnimatedContent( - targetState = state.content, - contentKey = { it::class }, - label = "NFT Collections", - ) { - val contentModifier = Modifier + TangemPullToRefreshContainer( + config = state.pullToRefreshConfig, + modifier = Modifier .padding(innerPadding) - .fillMaxSize() - when (val content = it) { - is NFTCollectionsUM.Content -> NFTCollectionsContent(content, contentModifier) - is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content, contentModifier) - is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content, contentModifier) - is NFTCollectionsUM.Loading -> Unit + .fillMaxSize(), + ) { + when (val content = state.content) { + is NFTCollectionsUM.Content -> NFTCollectionsContent(content) + is NFTCollectionsUM.Empty -> NFTCollectionsEmpty(content) + is NFTCollectionsUM.Failed -> NFTCollectionsFailed(content) + is NFTCollectionsUM.Loading -> NFTCollectionsLoading(content) } } }, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt index 1376f56e72..1566f4d125 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsEmpty.kt @@ -3,6 +3,8 @@ package com.tangem.features.nft.collections.ui import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -23,6 +25,7 @@ internal fun NFTCollectionsEmpty(state: NFTCollectionsUM.Empty, modifier: Modifi Box( modifier = modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding(TangemTheme.dimens.spacing16), ) { Column( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt index d4d99bc548..2020fc02c6 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsFailed.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,6 +24,7 @@ internal fun NFTCollectionsFailed(state: NFTCollectionsUM.Failed, modifier: Modi Box( modifier = modifier .fillMaxSize() + .verticalScroll(rememberScrollState()) .padding(TangemTheme.dimens.spacing16), ) { UnableToLoadData( diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsLoading.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsLoading.kt new file mode 100644 index 0000000000..0a50c87c68 --- /dev/null +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/ui/NFTCollectionsLoading.kt @@ -0,0 +1,142 @@ +package com.tangem.features.nft.collections.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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 com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.fields.SearchBar +import com.tangem.core.ui.components.fields.TangemSearchBarDefaults +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.nft.collections.entity.NFTCollectionsUM +import com.tangem.features.nft.impl.R + +private const val SHIMMER_ITEMS_COUNT = 5 + +@Composable +internal fun NFTCollectionsLoading(state: NFTCollectionsUM.Loading, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(TangemTheme.dimens.spacing16), + ) { + Column { + SearchBar( + state = state.search, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + enabled = false, + ) + Card( + modifier = modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing16, + ), + shape = RoundedCornerShape(TangemTheme.dimens.radius16), + colors = CardDefaults.cardColors( + containerColor = TangemTheme.colors.background.primary, + contentColor = TangemTheme.colors.text.primary1, + disabledContainerColor = TangemTheme.colors.background.primary, + disabledContentColor = TangemTheme.colors.text.primary1, + ), + ) { + Column { + repeat(SHIMMER_ITEMS_COUNT) { + CollectionPlaceholder() + } + } + } + } + PrimaryButton( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter), + text = stringResourceSafe(R.string.nft_collections_receive), + onClick = state.onReceiveClick, + ) + } +} + +@Composable +private fun CollectionPlaceholder(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .padding( + horizontal = TangemTheme.dimens.spacing12, + vertical = TangemTheme.dimens.spacing16, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Box( + contentAlignment = Alignment.Center, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens.size36) + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) + .background(TangemTheme.colors.field.primary), + ) + } + Column( + modifier = Modifier + .padding( + horizontal = TangemTheme.dimens.spacing12, + ) + .weight(1f), + horizontalAlignment = Alignment.Start, + ) { + TextShimmer( + modifier = Modifier.width(TangemTheme.dimens.size110), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + TextShimmer( + modifier = Modifier + .width(TangemTheme.dimens.size80) + .padding(top = TangemTheme.dimens.spacing4), + style = TangemTheme.typography.caption2, + textSizeHeight = true, + ) + } + } + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_NFTCollectionsLoading() { + TangemThemePreview { + NFTCollectionsLoading( + state = NFTCollectionsUM.Loading( + search = SearchBarUM( + placeholderText = resourceReference(R.string.common_search), + query = "", + isActive = false, + onQueryChange = { }, + onActiveChange = { }, + ), + onReceiveClick = { }, + ), + ) + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 0c3b257143..a0bc01da42 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-1041" +tangemBlockchainSdk = "develop-1042" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 5bc59f3789a71486099527601b8dcd1ef2476366 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 13:58:30 +0300 Subject: [PATCH 095/151] Updated on 2026-08-14 --- .../datasource/di/StakingStoreModule.kt | 29 ++++++---- .../staking/di/YieldBalanceFetcherModule.kt | 24 ++++++++ .../di/YieldBalanceProducerFactoryModule.kt | 28 ++++++++++ .../staking/di/YieldBalanceSupplierModule.kt | 56 +++++++++++++++++++ .../staking/model/stakekit/YieldBalance.kt | 10 ++++ 5 files changed, 137 insertions(+), 10 deletions(-) create mode 100644 data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt create mode 100644 data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index 3d632eaf97..a631c02284 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.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 @@ -49,21 +50,29 @@ internal object StakingStoreModule { @Provides @Singleton - fun provideStakingBalanceStore( + fun provideYieldsBalancesPersistenceStore( @NetworkMoshi moshi: Moshi, @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, + ): DataStore>> { + return DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(valueTypes = setTypes()), + defaultValue = emptyMap(), + ), + produceFile = { context.dataStoreFile(fileName = "yield_balances") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ) + } + + @Provides + @Singleton + fun provideStakingBalanceStore( + persistenceStore: DataStore>>, ): StakingBalanceStore { return DefaultStakingBalanceStore( - persistenceStore = DataStoreFactory.create( - serializer = MoshiDataStoreSerializer( - moshi = moshi, - types = mapWithStringKeyTypes(valueTypes = setTypes()), - defaultValue = emptyMap(), - ), - produceFile = { context.dataStoreFile(fileName = "yield_balances") }, - scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), - ), + persistenceStore = persistenceStore, runtimeStore = RuntimeSharedStore(), ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt new file mode 100644 index 0000000000..9224d037e3 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceFetcherModule.kt @@ -0,0 +1,24 @@ +package com.tangem.data.staking.di + +import com.tangem.data.staking.multi.DefaultMultiYieldBalanceFetcher +import com.tangem.data.staking.single.DefaultSingleYieldBalanceFetcher +import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher +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 YieldBalanceFetcherModule { + + @Binds + @Singleton + fun bindSingleYieldBalanceFetcher(impl: DefaultSingleYieldBalanceFetcher): SingleYieldBalanceFetcher + + @Binds + @Singleton + fun bindMultiYieldBalanceFetcher(impl: DefaultMultiYieldBalanceFetcher): MultiYieldBalanceFetcher +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt new file mode 100644 index 0000000000..5f737af9ee --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceProducerFactoryModule.kt @@ -0,0 +1,28 @@ +package com.tangem.data.staking.di + +import com.tangem.data.staking.multi.DefaultMultiYieldBalanceProducer +import com.tangem.data.staking.single.DefaultSingleYieldBalanceProducer +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.domain.staking.single.SingleYieldBalanceProducer +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 YieldBalanceProducerFactoryModule { + + @Binds + @Singleton + fun bindSingleYieldBalanceProducerFactory( + impl: DefaultSingleYieldBalanceProducer.Factory, + ): SingleYieldBalanceProducer.Factory + + @Binds + @Singleton + fun bindMultiYieldBalanceProducerFactory( + impl: DefaultMultiYieldBalanceProducer.Factory, + ): MultiYieldBalanceProducer.Factory +} \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt new file mode 100644 index 0000000000..599d495859 --- /dev/null +++ b/data/staking/src/main/java/com/tangem/data/staking/di/YieldBalanceSupplierModule.kt @@ -0,0 +1,56 @@ +package com.tangem.data.staking.di + +import androidx.datastore.core.DataStore +import com.tangem.data.staking.store.DefaultYieldsBalancesStore +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.domain.staking.multi.MultiYieldBalanceProducer +import com.tangem.domain.staking.multi.MultiYieldBalanceSupplier +import com.tangem.domain.staking.single.SingleYieldBalanceProducer +import com.tangem.domain.staking.single.SingleYieldBalanceSupplier +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 YieldBalanceSupplierModule { + + @Provides + @Singleton + fun provideYieldsBalancesStore( + persistenceStore: DataStore>>, + dispatchers: CoroutineDispatcherProvider, + ): YieldsBalancesStore { + return DefaultYieldsBalancesStore( + runtimeStore = RuntimeSharedStore(), + persistenceStore = persistenceStore, + dispatchers = dispatchers, + ) + } + + @Provides + @Singleton + fun provideSingleYieldBalanceSupplier(factory: SingleYieldBalanceProducer.Factory): SingleYieldBalanceSupplier { + return object : SingleYieldBalanceSupplier( + factory = factory, + keyCreator = { + "single_yield_balance_${it.userWalletId.stringValue}_${it.network.id.value}_" + + it.network.derivationPath.value + }, + ) {} + } + + @Provides + @Singleton + fun provideMultiYieldBalanceSupplier(factory: MultiYieldBalanceProducer.Factory): MultiYieldBalanceSupplier { + return object : MultiYieldBalanceSupplier( + factory = factory, + keyCreator = { "multi_yields_balances_${it.userWalletId.stringValue}" }, + ) {} + } +} \ 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 74d271b455..fc57d56fce 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 @@ -1,6 +1,7 @@ package com.tangem.domain.staking.model.stakekit import com.tangem.domain.models.StatusSource +import com.tangem.domain.staking.model.StakingID import com.tangem.domain.staking.model.stakekit.action.StakingActionType import org.joda.time.DateTime import java.math.BigDecimal @@ -19,6 +20,15 @@ sealed class YieldBalance { } } + fun getStakingId(): StakingID? { + val integrationId = integrationId + val address = address + + if (integrationId == null || address == null) return null + + return StakingID(integrationId = integrationId, address = address) + } + data class Data( override val integrationId: String?, override val address: String, From d6bf0992d7860d69efea742fdb9d9ca612f86dc0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 18:30:36 +0300 Subject: [PATCH 096/151] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 5 ++ .../data/staking/DefaultStakingRepository.kt | 56 +++++++++------ .../data/staking/di/StakingDataModule.kt | 10 ++- .../multi/DefaultMultiYieldBalanceFetcher.kt | 3 +- .../DefaultSingleYieldBalanceFetcher.kt | 3 +- .../store/DefaultYieldsBalancesStore.kt | 60 ++++++++++------ .../data/staking/store/YieldsBalancesStore.kt | 6 ++ .../YieldsBalancesStoreUpdateMethodsTest.kt | 15 +--- .../staking/repositories/StakingRepository.kt | 9 ++- .../BaseCurrencyStatusOperations.kt | 45 ++++++++---- .../CachedCurrenciesStatusesOperations.kt | 70 ++++++++++++++++--- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 1 + .../repository/MockStakingRepository.kt | 11 ++- 13 files changed, 209 insertions(+), 85 deletions(-) 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 695196deaf..075ab6d9f7 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 @@ -11,6 +11,7 @@ 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 +import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations @@ -409,6 +410,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, singleQuoteSupplier: SingleQuoteSupplier, + singleYieldBalanceSupplier: SingleYieldBalanceSupplier, ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -421,6 +423,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteFetcher = multiQuoteFetcher, singleQuoteSupplier = singleQuoteSupplier, + singleYieldBalanceSupplier = singleYieldBalanceSupplier, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -439,6 +442,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, singleQuoteSupplier: SingleQuoteSupplier, + singleYieldBalanceSupplier: SingleYieldBalanceSupplier, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -451,6 +455,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteFetcher = multiQuoteFetcher, singleQuoteSupplier = singleQuoteSupplier, + singleYieldBalanceSupplier = singleYieldBalanceSupplier, tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 3ccb9e47aa..03e1b51935 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -24,6 +24,8 @@ import com.tangem.data.staking.converters.transaction.GasEstimateConverter import com.tangem.data.staking.converters.transaction.StakingTransactionConverter import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter import com.tangem.data.staking.converters.transaction.StakingTransactionTypeConverter +import com.tangem.data.staking.store.YieldsBalancesStore +import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.data.staking.utils.StakingIdFactory.Companion.integrationIdMap import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.getOrThrow @@ -65,7 +67,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.plus import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber @@ -76,11 +77,13 @@ internal class DefaultStakingRepository( private val stakeKitApi: StakeKitApi, private val stakingYieldsStore: StakingYieldsStore, private val stakingBalanceStore: StakingBalanceStore, + private val stakingBalanceStoreV2: YieldsBalancesStore, private val cacheRegistry: CacheRegistry, private val dispatchers: CoroutineDispatcherProvider, private val walletManagersFacade: WalletManagersFacade, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, + private val stakingIdFactory: StakingIdFactory, moshi: Moshi, ) : StakingRepository { @@ -429,7 +432,7 @@ internal class DefaultStakingRepository( } }.cancellable() - override suspend fun getSingleYieldBalanceSync( + override suspend fun getSingleYieldBalanceSyncLegacy( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): YieldBalance = withContext(dispatchers.io) { @@ -447,6 +450,20 @@ internal class DefaultStakingRepository( ?: YieldBalance.Error(integrationId, address) } + override suspend fun getSingleYieldBalanceSync( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): YieldBalance { + val stakingId = stakingIdFactory.createForDefault( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ) ?: error("Could not create stakingId") + + return stakingBalanceStoreV2.getSyncOrNull(userWalletId = userWalletId, stakingId = stakingId) + ?: YieldBalance.Error(integrationId = stakingId.integrationId, address = stakingId.address) + } + @Suppress("LongMethod") override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, @@ -559,26 +576,7 @@ internal class DefaultStakingRepository( } } - override fun getMultiYieldBalanceUpdatesLegacy( - userWalletId: UserWalletId, - cryptoCurrencies: List, - ): Flow = channelFlow { - stakingBalanceStore.get( - userWalletId = userWalletId, - stakingIds = cryptoCurrencies.mapStakingId(userWalletId), - ) - .onEach { - val balances = YieldBalanceListConverter.convert(it) - send(balances) - } - .launchIn(scope = this + dispatchers.io) - - withContext(dispatchers.io) { - fetchMultiYieldBalance(userWalletId, cryptoCurrencies, refresh = false) - } - } - - override suspend fun getMultiYieldBalanceSync( + override suspend fun getMultiYieldBalanceSyncLegacy( userWalletId: UserWalletId, cryptoCurrencies: List, ): YieldBalanceList = withContext(dispatchers.io) { @@ -589,6 +587,20 @@ internal class DefaultStakingRepository( ?: YieldBalanceList.Error } + override suspend fun getMultiYieldBalanceSync( + userWalletId: UserWalletId, + cryptoCurrencies: List, + ): YieldBalanceList { + val stakingIds = cryptoCurrencies.flatMap { + stakingIdFactory.create(userWalletId = userWalletId, currencyId = it.id, network = it.network) + } + + return stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) + ?.filter { it.getStakingId() in stakingIds } + ?.let { YieldBalanceListConverter.convert(value = it.toSet()) } + ?: YieldBalanceList.Error + } + override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { return withContext(dispatchers.io) { stakingBalanceStore.getSyncOrNull(userWalletId) diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index 43c7003ba5..67dbb87a81 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -9,7 +9,9 @@ import com.tangem.data.staking.DefaultStakingErrorResolver import com.tangem.data.staking.DefaultStakingRepository import com.tangem.data.staking.DefaultStakingTransactionHashRepository import com.tangem.data.staking.converters.error.StakeKitErrorConverter +import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.toggles.DefaultStakingFeatureToggles +import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.error.StakeKitErrorResponse import com.tangem.datasource.di.NetworkMoshi @@ -41,23 +43,27 @@ internal object StakingDataModule { stakeKitApi: StakeKitApi, stakingYieldsStore: StakingYieldsStore, stakingBalanceStore: StakingBalanceStore, + yieldsBalancesStore: YieldsBalancesStore, cacheRegistry: CacheRegistry, dispatchers: CoroutineDispatcherProvider, walletManagersFacade: WalletManagersFacade, getUserWalletUseCase: GetUserWalletUseCase, stakingFeatureToggles: StakingFeatureToggles, + stakingIdFactory: StakingIdFactory, @NetworkMoshi moshi: Moshi, ): StakingRepository { return DefaultStakingRepository( stakeKitApi = stakeKitApi, stakingYieldsStore = stakingYieldsStore, stakingBalanceStore = stakingBalanceStore, + stakingBalanceStoreV2 = yieldsBalancesStore, cacheRegistry = cacheRegistry, dispatchers = dispatchers, walletManagersFacade = walletManagersFacade, getUserWalletUseCase = getUserWalletUseCase, stakingFeatureToggles = stakingFeatureToggles, moshi = moshi, + stakingIdFactory = stakingIdFactory, ) } @@ -89,7 +95,7 @@ internal object StakingDataModule { @Provides @Singleton - internal fun provideStakingErrorResolver( + fun provideStakingErrorResolver( @NetworkMoshi moshi: Moshi, analyticsEventHandler: AnalyticsEventHandler, ): StakingErrorResolver { @@ -102,7 +108,7 @@ internal object StakingDataModule { @Provides @Singleton - internal fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles { + fun provideFeatureToggles(featureTogglesManager: FeatureTogglesManager): StakingFeatureToggles { return DefaultStakingFeatureToggles(featureTogglesManager) } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt index 45cccaea38..7a5e721099 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt @@ -10,6 +10,7 @@ import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject /** * Default implementation of [MultiYieldBalanceFetcher] @@ -22,7 +23,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider * [REDACTED_AUTHOR] */ -internal class DefaultMultiYieldBalanceFetcher( +internal class DefaultMultiYieldBalanceFetcher @Inject constructor( private val stakingYieldsStore: StakingYieldsStore, private val yieldsBalancesStore: YieldsBalancesStore, private val stakingIdFactory: StakingIdFactory, diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt index 37d48ac768..a59151f771 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt @@ -11,6 +11,7 @@ import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import javax.inject.Inject /** * Default implementation of [MultiYieldBalanceFetcher] @@ -23,7 +24,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider * [REDACTED_AUTHOR] */ -internal class DefaultSingleYieldBalanceFetcher( +internal class DefaultSingleYieldBalanceFetcher @Inject constructor( private val stakingYieldsStore: StakingYieldsStore, private val yieldsBalancesStore: YieldsBalancesStore, private val stakingIdFactory: StakingIdFactory, 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 index 18fb91c240..52a301247f 100644 --- 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 @@ -58,14 +58,22 @@ internal class DefaultYieldsBalancesStore( return runtimeStore.get().mapNotNull { it[userWalletId] } } + override suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? { + return runtimeStore.getSyncOrNull() + ?.get(userWalletId) + ?.firstOrNull { stakingId == it.getStakingId() } + } + + override suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? { + return runtimeStore.getSyncOrNull()?.get(userWalletId) + } + override suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) { - updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = setOf(stakingId)) { - it.copySealed(source = StatusSource.CACHE) - } + refresh(userWalletId = userWalletId, stakingIds = setOf(stakingId)) } override suspend fun refresh(userWalletId: UserWalletId, stakingIds: Set) { - updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) { + updateInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) { it.copySealed(source = StatusSource.CACHE) } } @@ -78,9 +86,12 @@ internal class DefaultYieldsBalancesStore( } override suspend fun storeError(userWalletId: UserWalletId, stakingIds: Set) { - updateBalancesInRuntime(userWalletId = userWalletId, stakingIds = stakingIds) { - it.copySealed(source = StatusSource.ONLY_CACHE) - } + updateInRuntime( + userWalletId = userWalletId, + stakingIds = stakingIds, + ifNotFound = ::createErrorYieldBalance, + update = { it.copySealed(source = StatusSource.ONLY_CACHE) }, + ) } private suspend fun storeInRuntime(userWalletId: UserWalletId, values: Set) { @@ -89,9 +100,7 @@ internal class DefaultYieldsBalancesStore( runtimeStore.update(default = emptyMap()) { saved -> saved.toMutableMap().apply { this[userWalletId] = saved[userWalletId] - ?.addOrReplace(newBalances) { old, new -> - old.integrationId == new.integrationId && old.address == new.address - } + ?.addOrReplace(newBalances) { old, new -> old.getStakingId() == new.getStakingId() } ?: newBalances } } @@ -101,33 +110,33 @@ internal class DefaultYieldsBalancesStore( 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 - } + ?.addOrReplace(items = values) { old, new -> old.getStakingId() == new.getStakingId() } ?: values } } } - private suspend fun updateBalancesInRuntime( + private suspend fun updateInRuntime( userWalletId: UserWalletId, stakingIds: Set, + ifNotFound: (StakingID) -> YieldBalance? = { null }, update: (YieldBalance) -> YieldBalance, ) { runtimeStore.update(default = emptyMap()) { stored -> stored.toMutableMap().apply { val portfolioBalances = stored[userWalletId].orEmpty() - val balances = stakingIds.mapTo(hashSetOf()) { stakingId -> - val balance = portfolioBalances.firstOrNull { - it.integrationId == stakingId.integrationId && it.address == stakingId.address - } + val balances = stakingIds.mapNotNullTo(hashSetOf()) { stakingId -> + val balance = portfolioBalances + .firstOrNull { stakingId == it.getStakingId() } + ?: ifNotFound(stakingId) + ?: return@mapNotNullTo null - if (balance != null) update(balance) else createDefaultBalance(id = stakingId) + update(balance) } val updatedBalances = portfolioBalances.addOrReplace(items = balances) { old, new -> - old.integrationId == new.integrationId && old.address == new.address + old.getStakingId() == new.getStakingId() } put(key = userWalletId, value = updatedBalances) @@ -135,7 +144,16 @@ internal class DefaultYieldsBalancesStore( } } - private fun createDefaultBalance(id: StakingID): YieldBalance { + private fun createErrorYieldBalance(id: StakingID): YieldBalance { return YieldBalance.Error(integrationId = id.integrationId, address = id.address) } + + private fun YieldBalanceWrapperDTO.getStakingId(): StakingID? { + val integrationId = integrationId + val address = addresses.address + + if (integrationId == null || address.isBlank()) return null + + return StakingID(integrationId = integrationId, address = 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 index 326dc6fc73..4549a72ede 100644 --- 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 @@ -16,6 +16,12 @@ interface YieldsBalancesStore { /** Get flow of [YieldBalance]'s set by [userWalletId] */ fun get(userWalletId: UserWalletId): Flow> + /** Get [YieldBalance] by [userWalletId] and [stakingId] synchronously or null */ + suspend fun getSyncOrNull(userWalletId: UserWalletId, stakingId: StakingID): YieldBalance? + + /** Get all [YieldBalance] by [userWalletId] synchronously or null */ + suspend fun getAllSyncOrNull(userWalletId: UserWalletId): Set? + /** Refresh balance of [stakingId] by [userWalletId] */ suspend fun refresh(userWalletId: UserWalletId, stakingId: StakingID) 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 index dc05b3db34..8e6650c1e1 100644 --- 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 @@ -33,14 +33,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { 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, - ), - ), - ) + val runtimeExpected = mapOf(userWalletId to emptySet()) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) @@ -70,11 +63,7 @@ internal class YieldsBalancesStoreUpdateMethodsTest { 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) - }, - ) + val runtimeExpected = mapOf(userWalletId to emptySet()) Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt index d97ac0f2b9..5f5da8eeb2 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakingRepository.kt @@ -52,6 +52,11 @@ interface StakingRepository { fun getSingleYieldBalanceFlow(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Flow + suspend fun getSingleYieldBalanceSyncLegacy( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): YieldBalance + suspend fun getSingleYieldBalanceSync(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): YieldBalance suspend fun fetchMultiYieldBalance( @@ -65,10 +70,10 @@ interface StakingRepository { cryptoCurrencies: List, ): Flow - fun getMultiYieldBalanceUpdatesLegacy( + suspend fun getMultiYieldBalanceSyncLegacy( userWalletId: UserWalletId, cryptoCurrencies: List, - ): Flow + ): YieldBalanceList suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, 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 14c18382e1..fed0750cf9 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 @@ -16,6 +16,8 @@ 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 +import com.tangem.domain.staking.single.SingleYieldBalanceProducer +import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.* import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error @@ -46,6 +48,7 @@ abstract class BaseCurrencyStatusOperations( private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, private val singleQuoteSupplier: SingleQuoteSupplier, + private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -359,10 +362,18 @@ abstract class BaseCurrencyStatusOperations( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): EitherFlow { - return stakingRepository.getSingleYieldBalanceFlow( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - ).map> { it.right() } + return if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + singleYieldBalanceSupplier( + params = SingleYieldBalanceProducer.Params( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ), + ) + } else { + stakingRepository.getSingleYieldBalanceFlow(userWalletId = userWalletId, cryptoCurrency = cryptoCurrency) + } + .map> { it.right() } .catch { emit(Error.DataError(it).left()) } .onEmpty { emit(Error.EmptyYieldBalances.left()) } } @@ -396,10 +407,18 @@ abstract class BaseCurrencyStatusOperations( ): Either { return catch( block = { - stakingRepository.getMultiYieldBalanceSync( - userWalletId, - cryptoCurrencies, - ).right() + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + stakingRepository.getMultiYieldBalanceSync( + userWalletId = userWalletId, + cryptoCurrencies = cryptoCurrencies, + ) + } else { + stakingRepository.getMultiYieldBalanceSyncLegacy( + userWalletId = userWalletId, + cryptoCurrencies = cryptoCurrencies, + ) + } + .right() }, catch = { Error.EmptyYieldBalances.left() @@ -413,10 +432,12 @@ abstract class BaseCurrencyStatusOperations( ): Either { return catch( block = { - stakingRepository.getSingleYieldBalanceSync( - userWalletId, - cryptoCurrency, - ).right() + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + stakingRepository.getSingleYieldBalanceSync(userWalletId, cryptoCurrency) + } else { + stakingRepository.getSingleYieldBalanceSyncLegacy(userWalletId, cryptoCurrency) + } + .right() }, catch = { Error.EmptyYieldBalances.left() 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 919ee39358..c222b79fec 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 @@ -21,6 +21,8 @@ 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 +import com.tangem.domain.staking.single.SingleYieldBalanceProducer +import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.* @@ -46,6 +48,7 @@ class CachedCurrenciesStatusesOperations( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteFetcher: MultiQuoteFetcher, private val singleQuoteSupplier: SingleQuoteSupplier, + private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations( @@ -57,6 +60,7 @@ class CachedCurrenciesStatusesOperations( multiNetworkStatusSupplier = multiNetworkStatusSupplier, singleNetworkStatusSupplier = singleNetworkStatusSupplier, singleQuoteSupplier = singleQuoteSupplier, + singleYieldBalanceSupplier = singleYieldBalanceSupplier, tokensFeatureToggles = tokensFeatureToggles, ) { @@ -353,15 +357,19 @@ class CachedCurrenciesStatusesOperations( userWalletId: UserWalletId, cryptoCurrencies: List, ): EitherFlow { - return stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies) - .map> { 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.isStakingLoadingRefactoringEnabled) { + getYieldsBalancesUpdates(userWalletId, cryptoCurrencies) + } else { + stakingRepository.getMultiYieldBalanceUpdates(userWalletId, cryptoCurrencies) + .map> { it.right() } + .retryWhen { cause, _ -> + emit(TokenListError.DataError(cause).left()) + // adding delay before retry to avoid spam when flow restarted + delay(RETRY_DELAY) + true + } + .distinctUntilChanged() + } } // temporary code because token list is built using networks list @@ -423,6 +431,50 @@ class CachedCurrenciesStatusesOperations( .distinctUntilChanged() } + // temporary code because token list is built using networks list + private fun getYieldsBalancesUpdates( + userWalletId: UserWalletId, + cryptoCurrencies: List, + ): EitherFlow { + return channelFlow { + val state = MutableStateFlow(emptyList()) + + cryptoCurrencies.onEach { + launch { + singleYieldBalanceSupplier( + params = SingleYieldBalanceProducer.Params( + userWalletId = userWalletId, + currencyId = it.id, + network = it.network, + ), + ) + .onEach { balance -> + state.update { loadedBalances -> + loadedBalances.addOrReplace(balance) { + it.integrationId == balance.integrationId && it.address == balance.address + } + } + } + .launchIn(scope = this) + } + } + + state + .onEach(::send) + .launchIn(scope = this) + } + .map, Either> { balances -> + val yieldBalanceList = if (balances.isEmpty()) { + YieldBalanceList.Empty + } else { + YieldBalanceList.Data(balances) + } + + yieldBalanceList.right() + } + .distinctUntilChanged() + } + private fun isFetchingStarted(userWalletId: UserWalletId): Boolean { return fetchingState.value[userWalletId]?.let { it.isStarted() || it.isFinished() } ?: 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 841fbf11ab..e084456c8c 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 @@ -189,6 +189,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { multiQuoteFetcher = mockk(), singleQuoteSupplier = mockk(), quotesRepositoryV2 = mockk(), + singleYieldBalanceSupplier = mockk(), ), dispatchers = dispatchers, ) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index 04be38d010..51816af928 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -72,6 +72,11 @@ class MockStakingRepository : StakingRepository { send(YieldBalance.Error(integrationId = null, address = null)) } + override suspend fun getSingleYieldBalanceSyncLegacy( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): YieldBalance = YieldBalance.Error(integrationId = null, address = null) + override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, @@ -96,10 +101,12 @@ class MockStakingRepository : StakingRepository { ) } - override fun getMultiYieldBalanceUpdatesLegacy( + override suspend fun getMultiYieldBalanceSyncLegacy( userWalletId: UserWalletId, cryptoCurrencies: List, - ): Flow = flowOf() + ): YieldBalanceList = YieldBalanceList.Data( + balances = listOf(YieldBalance.Error(integrationId = null, address = null)), + ) override suspend fun getMultiYieldBalanceSync( userWalletId: UserWalletId, From 6936f04093e0b4eb0aab452d50e339d7971c424a Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 16:59:42 +0300 Subject: [PATCH 097/151] Updated on 2026-08-14 --- .../main/res/values-de/strings-blockchain.xml | 25 ------- core/res/src/main/res/values-de/strings.xml | 49 +++++++++++++ .../main/res/values-es/strings-blockchain.xml | 25 ------- core/res/src/main/res/values-es/strings.xml | 28 ++++++++ .../main/res/values-fr/strings-blockchain.xml | 25 ------- core/res/src/main/res/values-fr/strings.xml | 28 ++++++++ .../main/res/values-it/strings-blockchain.xml | 17 ----- core/res/src/main/res/values-it/strings.xml | 29 ++++++++ .../main/res/values-ja/strings-blockchain.xml | 25 ------- core/res/src/main/res/values-ja/strings.xml | 71 +++++++++++++++++++ .../main/res/values-ru/strings-blockchain.xml | 25 ------- core/res/src/main/res/values-ru/strings.xml | 31 ++++++++ .../res/values-uk-rUA/strings-blockchain.xml | 25 ------- .../src/main/res/values-uk-rUA/strings.xml | 28 ++++++++ .../res/values-zh-rTW/strings-blockchain.xml | 25 ------- .../src/main/res/values-zh-rTW/strings.xml | 29 ++++++++ .../main/res/values/strings-blockchain.xml | 25 ------- core/res/src/main/res/values/strings.xml | 42 ++++++++++- 18 files changed, 334 insertions(+), 218 deletions(-) delete mode 100644 core/res/src/main/res/values-de/strings-blockchain.xml delete mode 100644 core/res/src/main/res/values-es/strings-blockchain.xml delete mode 100644 core/res/src/main/res/values-fr/strings-blockchain.xml delete mode 100644 core/res/src/main/res/values-it/strings-blockchain.xml delete mode 100644 core/res/src/main/res/values-ja/strings-blockchain.xml delete mode 100644 core/res/src/main/res/values-ru/strings-blockchain.xml delete mode 100644 core/res/src/main/res/values-uk-rUA/strings-blockchain.xml delete mode 100644 core/res/src/main/res/values-zh-rTW/strings-blockchain.xml delete mode 100644 core/res/src/main/res/values/strings-blockchain.xml diff --git a/core/res/src/main/res/values-de/strings-blockchain.xml b/core/res/src/main/res/values-de/strings-blockchain.xml deleted file mode 100644 index adea1fc8ea..0000000000 --- a/core/res/src/main/res/values-de/strings-blockchain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - Standard - Altbestand - Erhalt der Gebühr fehlgeschlagen - Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren. - Nicht genug Geld für die Transaktion. Bitte lade dein Konto auf. - Es ist ein Fehler aufgetreten. Code: %s. - Um das %1$s -Netzwerk nutzen zu können, must du die Kontoreserve ( %2$s %3$s ) bezahlen, die diesen Betrag auf unbestimmte Zeit sperrt und verbirgt - Das Zielkonto ist nicht aktiv. Sende %s oder mehr, um das Konto zu aktivieren. - Senden Sie Geld an diese Andresse um ein Konto zu erstellen - Minimaler Betrag ist %s - Die Veränderung ist zu gering - Falsche Gebühr - Mindestguthaben ist %s - Das Zielkonto ist nicht erstellt. Der abzusendende Betrag soll %s + Gebühr oder mehr sein - Unbekannter Fehler - Der Betrag geht über die Bilanz hinaus - Ungültiger Betrag - Die Gebühr geht über die Bilanz hinaus - Der Gesamtbetrag geht über die Bilanz hinaus - Nein, alles senden - Um %s XTZ reduziert - Damit Sie beim nächsten Aufladen Ihrer Brieftasche keine erhöhte Provision zahlen, soll der Betrag um %s XTZ reduziert werden - diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 7d9d38c56c..67aa4da4df 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -21,6 +21,8 @@ Token anlegen Token verwalten Sende nur %1$s ( %2$s ) vom %3$s -Netzwerk an diese Adresse. Die Verwendung anderer Token und Netzwerke kann zum Verlust von Geldern führen. + Standard + Altbestand So scannt man Hilfe anfordern Erneut versuchen @@ -83,6 +85,7 @@ Cardano-Transaktionsanforderungen Um eine %1$s Transaktion durchzuführen, musst du einen gewissen ADA-Betrag einzahlen, um die Netzwerkgebühr und den ADA-Mindestwert zu decken (5 ADA werden empfohlen). Nicht genügend ADA für die Tokenübertrag + Betrag absenden, die Änderung darf nicht unter 1 ADA liegen Du musst einige ADA verwalten, weil du einige Token auf der Cardano-Blockchain besitzt Nicht genug ADA Akzeptieren @@ -100,6 +103,7 @@ Saldo biometrische Authentifizierung biometrische Daten + Transaktion fehlgeschlagen Kaufen Gehe zu %1$s D hast keinen Zugang zur Kamera erteilt, bitte passe deine Datenschutzeinstellungen an @@ -110,6 +114,8 @@ Stakingbelohnungen beanspruchen Schließen Bestätigen + Kontakt zum Tangem-Support + Kontakt zum Visa-Support Weiter Kopieren Adresse kopieren @@ -135,6 +141,7 @@ Erkunden Transaktionsverlauf einsehen Explorer + Erhalt der Gebühr fehlgeschlagen Netzwerkgebühren sind Gebühren, die Nutzer für die Verarbeitung und Bestätigung von Transaktionen zahlen. Die Höhe der Gebühren kann von der Überlastung des Netzes, der Größe der Transaktion und der Ausführungspriorität abhängen. %s Schnell Markt @@ -156,6 +163,7 @@ Netzgebühr Der überwiesene Betrag wird um %1$s (%2$s) gekürzt, um die gewählte Gebührenhöhe zu decken. Weiter + NFT Nein Keine Adresse Jetzt @@ -178,10 +186,12 @@ Suchen Token suchen sek. + Alles einblenden Seed phrase Aktion auswählen Verkaufen Senden + Absenden der Transaktion fehlgeschlagen Der Server ist nicht verfügbar. Bitte versuche es später erneut. Teilen Link teilen @@ -205,6 +215,7 @@ Es ist ein Fehler aufgetreten. Bitte versuche es erneut. Nicht erreichbar Staking beenden + Aufgrund der Beschränkungen von %1$s können nur %2$d UTXOs in eine einzige Transaktion passen. Das bedeutet, dass du nur %3$s oder weniger senden kannst. Du musst den Betrag reduzieren. woche mit Ja @@ -261,6 +272,7 @@ Überprüfe deine Internetverbindung oder wechseln zu einem anderen Netzwerk Nutzungsbedingungen Du hast eine Karte oder Ring aus einer anderen Wallet verwendet. Tippe auf die Karte oder Ring, die dieser Wallet zugeordnet ist. + Nicht genug Geld für die Transaktion. Bitte lade dein Konto auf. Meine Token Du hast noch keine Token hinzugefügt. Füge Token über den Markt zum Tausch hinzu Kann nicht gegen %s ausgetauscht werden @@ -340,6 +352,9 @@ Feedback zu Tangem Eine Transaktion kann nicht gesendet werden Fehler in der Coinbeschreibung + Es ist ein Fehler aufgetreten + Es ist ein Fehler aufgetreten. Code: %s. + Memo erforderlich Nur diese Gib das Genehmigungslimit für das ausgewählte Token an Betrag %s @@ -509,10 +524,28 @@ NFT-Kollektionen Einige Daten werden möglicherweise nicht geladen Vorübergehende Ladeprobleme + Basisinformationen + Netzwerk + Vertragsadresse + Letzter Verkaufspreis + Raritätsetikett + Seltenheitsgrad + Token-Adresse + Token-ID + Token-Standard + Eigenschaften + Keine Ergebnisse. Bitte versuche eine andere Anfrage. + Netzwerk auswählen + NFT erhalten + Eigenschaften %1$d NFTs in der %2$d Sammlung Tippe hier, um das erste NFT zu erhalten NFT-Sammlungen Die Daten konnten nicht geladen werden + Um das %1$s -Netzwerk nutzen zu können, must du die Kontoreserve ( %2$s %3$s ) bezahlen, die diesen Betrag auf unbestimmte Zeit sperrt und verbirgt + Das Zielkonto ist nicht aktiv. Sende %s oder mehr, um das Konto zu aktivieren. + Senden Sie Geld an diese Andresse um ein Konto zu erstellen + Das Zielkonto verfügt nicht über eine Vertrauensstellung für das gesendete Asset. Du musst einen einzigen Zugangscode einrichten, um alle deine Geräte zu schützen Schützen Du kannst später auf jeder Karte oder Ring einen individuellen Zugangscode einrichten @@ -644,6 +677,7 @@ Kamerazugriff verweigert Kein Memo erforderlich %1$s ( %2$s ) im %3$s Netzwerk + %1$s im %2$s Netzwerk Das Senden einer anderen Währung führt zu deren irreversiblem Verlust. Sende nur %s an diese Adresse. Der Versand einer anderen Währung führt zu ihrem unwiderruflichen Verlust. Senden nur %1$s im Netzwerk %2$s @@ -722,6 +756,12 @@ Ziel-Tag Adresse eingeben Die Adresse stimmt mit der Adresse Ihrer Brieftasche überein + Minimaler Betrag ist %s + Minimaler Wechselgeld ist %s + Falsche Gebühr + Mindestguthaben ist %s + Das Zielkonto ist nicht erstellt. Der abzusendende Betrag soll %s + Gebühr oder mehr sein + Unbekannter Fehler Ungültiges Memo. Sie wird der Transaktion nicht hinzugefügt. Memo Überprüfe deine Netzwerkverbindung @@ -779,8 +819,12 @@ inklusive einer Netzgebühr von %1$s Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert %1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen. + Der Betrag geht über die Bilanz hinaus Um diese Transaktion für die angegebene Adresse abzuschließen, ist ein Ziel-Tag (Memo) erforderlich. Ziel-Tag erforderlich + Ungültiger Betrag + Die Gebühr geht über die Bilanz hinaus + Der Gesamtbetrag geht über die Bilanz hinaus Transaktion gesendet Bereite das Scannen der Karte oder Ring vor, die du einrichten möchtest. Entferne diese Wallet @@ -1028,6 +1072,7 @@ Das wird nicht lange dauern. Wir richten gerade Dein Konto ein. Das wird nicht lange dauern. Wir schließen die Aktivierung ab. Alles wird vorbereitet! + PIN wurde nicht akzeptiert. Versuche es erneut oder verwende einen anderen Code. Ungültige PIN: Sequenzen oder Wiederholungen vermeiden Schon kann es losgehen! Du kannst Deine Verbindung \n auf der Website des Drittanbieters \n herstellen und zurück zur Tangem-App gelangen @@ -1036,6 +1081,7 @@ Willkommen zurück! Folge den Schritten, um Dein Konto einzurichten. Willkommen! + Diese Transaktion anfechten Freischalten Scanne Deine Karte, um den Zugriff freizuschalten Benötigte Freischaltung @@ -1187,4 +1233,7 @@ Karte oder Ring scannen Verwenden %s oder scanne eine Karte oder Ring, um auf die App zuzugreifen Willkommen zurück! + Nein, alles senden + Um %s XTZ reduziert + Damit Sie beim nächsten Aufladen Ihrer Brieftasche keine erhöhte Provision zahlen, soll der Betrag um %s XTZ reduziert werden diff --git a/core/res/src/main/res/values-es/strings-blockchain.xml b/core/res/src/main/res/values-es/strings-blockchain.xml deleted file mode 100644 index 709f3ab9dd..0000000000 --- a/core/res/src/main/res/values-es/strings-blockchain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - Por defecto - Legado - No se pudo obtener la tarifa - Debido a limitaciónes sobre %1$s, solos %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. - No hay fondos suficientes para la transacción. Por favor, recargue su cuenta. - Ocurrió un error. Código: %s - Para utilizar la red %1$s, debe pagar la reserva de cuenta (%2$s%3$s), que bloquea y oculta ese monto indefinidamente - La cuenta de destino no está activa. Envíe %s o más para activar la cuenta. - Para crear una cuenta, envíe fondos a esta dirección - El monto mínimo es %s - La cantidad es demasiado baja - Tarifa no válida - El saldo mínimo es %s - No se ha creado la cuenta de destino. El monto a enviar debe ser %s + comisiones o más - Error desconocido - El monto excede el saldo - Cantidad no válida - La tarifa excede el saldo - El monto total excede el saldo - No, enviar todo - Réduire de %s XTZ - Para evitar pagar una comisión mayor la próxima vez que recargue su billetera, reduzca el monto en %s - diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index abe6273ee4..8d8cd1ccfe 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -21,6 +21,8 @@ Agregue un token personalizado Gestionar tokens Envíe solo %1$s (%2$s) desde redes como %3$s a esta dirección. Usar otros tokens y redes puede resultar en la pérdida de fondos. + Por defecto + Legado Cómo escanear Solicitar soporte Inténtelo de nuevo @@ -83,6 +85,7 @@ Requisitos de transacción de Cardano Para realizar una %1$s transacción, deba depositar algo de ADA para cubrir la tarifa de red y el valor mínimo de ADA (se recomiendan 5 ADA) ADA insuficiente para la transferencia de tokens + El monto enviado y el cambio no pueden ser menores a 1 ADA Deba mantener algo de ADA porque tiene algunos tokens en la blockchain de Cardano ADA insuficiente Aceptar @@ -100,6 +103,7 @@ Saldo autenticación biométrica biometría + No se pudo crear la transacción Comprar Ir a %1$s No ha otorgado acceso a su cámara, cambie su configuración de privacidad @@ -135,6 +139,7 @@ Explore Explore historial de transacciones Explorador(a) + No se pudo obtener la tarifa Las tarifas de red son cargos que los usuarios pagan para procesar y confirmar transacciones. El importe de la tarifa puede verse afectado por la congestión de la red, el tamaño de la transacción y la prioridad de ejecución. %s Rápido Mercado @@ -182,6 +187,7 @@ Seleccione una acción Vender Enviar + Error al enviar la transacción El servidor no está disponible, por favor inténtelo de nuevo más tarde Compartir Compartir enlace @@ -205,6 +211,7 @@ Hubo un error. Por favor inténtelo de nuevo. Inaccesible Termine el staking + Debido a limitaciónes sobre %1$s, solos %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. semana con @@ -261,6 +268,7 @@ Compruebe su conexión a internet o cambia a una red diferente Condiciones de uso Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera + No hay fondos suficientes para la transacción. Por favor, recargue su cuenta. Mis tokens Aún no ha añadido ningún token. Agregue tokens a través de Market para hacer swap No se puede intercambiar por %s @@ -340,6 +348,9 @@ Comentarios en Tangem No se puede completar una transacción Error de descripción de moneda + Ocurrió un error + Ocurrió un error. Código: %s + Requiere memo Esta transacción Especifique el límite aprobado para el token seleccionado. Montante %s @@ -499,6 +510,10 @@ Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado Agregar tokens NFC no está disponible en su dispositivo + Para utilizar la red %1$s, debe pagar la reserva de cuenta (%2$s%3$s), que bloquea y oculta ese monto indefinidamente + La cuenta de destino no está activa. Envíe %s o más para activar la cuenta. + Para crear una cuenta, envíe fondos a esta dirección + La cuenta de destino no tiene una Trustline para el activo que se envía. Deba configurar un único código de acceso para proteger todss sus dispositivos. Proteger Puede configurar un código de acceso individual en cada tarjeta más adelante @@ -708,6 +723,12 @@ ID de destino Introduce la dirección La dirección es la misma que su billetera. + El monto mínimo es %s + El cambio mínimo es %s + Tarifa no válida + El saldo mínimo es %s + No se ha creado la cuenta de destino. El monto a enviar debe ser %s + comisiones o más + Error desconocido Memo no válido. No se añadirá a la transacción. Memo Compruebe su conexión de red @@ -765,8 +786,12 @@ incluida una tarifa de red de %1$s La transacción se firmó con éxito y se envió al nodo blockchain. El saldo de la billetera se actualizará después de un tiempo. %1$s es un activo en la red Tron. Para calcular la tarifa y realizar una transacción, deba depositar algo de Tron (TRX) en su cuenta. + El monto excede el saldo Es necesario una etiqueta de destino (memo) para completar esta transacción para la dirección especificada. Etiqueta de destino obligatoria + Cantidad no válida + La tarifa excede el saldo + El monto total excede el saldo Transacción enviada Escanee la tarjeta/anillo que quiere configurar Olvidar la billetera @@ -1144,4 +1169,7 @@ Escanee la tarjeta o el anillo Use %s o escanee una tarjeta/anillo para acceder a la app ¡Bienvenido de nuevo! + No, enviar todo + Réduire de %s XTZ + Para evitar pagar una comisión mayor la próxima vez que recargue su billetera, reduzca el monto en %s diff --git a/core/res/src/main/res/values-fr/strings-blockchain.xml b/core/res/src/main/res/values-fr/strings-blockchain.xml deleted file mode 100644 index 8269ca16c6..0000000000 --- a/core/res/src/main/res/values-fr/strings-blockchain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - Défaut - Héritage - Échec de réception des frais de commissions - En raison d\'une limitations sur les %1$s, seuls les %2$d UTXO peuvent s\'intégrer dans une seule transaction. Ce qui signifie vous ne pouvez envoyer que %3$s ou moins. Réduisez le montant. - Pas assez de fonds pour la transaction. Veuillez recharger votre compte. - Une erreur s\'est produite. Code:%s - Pour utiliser le réseau %1$s, vous devez payer la réserve de compte (%2$s%3$s), qui bloque et cache ce montant indéfiniment - Le compte de destination est inactif. Envoyez %s ou plus pour activer le compte. - Pour créer un compte, envoyez des fonds à cette adresse - Le montant minimal est de %s - Le montant est trop faible - Frais de commissions non valides - Le solde minimal est de %s - Le compte cible n\'a pas été créé. Le montant à envoyer doit être de %s + frais de commissions ou plus - Erreur inconnue - Le montant dépasse le solde - Montant invalide - Les frais de commissions dépassent le solde - Le total dépasse le solde - Non, envoyer toute la somme - Réduire de %s XTZ - Pour ne pas payer un fraid de commissions élevé la prochaine fois que vous rechargez votre portefeuille, veuillez réduire le montant de %s XTZ - diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 47b464f247..e7209c6305 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -21,6 +21,8 @@ Ajouter un jeton personnalisé Gérer les jetons Envoyez uniquement %1$s (%2$s) depuis les réseaux %3$s à cette adresse. L\'utilisation d\'autres jetons et réseaux peut entraîner une perte de fonds. + Défaut + Héritage Comment scanner Demander de l\'aide Réessayez @@ -83,6 +85,7 @@ Exigences pour les transactions Cardano Pour effectuer une transaction %1$s, vous devez déposer de l\'ADA pour couvrir les frais de réseau et la valeur minimale en ADA (5 ADA recommandés) ADA insuffisant pour le transfert de jetons + Le montant envoyé et la change ne peuvent être inférieurs à 1 ADA Vous devez conserver un peu d\'ADA car vous avez des jetons sur la blockchain Cardano ADA insuffisant Accepter @@ -100,6 +103,7 @@ Solde authentification biométrique biométrie + Échec de création de la transaction Acheter Aller à %1$s Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité @@ -135,6 +139,7 @@ Explorez Explorez l\'historique des transactions Explorateur + Échec de réception des frais de commissions Les frais de réseau sont des charges que les utilisateurs paient pour traiter et confirmer les transactions. Le montant des frais peut être affecté par la congestion du réseau, la taille de la transaction et la priorité d\'exécution. %s Rapide Marché @@ -182,6 +187,7 @@ Sélectionnez une action Vendre Envoyer + Échec d\'envoi de la transaction Le serveur n\'est pas disponible, veuillez réessayer plus tard Partager Partager le lien @@ -205,6 +211,7 @@ Il y avait une erreur. Veuillez réessayer. Inaccessible Unstakez + En raison d\'une limitations sur les %1$s, seuls les %2$d UTXO peuvent s\'intégrer dans une seule transaction. Ce qui signifie vous ne pouvez envoyer que %3$s ou moins. Réduisez le montant. semaine avec Oui @@ -261,6 +268,7 @@ Vérifiez votre connexion Internet ou passez à un réseau différent Conditions d\'utilisation Vous avez utilisé une carte d\'un autre portefeuille. Appuyez sur la carte associée à ce portefeuille + Pas assez de fonds pour la transaction. Veuillez recharger votre compte. Mes jetons Vous n\'avez pas encore ajouté de jetons. Ajoutez des jetons via Market pour effectuer une échange Ne peut pas être échangé contre %s @@ -340,6 +348,9 @@ Commentaires sur Tangem Impossible d\'effectuer une transaction Erreur de description de la pièce + Une erreur s\'est produite + Une erreur s\'est produite. Code:%s + Un mémo est requis Cette transaction Spécifiez la limite approuvée pour le jeton sélectionné Montant %s @@ -509,6 +520,10 @@ Appuyez ici pour recevoir le premier NFT Collections NFT Impossible de charger les données + Pour utiliser le réseau %1$s, vous devez payer la réserve de compte (%2$s%3$s), qui bloque et cache ce montant indéfiniment + Le compte de destination est inactif. Envoyez %s ou plus pour activer le compte. + Pour créer un compte, envoyez des fonds à cette adresse + Le compte destinataire n\'a pas de Trustline pour l\'actif envoyé qu\'il tente d\'envoyer. 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 @@ -718,6 +733,12 @@ ID de destination Entrez l\'adresse L\'adresse est la même que celle de votre portefeuille + Le montant minimal est de %s + La monnaie minimale est de %s + Frais de commissions non valides + Le solde minimal est de %s + Le compte cible n\'a pas été créé. Le montant à envoyer doit être de %s + frais de commissions ou plus + Erreur inconnue Mémo invalide. Il ne sera pas ajouté à la transaction. Mémo Vérifiez votre connexion réseau @@ -775,8 +796,12 @@ 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. + Le montant dépasse le solde Une balise de destination (mémo) est requise pour terminer cette transaction pour l\'adresse spécifiée. Étiquette de destination requise + Montant invalide + Les frais de commissions dépassent le solde + Le total dépasse le solde Transaction envoyée Scannez la carte/ bague que vous souhaitez configurer. Oublier le portefeuille @@ -1178,4 +1203,7 @@ Scanner la carte Utiliser %s ou scanner une carte pour accéder à l\'application Bon retour ! + Non, envoyer toute la somme + Réduire de %s XTZ + Pour ne pas payer un fraid de commissions élevé la prochaine fois que vous rechargez votre portefeuille, veuillez réduire le montant de %s XTZ diff --git a/core/res/src/main/res/values-it/strings-blockchain.xml b/core/res/src/main/res/values-it/strings-blockchain.xml deleted file mode 100644 index bedc136ed0..0000000000 --- a/core/res/src/main/res/values-it/strings-blockchain.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - Impossibile ottenere la commissione - Per creare un account, invia fondi a questo indirizzo - L\'importo minimo è di %s - L\'importo residuo è molto basso - Commissione non valida - Il conto totale non è stato creato L\'importo da inviare deve essere %s + commissione o maggiore - Errore sconosciuto - L\'importo supera il saldo - Importo non valido - La commissione supera il saldo - Il totale supera il saldo - No, invia l\'intero importo - Riduci di %s XTZ - Per evitare di pagare una commissione maggiore la prossima volta che ricarichi il tuo portafoglio, riduci l\'importo di %s XTZ - diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 9b25886a73..001ae1f84d 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -1,18 +1,25 @@ + Default + Legacy Questa carta non è progettata per funzionare con Tangem + L\'importo inviato e il cambio non può essere inferiore a 1 ADA Accetta Saldo: %s + Impossibile creare la transazione Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy Annulla Rimuovere Fatto Errore + Impossibile ottenere la commissione Costi della rete OK Mantieni le modifiche Invia + Impossibile inviare la transazione Con successo + Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta @@ -27,22 +34,44 @@ Firmato Requisiti Termini del servizio + Not enough funds for the transaction. Please top up your account. + An error occurred + An error occurred. Code: %s. + Notifica richiesta Scansiona carta Avvicina per modificare il codice di accesso Avvicina per modificare la password Avvicina per scansionare Avvicina per firmare Avvicina la carta + You don\'t have enough Mana for this transaction. Please wait until the Mana is refilled. Your Mana balance is %1$s/%2$s + To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely + Destination account is not active. Send %s or more to activate the account. + Per creare un account, invia fondi a questo indirizzo + The destination account does not have a trustline for the asset being sent. Crea portafoglio Importo L\'indirizzo corrisponde all\'indirizzo del tuo portafoglio + L\'importo minimo è di %s + Il resto minimo è di %s + Commissione non valida + Minimum balance is %s + Il conto totale non è stato creato L\'importo da inviare deve essere %s + commissione o maggiore + Errore sconosciuto Memo Importo totale La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo + L\'importo supera il saldo + Importo non valido + La commissione supera il saldo + Il totale supera il saldo Tangem Twin WalletConnect L\'indirizzo è stato copiato con successo Nessuna connessione a Internet Tangem Ok, ho capito! + No, invia l\'intero importo + Riduci di %s XTZ + Per evitare di pagare una commissione maggiore la prossima volta che ricarichi il tuo portafoglio, riduci l\'importo di %s XTZ diff --git a/core/res/src/main/res/values-ja/strings-blockchain.xml b/core/res/src/main/res/values-ja/strings-blockchain.xml deleted file mode 100644 index 5c5a94491d..0000000000 --- a/core/res/src/main/res/values-ja/strings-blockchain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - デフォルト - レガシー - 手数料を取得できませんでした - %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 - 取引に必要な資金が不足しています。アカウントに入金してください。 - エラーが発生しました。コード: %s 。 - %1$sネットワークを使用するには、アカウント準備金 ( %2$s %3$s ) を支払う必要があります。これにより、その金額はロックされ、無期限に非表示になります。 - 送信先アカウントが有効ではありません。%s 以上を送信してアカウントを有効にしてください。 - アカウントを作成するには、このアドレスに資金を送金してください - 最低額は%sです - 値が小さすぎます - 無効な手数料 - 最低残高は%sです - 対象アカウントは作成されていません。送信金額は、%s + 手数料またはそれ以上である必要があります。 - 不明なエラー - 金額が残高を超えています - 無効な金額 - 手数料が残高を超えています - 合計金額が残高を超えています - いいえ、すべて送信します - %s XTZを減らす - 次回ウォレットにチャージするときに手数料の増加を避けるには、金額を%s XTZ減らしてください。 - diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index df67256173..ec498830f5 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -21,6 +21,8 @@ カスタムトークンの追加 トークンの管理 このアドレスには%3$s ネットワークから%1$s (%2$s) のみを送信してください。他のトークンやネットワークを使用すると、資金を失う可能性があります。 + デフォルト + レガシー スキャン方法 サポートをリクエストする もう一度やり直してください @@ -82,6 +84,7 @@ Cardanoの取引要件 %1$s 取引を行うには、いくらかのADAを入金し、ネットワーク手数料と最小ADA (5ADAを推奨) をカバーする必要があります。 トークンを送るために必要なADAが不足しています。 + 送金額とお釣りは1ADA未満にすることはできません カルダノブロックチェーン上にトークンを持っているため、いくらかのADA残高を維持する必要があります。 ADAが不足しています。 受け入れる @@ -99,6 +102,7 @@ 残高 生体認証 生体認証 + 取引の構築に失敗しました 買う %1$sへ移動 カメラへのアクセスを許可していません。プライバシー設定を調整してください。 @@ -134,6 +138,7 @@ 移動する 取引履歴を調べる エクスプローラー + 手数料を取得できませんでした ネットワーク手数料は、取引の処理と確認のためにユーザーが支払う料金です。手数料の額は、ネットワークの混雑さ、取引のサイズ、実行の優先度によって左右されます。 %s 速い マーケット @@ -183,6 +188,7 @@ アクションを選択 売る 送る + 取引の送信に失敗しました サーバーが利用できません。しばらくしてからもう一度お試しください。 共有 リンクを共有 @@ -206,6 +212,7 @@ エラーが発生しました。もう一度お試しください。 アクセスできません ステーキング解除 + %1$sの制限により、1つのトランザクションに収まるUTXOは%2$d個のみです。つまり、 %3$s以下しか送信できません。量を減らす必要があります。 はい @@ -262,6 +269,7 @@ インターネット接続を確認するか、別のネットワークに切り替えてください。 利用規約 別のウォレットのカードまたはリングを使用しました。このウォレットにリンクしているカードまたはリングをタップしてください。 + 取引に必要な資金が不足しています。アカウントに入金してください。 マイトークン まだトークンを追加していません。マーケットからトークンを追加して交換してください。 %sとの交換はできません @@ -299,6 +307,7 @@ 入金待ち 入金待ち... 返金済み + 処理中 あなたへ送金しています 送金中... 送金済み @@ -341,6 +350,9 @@ Tangemへのフィードバック 取引を送信できません コインの説明エラー + エラーが発生しました + エラーが発生しました。コード: %s 。 + メモが必要 取引 選択したトークンの承認制限を指定します 金額%s @@ -497,6 +509,8 @@ これをドラッグするか、検索窓をタップして、マーケットから直接トークンを追加します トークンを追加 お使いのデバイスではNFCが使用できません + NFTについて + NFTアセット アイテム @@ -517,12 +531,23 @@ トークン標準 特徴 結果がありません。別のリクエストをお試しください。 + コレクションなし + 利用可能 ネットワークを選択 NFTを受け取る + このネットワークはまだ追加されていません。NFTの受け取りを開始するには、メイン画面に追加してください。 + ネットワークが追加されていません + 未追加 + NFTを送信する + 特徴 %1$dコレクションの%2$dNFT ここをタップして最初のNFTを受け取ります NFTコレクション データを読み込めません + %1$sネットワークを使用するには、アカウント準備金 ( %2$s %3$s ) を支払う必要があります。これにより、その金額はロックされ、無期限に非表示になります。 + 送信先アカウントが有効ではありません。%s 以上を送信してアカウントを有効にしてください。 + アカウントを作成するには、このアドレスに資金を送金してください + 送信先アカウントには、送金されるアセットのトラストラインがありません。 すべてのデバイスを保護するには、単一のアクセスコードを設定してください。 保護する 後で各カードおよびリングに個別のアクセスコードを設定できます。 @@ -729,6 +754,12 @@ 宛先タグ アドレスを入力 アドレスはウォレットアドレスと同じです + 最低額は%sです + 最小の変更は%sです + 無効な手数料 + 最低残高は%sです + 対象アカウントは作成されていません。送信金額は、%s + 手数料またはそれ以上である必要があります。 + 不明なエラー 無効なメモです。取引には追加されません。 メモ ネットワーク接続を確認してください @@ -786,8 +817,12 @@ ネットワーク手数料%1$sを含む 取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。 %1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTron(TRX)を入金する必要があります。 + 金額が残高を超えています 指定されたアドレスへの取引を完了するには、宛先タグ(メモ)が必要です。 宛先タグが必要です + 無効な金額 + 手数料が残高を超えています + 合計金額が残高を超えています 取引が送信されました 設定したいカードまたはリングをスキャンするために準備してください。 ウォレット削除 @@ -857,6 +892,11 @@ 再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。 残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。 TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。 + TONをステーキングまたは出金する場合、入金と出金の両方でステーキング額から0.2 TONの手数料が差し引かれます。 + この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。 + ステーキング手数料 + このアクションは、ネットワークのルールに従って、他のポジションをクローズするか、引き出しステータスに切り替えます。 + ポジション状況 資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。 資金は、21日間のロック解除期間後に使用可能になります。報酬は、ロック解除後の資金とともに引き出されます。 %sのステーキング解除期間後、資金はすぐ利用可能となります。 @@ -1173,13 +1213,41 @@ これはテストネットカードです。取引処理はできませんので、テストおよび開発目的でのみご利用ください。 テスト目的のみ 残高が古い可能性があります。ページを更新してください。 + 不明なドメイン + とにかく接続する + タイムアウトエラーが発生しました。しばらくしてからもう一度お試しください。 + WalletConnectを確立できませんでした + このドメインは検証できません。承認前にリクエスト内容をよく確認してください。 + ブラウザに戻り、WalletConnect経由で再度接続してください。 + WalletConnectセッションが接続解除されました + エラーコード: %s 。問題が解決しない場合は、お気軽にサポートまでお問い合わせください。 + 不明なエラーが発生しました + Tangemは現在%sで必要なネットワークをサポートしていません。 + サポートされていないネットワーク + Tangemは%sで必要なネットワークをサポートします。 + 検証済みドメイン + Tangemアプリで誤ったカードまたはリングが選択されました + 問題が起きています + 接続する + ネットワーク + ネットワーク + ウォレット + ウォレットの残高とアクティビティを表示する + 通知なしに取引に署名する + 取引の承認をリクエストする + できなくなります + したい + 接続リクエスト 接続 すべての接続を解除する すべてのdAppsの接続解除に関するテキスト すべてのdAppを接続解除する + このウォレットのプロフィールに%sネットワークを追加する + ウォレットに必要なネットワークはありません 新しい接続 ウォレットを別のdAppに接続する セッションなし + ウォレットコネクト 破棄 バックアップが中断されました。再開しますか? はい、再開します @@ -1191,4 +1259,7 @@ カードまたはリングをスキャン アプリにアクセスするには、 %sを使用するか、カードまたはリングをスキャンします お帰りなさい! + いいえ、すべて送信します + %s XTZを減らす + 次回ウォレットにチャージするときに手数料の増加を避けるには、金額を%s XTZ減らしてください。 diff --git a/core/res/src/main/res/values-ru/strings-blockchain.xml b/core/res/src/main/res/values-ru/strings-blockchain.xml deleted file mode 100644 index f589ed1f9f..0000000000 --- a/core/res/src/main/res/values-ru/strings-blockchain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - По умолчанию - Устаревший - Не удалось получить комиссию - Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. - Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. - Произошла ошибка. Код: %s. - Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе - Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. - Для создания аккаунта отправьте средства на этот адрес - Минимальная сумма: %s - Сдача слишком мала - Неверная комиссия - Минимальный баланс: %s - Аккаунт получателя не создан. Сумма для отправки должна быть %s + комиссия или больше - Неизвестная ошибка - Сумма превышает остаток - Недопустимая сумма - Комиссия превышает остаток - Отправляемая сумма превышает остаток - Нет, отправить все - Уменьшить на %s XTZ - Чтобы не платить повышенную комиссию при следующем пополнении кошелька, уменьшите сумму на %s XTZ - diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 3ea8b2da0c..401b2fea2a 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -21,6 +21,8 @@ Добавить токен Валюты Отправляйте только %1$s (%2$s) в сети %3$s на этот адрес. Использование другой сети может привести к утрате средств. + По умолчанию + Устаревший Как сканировать Обратиться в поддержку Попробовать снова @@ -85,6 +87,7 @@ Требования к транзакции Cardano Чтобы совершить транзакцию %1$s, внесите некоторую сумму ADA для покрытия сетевой комиссии и минимального значения ADA (рекомендуется 5 ADA) Недостаточно ADA для транзакции + Отправляемая сумма и сдача не могут быть меньше 1 ADA Вы должны поддерживать некоторое количество ADA, поскольку у вас на балансе есть токены в сети Cardano Недостаточно ADA Принять @@ -102,6 +105,7 @@ Баланс биометрическую аутентификацию биометрией + Не удалось создать транзакцию Купить Перейти на %1$s Вы не предоставили доступ к камере, пожалуйста, измените настройки конфиденциальности. @@ -141,6 +145,7 @@ Обозреватель Посмотреть историю транзакций Обозреватель + Не удалось получить комиссию Сетевые комиссии – это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s Быстро По рынку @@ -188,6 +193,7 @@ Выберите действие Продать Отправить + Не удалось отправить транзакцию Сервер недоступен, повторите попытку позднее Поделиться Поделиться ссылкой @@ -211,6 +217,7 @@ Произошла ошибка. Пожалуйста, попробуйте снова. Недоступно Завершить стейкинг + Из-за ограничений %1$s в одну транзакцию может поместиться только %2$d UTXO. Это означает, что вы можете отправить только %3$s или меньше. Вам нужно уменьшить сумму. неделю с Да @@ -267,6 +274,7 @@ Проверьте подключение с интернетом или переключитесь на другую сеть Условия использования Вы использовали карту или кольцо от другого кошелька. Приложите карту или кольцо, связанную с этим кошельком. + Недостаточно средств для совершения транзакции. Пожалуйста, пополните свой аккаунт. Мои токены У вас нет добавленных токенов. Добавьте токены для обмена Недоступен для обмена с %s @@ -345,6 +353,9 @@ Обращение в поддержку Tangem Не могу отправить транзакцию Ошибка в описании монеты + Произошла ошибка + Произошла ошибка. Код: %s. + Требуется memo Транзакция Укажите лимит доступа к выбранному токену Количество %s @@ -510,6 +521,10 @@ Потяните вверх или коснитесь поисковой строки, чтобы добавить токены напрямую из рынка Добавить токены Функция NFC недоступна на вашем устройстве + Чтобы использовать сеть %1$s, вы должны оплатить резерв аккаунта (%2$s %3$s), который блокируется и не используется в вашем балансе + Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. + Для создания аккаунта отправьте средства на этот адрес + Аккаунт получателя не содержит трастлайна для отправляемого актива. Установите единый код доступа для защиты всех ваших карт или колец Защита Установите индивидуальный код доступа для каждой карты или кольца позже. @@ -727,6 +742,12 @@ Код назначения Введите адрес Адрес совпадает с адресом кошелька + Минимальная сумма: %s + Минимальная сдача: %s + Неверная комиссия + Минимальный баланс: %s + Аккаунт получателя не создан. Сумма для отправки должна быть %s + комиссия или больше + Неизвестная ошибка Недопустимый Memo. Он не будет добавлен в транзакцию. Memo Проверьте своё интернет соединение @@ -784,7 +805,11 @@ включая комиссию сети в размере %1$s Транзакция успешно подписана и отправлена в блокчейн. Баланс будет обновлен через некоторое время %1$s — это монета в сети Tron. Чтобы рассчитать комиссию и совершить транзакцию, вам необходимо внести немного Tron (TRX) на свой адрес. + Сумма превышает остаток Для завершения транзакции на указанный адрес необходим тег назначения (мемо). + Недопустимая сумма + Комиссия превышает остаток + Отправляемая сумма превышает остаток Транзакция отправлена Подготовьтесь к сканированию кольца или карты, которую вы хотите настроить. Забыть кошелек @@ -853,6 +878,9 @@ Рестейк позволяет вам переместить средства из одного валидатора в другого без необходимости выхода из стейкинга. Вы собираетесь застейкать весь баланс, рекомендуем оставить небольшую сумму для оплаты комиссии сети при выходе из стейкинга или получении награды. Чтобы начать стейкинг в TON, сначала осуществите исходящую транзакцию на любую сумму — это активирует ваш кошелек. + В дополнение к сетевой комиссии может потребоваться до 0.2 TON для завершения всех этапов транзакции. Неиспользованная часть будет возвращена после выполнения. + Для выполнения операции требуется дополнительно 0.2 TON, помимо сетевой комиссии. Пожалуйста, пополните баланс. + Зарезервировано 0.2 TON Разблокируйте свои средства, чтобы вывести их из стейкинга. Разблокировка займёт %s. Ваши средства будут доступны для использования после 21-дневного периода отзыва. Награда будет выведена вместе с вашими выводими средствами. Ваши средства будут доступны после %s периода отзыва. @@ -1164,4 +1192,7 @@ Сканировать карту или кольцо Используйте %s или отсканируйте карту либо кольцо для входа в приложение C возвращением! + Нет, отправить все + Уменьшить на %s XTZ + Чтобы не платить повышенную комиссию при следующем пополнении кошелька, уменьшите сумму на %s XTZ diff --git a/core/res/src/main/res/values-uk-rUA/strings-blockchain.xml b/core/res/src/main/res/values-uk-rUA/strings-blockchain.xml deleted file mode 100644 index d2c3ed6cdf..0000000000 --- a/core/res/src/main/res/values-uk-rUA/strings-blockchain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - За замовчуванням - Застарілий - Не вдалося отримати комісію - Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. - Недостатньо коштів для здійснення транзакції. Будь ласка, поповніть свій акаунт. - Виникла помилка. Код: %s. - Щоб користуватися мережею %1$s, ви маєте оплатити резерв акаунта (%2$s %3$s), який блокується і не використовується у вашому балансі - Обліковий запис одержувача не активовано. Надішліть %s або більше, щоб активувати обліковий запис. - Для створення акаунту надішліть кошти на цю адресу - Мінімальна сума: %s - Решта занадто мала - Невірна комісія - Мінімальний баланс: %s - Обліковий запис одержувача не створено. Сума для переказу повинна бути %s + комісія або більше - Невідома помилка - Сума перевищує залишок - Недопустима сума - Комісія перевищує залишок - Сума, що відправляється, перевищує залишок - Ні, відправити все - Зменшити на %s XTZ - Щоб не платити підвищену комісію при наступному поповненні гаманця, зменште суму на %s XTZ - diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 4b6a67ab8c..1a942a7a26 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -21,6 +21,8 @@ Додати токен Токени Надсилайте тільки %1$s (%2$s) в мережі %3$s на цю адресу. Використання іншої мережі може призвести до втрати коштів. + За замовчуванням + Застарілий Як сканувати Звернутися в підтримку Спробуйте ще раз @@ -85,6 +87,7 @@ Вимоги до транзакції Cardano Щоб здійснити транзакцію %1$s, внесіть певну суму ADA для покриття мережевої комісії та мінімальне значення ADA (рекомендовано 5 ADA) Недостатньо ADA для транзакції + Сума і решта, що відправляються, не можуть бути меншими за 1 ADA Ви повинні утримувати деяку кількість ADA, оскільки у вас на балансі є токени в мережі Cardano Недостатньо ADA Прийняти @@ -102,6 +105,7 @@ Баланс біометричну автентифікацію біометрії + Не вдалося створити транзакцію Купити Перейдіть до %1$s Ви не надали доступ до камери, будь ласка, змініть налаштування конфіденційності @@ -141,6 +145,7 @@ Оглядач Переглянути історію транзакцій Оглядач + Не вдалося отримати комісію Мережеві комісії – це збори, які користувачі сплачують за обробку та підтвердження транзакцій. На розмір комісії може впливати перевантаження мережі, розмір транзакції та пріоритет виконання. %s Швидко За ринком @@ -188,6 +193,7 @@ Оберіть дію Продати Надіслати + Не вдалося надіслати транзакцію Сервер недоступний, спробуйте пізніше Поширити Поділитися посиланням @@ -211,6 +217,7 @@ Виникла помилка. Будь ласка, спробуйте ще раз. Недоступно Скасувати стейкінг + Через обмеження %1$s в одну транзакцію може поміститися тільки %2$d UTXO. Це означає, що ви можете відправити тільки %3$s або менше. Вам потрібно зменшити суму. тиждень з Так @@ -267,6 +274,7 @@ Перевірте підключення до інтернету або змініть мережу Умови використання Ви використали картку або кільце від іншого гаманця. Прикладіть картку або кільце, пов\'язану з цим гаманцем. + Недостатньо коштів для здійснення транзакції. Будь ласка, поповніть свій акаунт. Мої токени У вас немає доданих токенів. Додайте токени для обміну Недоступний для обміну з %s @@ -346,6 +354,9 @@ Звернення в підтримку Tangem Не вдається відправити транзакцію Помилка в описі монети + Виникла помилка + Виникла помилка. Код: %s. + Вимагається memo Транзакція Вкажіть ліміт доступу для обраного токена Кількість %s @@ -511,6 +522,10 @@ Потягніть вгору або торкніться панелі пошуку, щоб додати токени безпосередньо з маркету Додати токени Функція NFC недоступна на вашому пристрої + Щоб користуватися мережею %1$s, ви маєте оплатити резерв акаунта (%2$s %3$s), який блокується і не використовується у вашому балансі + Обліковий запис одержувача не активовано. Надішліть %s або більше, щоб активувати обліковий запис. + Для створення акаунту надішліть кошти на цю адресу + Акаунт одержувача не містить трастлайну для активу, що надсилається. Налаштуйте єдиний код доступу для захисту всіх ваших карток або кілець Захист Встановіть індивідуальний код доступу для кожної картки або кільця пізніше. @@ -728,6 +743,12 @@ Тег призначення Введіть адресу Адреса збігається з адресою гаманця + Мінімальна сума: %s + Мінімальна решта: %s + Невірна комісія + Мінімальний баланс: %s + Обліковий запис одержувача не створено. Сума для переказу повинна бути %s + комісія або більше + Невідома помилка Недопустимий Memo. Він не буде доданий до транзакції. Memo Перевірте підключення до мережі @@ -785,8 +806,12 @@ включно з комісією мережі %1$s Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час %1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок. + Сума перевищує залишок Тег призначення (memo) потрібен для завершення цієї транзакції для вказаної адреси. Тег призначення обов\'язковий + Недопустима сума + Комісія перевищує залишок + Сума, що відправляється, перевищує залишок Трансакцію надіслано Підготуйтеся до сканування кільця або картки, яку ви хочете налаштувати. Забути гаманець @@ -1166,4 +1191,7 @@ Сканувати картку або кільце Використовуйте %s або відскануйте картку або кільце для входу у додаток З поверненням! + Ні, відправити все + Зменшити на %s XTZ + Щоб не платити підвищену комісію при наступному поповненні гаманця, зменште суму на %s XTZ diff --git a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml b/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml deleted file mode 100644 index 7545da58e5..0000000000 --- a/core/res/src/main/res/values-zh-rTW/strings-blockchain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - 默認 - 遺留資產 - 獲取費用失敗 - 由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。 - 沒有足夠的資金進行交易。請先入金 - - - 目標帳戶未激活。發送 %s 或更多以激活帳戶 - 要創建帳戶,請將資金發送到此地址 - 最小數量是 %s - 更動太小 - 無效費用 - 最低餘額為 %s - 未創建目標帳戶。發送金額應為 %s + 費用或更多 - 未知錯誤 - 金額超出餘額 - 無效金額 - 費用超出餘額 - 總金額超出餘額 - 不,全部發送 - 減少 %s XTZ - 為避免下次給錢包充值時支付增加的佣金,請將金額減少 %s XTZ - diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 814be67145..78849a3497 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -3,6 +3,8 @@ 添加自定義代幣 管理代幣 僅將 %1$s (%2$s) 從 %3$s 網絡發送到此地址。使用其他代幣和網絡可能會導致資金損失 + 默認 + 遺留資產 請求支持 再試一次 此功能不在展示模式中提供 @@ -36,6 +38,7 @@ 回復至原廠設置 安全模式 卡片設置 + 發送金額和找零不能少於1個ADA 接受 允許 注意 @@ -43,6 +46,7 @@ 餘額 生物識別 生物 + 建立交易失敗 購買 您尚未授予相機訪問權限,請更改您的隱私設置 删除 @@ -58,6 +62,7 @@ 允許 啟用 錯誤 + 獲取費用失敗 導入 進行中 網路費 @@ -73,6 +78,7 @@ 選擇 銷售 發送 + 發送交易失敗 伺服器不可用,請稍後在試 分享 簽署 @@ -85,6 +91,7 @@ 交易 我了解 無法觸達 + 由於 %1$s 的限制,只有%2$d UTXO 可以放入單次交易中。這意味著您只能發送%3$s或更少數量。您需要減少數量。 已複製代幣地址 支持的網路 @@ -121,6 +128,7 @@ 檢查您的網路連接或切換到其他網絡 服務條款 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 + 沒有足夠的資金進行交易。請先入金 以下信息是可選的。如果不想共享,可以將其刪除 告訴我們您缺少哪些功能,我們會盡力幫助您 請告訴我們你有什麼卡 @@ -131,6 +139,9 @@ 反饋 Tangem反饋 無法發送交易 + An error occurred + An error occurred. Code: %s. + 需要memo 數量 %s 要繼續,您需要允許 %1s 智能合約使用您的 %2s 賦予權限 @@ -142,10 +153,15 @@ 點擊簽名 點按卡片 您已更新生物識別登入,掃描您的卡進入 + You don\'t have enough Mana for this transaction. Please wait until the Mana is refilled. Your Mana balance is %1$s/%2$s 管理代幣 要訪問所有的網路您需要掃描卡片 掃描卡片 代幣 + To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely + 目標帳戶未激活。發送 %s 或更多以激活帳戶 + 要創建帳戶,請將資金發送到此地址 + 目標帳戶沒有用於發送資產的 trustline 保護 您可以稍後在每張卡上設置單獨的訪問密碼 個人化 @@ -255,10 +271,20 @@ 準備好您的卡! 數量 地址與錢包地址相同 + 最小數量是 %s + 最小變化是 %s + 無效費用 + 最低餘額為 %s + 未創建目標帳戶。發送金額應為 %s + 費用或更多 + 未知錯誤 Memo無效。 它不會被添加到交易中 Memo 最大值 交易已成功簽署並發送至區塊鏈節點。錢包餘額稍後更新 + 金額超出餘額 + 無效金額 + 費用超出餘額 + 總金額超出餘額 安全地存儲您的加密貨幣,同時將私鑰保存在您的卡中 創新式的硬體錢包 最多 3張實體卡片 到一個錢包 @@ -358,4 +384,7 @@ 掃描卡片 使用 %s 或掃描卡片以訪問該應用程序 歡迎回來! + 不,全部發送 + 減少 %s XTZ + 為避免下次給錢包充值時支付增加的佣金,請將金額減少 %s XTZ diff --git a/core/res/src/main/res/values/strings-blockchain.xml b/core/res/src/main/res/values/strings-blockchain.xml deleted file mode 100644 index ef4e2b2edb..0000000000 --- a/core/res/src/main/res/values/strings-blockchain.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - Default - Legacy - Failed to get fee - Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. - Not enough funds for the transaction. Please top up your account. - An error occurred. Code: %s. - To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely - Destination account is not active. Send %s or more to activate the account. - To create account send funds to this address - Minimum amount is %s - Change is too small - Invalid Fee - Minimum balance is %s - Target account is not created. Amount to send should be %s + fee or more - Unknown error - Amount exceeds balance - Invalid amount - Fee exceeds balance - Total amount exceeds balance - No, send all - Reduce by %s XTZ - To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ - diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 074a5c4164..bda274d668 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -21,6 +21,8 @@ Add custom token Manage tokens Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds. + Default + Legacy How to scan Request support Try again @@ -83,6 +85,7 @@ Cardano transaction requirements To make a %1$s transaction, you must deposit some ADA to cover the network fee and minimum ADA value (5 ADA recommended) Insufficient ADA for token transfer + Sent amount and change cannot be less than 1 ADA You must maintain some ADA because you have some tokens on the Cardano blockchain Not enough ADA Accept @@ -100,6 +103,7 @@ Balance biometric authentication biometrics + Failed to build transaction Buy Go to %1$s You have not given access to your camera, please adjust your privacy settings @@ -137,6 +141,7 @@ Explore Explore transaction history Explorer + Failed to get fee Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s Fast Market @@ -186,6 +191,7 @@ Select action Sell Send + Failed to send transaction The server is not available, please try again later Share Share Link @@ -210,6 +216,7 @@ There was an error. Please try again. Unreachable Unstake + Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. week with Yes @@ -266,6 +273,7 @@ Check your internet connection or switch to a different network Terms of service You have used a card or ring from another wallet. Tap the card or ring associated with this wallet + Not enough funds for the transaction. Please top up your account. My tokens You haven\'t added any tokens yet. Add tokens via Market to swap Cannot be swapped for %s @@ -303,6 +311,7 @@ Awaiting deposit Awaiting deposit... Refunded + Refunding Sending to you Sending to you... Sent @@ -345,6 +354,9 @@ Tangem feedback Can\'t send a transaction Coin description error + An error occurred + An error occurred. Code: %s. + Requires memo Transaction Specify the approve limit for the selected token Amount %s @@ -505,6 +517,7 @@ Add tokens NFC is not available on your device About NFT + NFT asset %d item %d items @@ -526,14 +539,23 @@ Token Standard Traits No results. Please try another request. + No collection + Available Choose network Receive NFT + You haven\'t added this network yet. To start receiving NFTs, add it to the main screen! + Network not added + Not Added + Send NFT Traits %1$d NFTs in %2$d collection Tap here to receive first NFT NFT collections Unable to load the data - No collection + To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely + Destination account is not active. Send %s or more to activate the account. + To create account send funds to this address + The destination account does not have a trustline for the asset being sent. Set up a single access code to protect all your devices. Protect Set an individual access code for each card or ring later. @@ -744,6 +766,12 @@ Destination Tag Enter address Address is the same as wallet address + Minimum amount is %s + Minimum change is %s + Invalid Fee + Minimum balance is %s + Target account is not created. Amount to send should be %s + fee or more + Unknown error Invalid Memo. It won\'t be added to the transaction. Memo Check your network connection @@ -801,8 +829,12 @@ including a network fee of %1$s Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while %1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account. + Amount exceeds balance A destination tag (memo) is required to complete this transaction for the specified address. Destination Tag Required + Invalid amount + Fee exceeds balance + Total amount exceeds balance Transaction sent Prepare to scan card or ring you want to set up. Forget wallet @@ -872,6 +904,11 @@ Restake lets you move your funds from one validator to another without the need to unstake You’re about to stake your entire balance. We recommend leaving a small amount to cover network fees for unstaking or claiming rewards. To start staking in TON, first make an outgoing transaction of any amount — this will activate your wallet. + In addition to the network fee, up to 0.2 TON is needed to complete all steps of the transaction. Unused amount will be returned after it\'s finished. + 0.2 TON is required to proceed with this operation, in addition to the network fee. Please top up your balance. + 0.2 Ton Reserved + This action will close other positions or switch them to withdrawal status, according to network rules. + Positions status Unlock your money to withdraw it from staking process. Unlocking takes %s. Your funds will be available for use after the 21-day unbonding period. Reward will be withdrawn along with your unstaking funds. Your funds will be available for use after the %s unbonding period. @@ -1283,4 +1320,7 @@ Scan card or ring Use %s or scan a card or ring to access the app Welcome back! + No, send all + Reduce by %s XTZ + To avoid paying an increased commission the next time you top up your wallet, reduce the amount by %s XTZ From c7151d95a7dfc6f059d65d8373d199c0a8713a8d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 14:39:47 +0300 Subject: [PATCH 098/151] Updated on 2026-08-14 --- .../tap/data/RuntimeUserWalletsStore.kt | 2 +- .../tap/di/domain/TransactionDomainModule.kt | 6 + .../tap/proxy/TransactionManagerImpl.kt | 367 ---------- .../com/tangem/tap/proxy/di/ProxyModule.kt | 14 - .../local/userwallet/UserWalletsStore.kt | 2 +- .../DefaultWalletManagersFacade.kt | 5 +- .../CreateTransactionDataExtrasUseCase.kt | 38 +- .../usecase/GetEthSpecificFeeUseCase.kt | 128 ++++ features/swap/domain/models/build.gradle.kts | 1 + .../swap/domain/models/ui/SwapState.kt | 29 +- .../swap/domain/BlockchainInteractor.kt | 15 - .../domain/DefaultBlockchainInteractor.kt | 23 - .../feature/swap/domain/SwapInteractorImpl.kt | 659 ++++-------------- .../swap/domain/di/SwapDomainModule.kt | 9 - features/swap/impl/build.gradle.kts | 2 + .../tangem/feature/swap/model/SwapModel.kt | 27 +- .../tangem/lib/crypto/TransactionManager.kt | 50 -- .../com/tangem/lib/crypto/models/ProxyFee.kt | 34 - .../com/tangem/lib/crypto/models/ProxyFees.kt | 14 - .../lib/crypto/models/ProxyNetworkInfo.kt | 6 - 20 files changed, 335 insertions(+), 1096 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt delete mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt delete mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt delete mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt delete mode 100644 libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyNetworkInfo.kt diff --git a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt index 9870501655..ae906c4a76 100644 --- a/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt +++ b/app/src/main/java/com/tangem/tap/data/RuntimeUserWalletsStore.kt @@ -24,7 +24,7 @@ internal class RuntimeUserWalletsStore( return userWalletsListManager.userWalletsSync.firstOrNull { it.walletId == key } } - override suspend fun getSyncStrict(key: UserWalletId): UserWallet { + override fun getSyncStrict(key: UserWalletId): UserWallet { return requireNotNull(getSyncOrNull(key)) { "Unable to find user wallet with provided ID: $key" } } 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 3e76a02638..45734e3796 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,12 @@ internal object TransactionDomainModule { ) } + @Provides + @Singleton + fun provideGetEthSpecificFeeUseCase(walletManagersFacade: WalletManagersFacade): GetEthSpecificFeeUseCase { + return GetEthSpecificFeeUseCase(walletManagersFacade = walletManagersFacade) + } + @Provides @Singleton fun provideTransferGetFeeUseCase(walletManagersFacade: WalletManagersFacade): GetTransferFeeUseCase { diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt deleted file mode 100644 index 396fc681f2..0000000000 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ /dev/null @@ -1,367 +0,0 @@ -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 -import com.tangem.blockchain.externallinkprovider.TxExploreState -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.lib.crypto.TransactionManager -import com.tangem.lib.crypto.models.* -import java.math.BigDecimal -import java.math.BigInteger -import java.math.MathContext -import java.math.RoundingMode - -@Suppress("LargeClass") -class TransactionManagerImpl( - private val walletManagersFacade: WalletManagersFacade, - private val userWalletsListManager: UserWalletsListManager, -) : TransactionManager { - - override fun getExplorerTransactionLink(networkId: String, txAddress: String): String { - val blockchain = Blockchain.fromNetworkId(networkId) ?: error("blockchain not found") - return when (val txUrlState = blockchain.getExploreTxUrl(txAddress)) { - TxExploreState.Unsupported -> "" - is TxExploreState.Url -> txUrlState.url - } - } - - override suspend fun updateWalletManager(networkId: String, derivationPath: String?) { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - getActualWalletManager(blockchain, derivationPath).update() - } - - @Throws(IllegalStateException::class) - override suspend fun getFee( - networkId: String, - amountToSend: Amount, - currencyToSend: Currency, - destinationAddress: String, - increaseBy: Int?, - callData: SmartContractCallData?, - derivationPath: String?, - ): ProxyFees { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - if (walletManager is EthereumWalletManager) { - if (walletManager is EthereumOptimisticRollupWalletManager) { - return getFeeForOptimismBlockchain( - walletManager = walletManager, - amount = amountToSend, - destinationAddress = destinationAddress, - callData = callData, - ) - } - return getFeeForEthereumBlockchain( - walletManager = walletManager, - blockchain = blockchain, - amountToSend = amountToSend, - destinationAddress = destinationAddress, - callData = callData, - increaseBy = increaseBy, - ) - } else { - return getFeeForBlockchain( - walletManager = walletManager, - amountToSend = amountToSend, - destinationAddress = destinationAddress, - ) - } - } - - override fun getBlockchainInfo(networkId: String): ProxyNetworkInfo { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - return ProxyNetworkInfo( - name = blockchain.fullName, - blockchainId = blockchain.id, - ) - } - - private suspend fun getFeeForBlockchain( - walletManager: WalletManager, - amountToSend: Amount, - destinationAddress: String, - ): ProxyFees { - val fee = (walletManager as? TransactionSender)?.getFee( - amount = amountToSend, - destination = destinationAddress, - ) ?: error("Cannot cast to TransactionSender") - return when (fee) { - is Result.Success -> { - // for not EVM blockchains set gasLimit ZERO for now - when (fee.data) { - is TransactionFee.Single -> { - val singleFee = when (val normalFee = (fee.data as TransactionFee.Single).normal) { - is Fee.CardanoToken -> { - ProxyFee.CardanoToken( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - minAdaValue = normalFee.minAdaValue, - ) - } - is Fee.Filecoin -> { - ProxyFee.Filecoin( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - gasPremium = normalFee.gasPremium, - ) - } - is Fee.Sui -> { - ProxyFee.Sui( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - gasPrice = normalFee.gasPrice, - gasBudget = normalFee.gasBudget, - ) - } - else -> { - ProxyFee.Common( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = normalFee.amount), - ) - } - } - - ProxyFees.SingleFee(singleFee = singleFee) - } - is TransactionFee.Choosable -> { - val choosableFee = fee.data as TransactionFee.Choosable - ProxyFees.MultipleFees( - minFee = ProxyFee.Common( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = choosableFee.minimum.amount), - ), - normalFee = ProxyFee.Common( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = choosableFee.normal.amount), - ), - priorityFee = ProxyFee.Common( - gasLimit = BigInteger.ZERO, - fee = convertToProxyAmount(amount = choosableFee.priority.amount), - ), - ) - } - } - } - is Result.Failure -> { - error(fee.error.message ?: fee.error.customMessage) - } - } - } - - @Suppress("LongParameterList") - private suspend fun getFeeForEthereumBlockchain( - walletManager: EthereumWalletManager, - blockchain: Blockchain, - amountToSend: Amount, - destinationAddress: String, - callData: SmartContractCallData?, - increaseBy: Int?, - ): ProxyFees { - val gasLimit = getGasLimit( - evmWalletManager = walletManager, - amount = amountToSend, - destinationAddress = destinationAddress, - callData = callData, - ).increaseBigIntegerByPercents(increaseBy) - return when (val gasPrice = walletManager.getGasPrice()) { - is Result.Success -> { - createMultipleProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain) - } - is Result.Failure -> { - error(gasPrice.error.message ?: gasPrice.error.customMessage) - } - } - } - - private suspend fun getFeeForOptimismBlockchain( - walletManager: EthereumOptimisticRollupWalletManager, - amount: Amount, - destinationAddress: String, - callData: SmartContractCallData?, - ): ProxyFees { - val fee = if (callData == null) { - walletManager.getFee(amount, destinationAddress) - } else { - walletManager.getFee(amount, destinationAddress, callData) - } - return when (fee) { - is Result.Success -> { - val choosableFee = fee.data as? TransactionFee.Choosable ?: error("Incorrect fee type") - - val minProxyFee = ProxyFee.Common( - gasLimit = (choosableFee.minimum as Fee.Ethereum).gasLimit, - fee = convertToProxyAmount(amount = choosableFee.minimum.amount), - ) - val normalProxyFee = ProxyFee.Common( - gasLimit = (choosableFee.normal as Fee.Ethereum).gasLimit, - fee = convertToProxyAmount(amount = choosableFee.normal.amount), - ) - val priorityProxyFee = ProxyFee.Common( - gasLimit = (choosableFee.priority as Fee.Ethereum).gasLimit, - fee = convertToProxyAmount(amount = choosableFee.priority.amount), - ) - - ProxyFees.MultipleFees( - minFee = minProxyFee, - normalFee = normalProxyFee, - priorityFee = priorityProxyFee, - ) - } - is Result.Failure -> { - error(fee.error.message ?: fee.error.customMessage) - } - } - } - - private suspend fun getGasLimit( - evmWalletManager: EthereumWalletManager, - amount: Amount, - destinationAddress: String, - callData: SmartContractCallData?, - ): BigInteger { - val result = if (callData == null) { - evmWalletManager.getGasLimit( - amount = amount, - destination = destinationAddress, - ) - } else { - evmWalletManager.getGasLimit( - amount = amount, - destination = destinationAddress, - callData = callData, - ) - } - when (result) { - is Result.Success -> { - return result.data - } - is Result.Failure -> { - error(result.error.message ?: result.error.customMessage) - } - } - } - - override suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees { - val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } - val walletManager = getActualWalletManager(blockchain, derivationPath) - val gasPriceResult = (walletManager as? EthereumWalletManager)?.getGasPrice() - ?: error("not supported for $blockchain") - val gasPrice = when (gasPriceResult) { - is Result.Failure -> error("fail to receive gasPrice") - is Result.Success -> gasPriceResult.data - } - return createMultipleProxyFees(gasPrice, gas, blockchain) - } - - private suspend fun getActualWalletManager(blockchain: Blockchain, derivationPath: String?): WalletManager { - val selectedUserWallet = requireNotNull( - userWalletsListManager.selectedUserWalletSync, - ) { "userWallet or userWalletsListManager is null" } - val walletManager = walletManagersFacade.getOrCreateWalletManager( - selectedUserWallet.walletId, - blockchain, - derivationPath, - ) - - return requireNotNull(walletManager) { "no wallet manager found" } - } - - /** - * Create proxy fees - * - * @param gasPrice min fee gasPrice - * @param gasLimit - * @param blockchain - */ - private fun createMultipleProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees { - val patchedGasLimit = gasLimit.toBigDecimal().increaseForMantleIfNeeded(blockchain).toBigInteger() - val gasPriceNormal = gasPrice - .increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE) - val gasPricePriority = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE) - val feeMin = patchedGasLimit.multiply(gasPrice).toBigDecimal( - scale = blockchain.decimals(), - mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), - ).increaseForMantleIfNeeded(blockchain) - val feeNormal = patchedGasLimit.multiply(gasPriceNormal).toBigDecimal( - scale = blockchain.decimals(), - mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), - ).increaseForMantleIfNeeded(blockchain) - val feePriority = patchedGasLimit.multiply(gasPricePriority).toBigDecimal( - scale = blockchain.decimals(), - mathContext = MathContext(blockchain.decimals(), RoundingMode.HALF_EVEN), - ).increaseForMantleIfNeeded(blockchain) - val minFee = ProxyFee.Common( - gasLimit = patchedGasLimit, - fee = ProxyAmount( - currencySymbol = blockchain.currency, - value = feeMin, - decimals = blockchain.decimals(), - ), - ) - val normalFee = ProxyFee.Common( - gasLimit = patchedGasLimit, - fee = ProxyAmount( - currencySymbol = blockchain.currency, - value = feeNormal, - decimals = blockchain.decimals(), - ), - ) - val priorityFee = ProxyFee.Common( - gasLimit = patchedGasLimit, - fee = ProxyAmount( - currencySymbol = blockchain.currency, - value = feePriority, - decimals = blockchain.decimals(), - ), - ) - return ProxyFees.MultipleFees( - minFee = minFee, - normalFee = normalFee, - priorityFee = priorityFee, - ) - } - - private fun convertToProxyAmount(amount: Amount): ProxyAmount { - return ProxyAmount( - currencySymbol = amount.currencySymbol, - value = amount.value ?: BigDecimal.ZERO, - decimals = amount.decimals, - ) - } - - /** - * Increase big integer by percents - * - * @param percents in format 150 -> 50% - * @return increased value - */ - private fun BigInteger.increaseBigIntegerByPercents(percents: Int?): BigInteger { - return if (percents != null && percents != 0) { - this.multiply(percents.toBigInteger()).divide(BigInteger("100")) - } else { - this - } - } - - // TODO Workaround for Mantle. Remove after [REDACTED_JIRA] - private fun BigDecimal.increaseForMantleIfNeeded(blockchain: Blockchain): BigDecimal { - return if (blockchain == Blockchain.Mantle) { - this.multiply(MANTLE_FEE_ESTIMATE_MULTIPLIER) - } else { - this - } - } - - companion object { - private const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50% - private const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50% - private val MANTLE_FEE_ESTIMATE_MULTIPLIER = BigDecimal("1.8") - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt index e9bcad2a6b..4d59c1f56e 100644 --- a/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt +++ b/app/src/main/java/com/tangem/tap/proxy/di/ProxyModule.kt @@ -2,10 +2,8 @@ package com.tangem.tap.proxy.di import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.tap.proxy.AppStateHolder -import com.tangem.tap.proxy.TransactionManagerImpl import com.tangem.tap.proxy.UserWalletManagerImpl import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -37,16 +35,4 @@ internal object ProxyModule { dispatchers = dispatchers, ) } - - @Provides - @Singleton - fun provideTransactionManager( - walletManagersFacade: WalletManagersFacade, - userWalletsListManager: UserWalletsListManager, - ): TransactionManager { - return TransactionManagerImpl( - walletManagersFacade = walletManagersFacade, - userWalletsListManager = userWalletsListManager, - ) - } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt index 56acd23a53..df1e2e84bd 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/userwallet/UserWalletsStore.kt @@ -13,7 +13,7 @@ interface UserWalletsStore { fun getSyncOrNull(key: UserWalletId): UserWallet? - suspend fun getSyncStrict(key: UserWalletId): UserWallet + fun getSyncStrict(key: UserWalletId): UserWallet suspend fun getAllSyncOrNull(): List? 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 ba14ca68b2..765662941b 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 @@ -277,10 +277,7 @@ class DefaultWalletManagersFacade( } } - private suspend fun getUserWallet(userWalletId: UserWalletId) = - requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "Unable to find a user wallet with provided ID: $userWalletId" - } + private fun getUserWallet(userWalletId: UserWalletId) = userWalletsStore.getSyncStrict(userWalletId) private suspend fun getAndUpdateWalletManager( userWallet: UserWallet, 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 c63384ba5e..b6d704cbbc 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,7 +1,9 @@ package com.tangem.domain.transaction.usecase -import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either import com.tangem.blockchain.common.smartcontract.CompiledSmartContractCallData +import com.tangem.blockchain.common.smartcontract.SmartContractCallData import com.tangem.common.extensions.hexToBytes import com.tangem.domain.tokens.model.Network import com.tangem.domain.transaction.TransactionRepository @@ -12,14 +14,38 @@ class CreateTransactionDataExtrasUseCase( ) { operator fun invoke(data: String, network: Network, gasLimit: BigInteger? = null, nonce: BigInteger? = null) = - Either.catch { - requireNotNull( + either { + catch( + { + transactionRepository.createTransactionDataExtras( + callData = CompiledSmartContractCallData(data.hexToBytes()), + network = network, + nonce = nonce, + gasLimit = gasLimit, + ) + }, + ) { + raise(it) + } + } + + operator fun invoke( + callData: SmartContractCallData, + network: Network, + gasLimit: BigInteger? = null, + nonce: BigInteger? = null, + ) = either { + catch( + { transactionRepository.createTransactionDataExtras( - callData = CompiledSmartContractCallData(data.hexToBytes()), + callData = callData, network = network, nonce = nonce, gasLimit = gasLimit, - ), - ) { "Failed to create transaction" } + ) + }, + ) { + raise(it) } + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt new file mode 100644 index 0000000000..d688ccbd9b --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt @@ -0,0 +1,128 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWallet +import java.math.BigInteger +import java.math.MathContext +import java.math.RoundingMode + +/** + * Use case to get transaction fee for ETH when we have gas from other services + * + */ +class GetEthSpecificFeeUseCase( + private val walletManagersFacade: WalletManagersFacade, +) { + suspend operator fun invoke( + userWallet: UserWallet, + cryptoCurrency: CryptoCurrency, + gasLimit: BigInteger, + gasPrice: BigInteger? = null, + ) = either { + catch( + block = { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ) + val gasPriceResult = gasPrice + ?: (walletManager as? EthereumWalletManager)?.getGasPriceValue() + ?: error("not supported for ${cryptoCurrency.network}") + + val blockchain = Blockchain.fromNetworkId(networkId = cryptoCurrency.network.backendId) + ?: error("unknown networkId ${cryptoCurrency.network.backendId}") + + val minimalFee = getEthLegacyFee( + gasPrice = gasPriceResult, + gasLimit = gasLimit, + decimals = cryptoCurrency.decimals, + blockchain = blockchain, + ) + + val normalGasPrice = gasPriceResult.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE) + val normalFee = getEthLegacyFee( + gasPrice = normalGasPrice, + gasLimit = gasLimit, + decimals = cryptoCurrency.decimals, + blockchain = blockchain, + ) + + val priorityGasPrice = gasPriceResult.increaseBigIntegerByPercents( + MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE, + ) + val priorityFee = getEthLegacyFee( + gasPrice = priorityGasPrice, + gasLimit = gasLimit, + decimals = cryptoCurrency.decimals, + blockchain = blockchain, + ) + + TransactionFee.Choosable( + minimum = minimalFee, + normal = normalFee, + priority = priorityFee, + ) + }, + catch = { + raise(GetFeeError.DataError(it)) + }, + ) + } + + private fun getEthLegacyFee( + gasPrice: BigInteger, + decimals: Int, + gasLimit: BigInteger, + blockchain: Blockchain, + ): Fee.Ethereum.Legacy { + val amount = Amount( + value = gasLimit.multiply(gasPrice).toBigDecimal( + scale = decimals, + mathContext = MathContext(decimals, RoundingMode.HALF_EVEN), + ), + blockchain = blockchain, + ) + return Fee.Ethereum.Legacy( + amount = amount, + gasLimit = gasLimit, + gasPrice = gasPrice, + ) + } + + private suspend fun EthereumWalletManager.getGasPriceValue(): BigInteger { + return when (val gasPrice = this.getGasPrice()) { + is Result.Failure -> throw gasPrice.error + is Result.Success -> gasPrice.data + } + } + + /** + * Increase big integer by percents + * + * @param percents in format 150 -> 50% + * @return increased value + */ + private fun BigInteger.increaseBigIntegerByPercents(percents: Int?): BigInteger { + return if (percents != null && percents != 0) { + this.multiply(percents.toBigInteger()).divide(BigInteger("100")) + } else { + this + } + } + + private companion object { + const val MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE = 150 // 50% + const val MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE = 200 // 50% + } +} \ No newline at end of file diff --git a/features/swap/domain/models/build.gradle.kts b/features/swap/domain/models/build.gradle.kts index 6ebbca292e..e4496dc053 100644 --- a/features/swap/domain/models/build.gradle.kts +++ b/features/swap/domain/models/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.core.datasource) /** Other Libraries **/ + implementation(tangemDeps.blockchain) implementation(deps.kotlin.serialization) implementation(deps.arrow.core) implementation(deps.moshi.kotlin) 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 f6aa62d8cd..be89782000 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 @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain.models.ui +import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.feature.swap.domain.models.ExpressDataError @@ -86,11 +87,6 @@ data class RequestApproveStateData( val spenderAddress: String, ) -// data class SwapStateData( -// val fee: TxFeeState, -// val swapModel: SwapDataModel, -// ) - sealed class TxFeeState { data class MultipleFeeState( val normalFee: TxFee, @@ -114,34 +110,15 @@ sealed class TxFeeState { data class TxFee( val feeValue: BigDecimal, - val gasLimit: Int, val feeFiatFormatted: String, val feeCryptoFormatted: String, val feeIncludeOtherNativeFee: BigDecimal, val feeFiatFormattedWithNative: String, val feeCryptoFormattedWithNative: String, - val decimals: Int, val cryptoSymbol: String, val feeType: FeeType, - val params: Params?, -) { - - sealed class Params { - - data class Filecoin( - val gasPremium: Long, - ) : Params() - - data class Sui( - val gasBudget: Long, - val gasPrice: Long, - ) : Params() - - data class Hedera( - val additionalHBARFee: BigDecimal, - ) : Params() - } -} + val fee: Fee, +) enum class FeeType { NORMAL, PRIORITY diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt deleted file mode 100644 index ceab0eb503..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/BlockchainInteractor.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.swap.domain - -import com.tangem.feature.swap.domain.models.domain.NetworkInfo - -interface BlockchainInteractor { - - /** - * In app blockchain id, actual in blockchain sdk, not the same as networkId - * - * workaround till not use backend only and not integrated server vs sdk - */ - fun getBlockchainInfo(networkId: String): NetworkInfo - - fun getExplorerTransactionLink(networkId: String, txHash: String): String -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt deleted file mode 100644 index 284b1572cb..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultBlockchainInteractor.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.feature.swap.domain - -import com.tangem.feature.swap.domain.models.domain.NetworkInfo -import com.tangem.lib.crypto.TransactionManager -import javax.inject.Inject - -internal class DefaultBlockchainInteractor @Inject constructor( - private val transactionManager: TransactionManager, -) : BlockchainInteractor { - - override fun getBlockchainInfo(networkId: String): NetworkInfo { - return transactionManager.getBlockchainInfo(networkId).let { - NetworkInfo( - name = it.name, - blockchainId = it.blockchainId, - ) - } - } - - override fun getExplorerTransactionLink(networkId: String, txHash: String): String { - return transactionManager.getExplorerTransactionLink(networkId, txHash) - } -} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 34954d7dc3..6d119df85d 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 @@ -3,16 +3,13 @@ package com.tangem.feature.swap.domain import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Blockchain.* +import com.tangem.blockchain.common.TransactionData 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.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 @@ -20,6 +17,7 @@ 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.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.GetCurrencyCheckUseCase import com.tangem.domain.tokens.TokensFeatureToggles @@ -35,17 +33,13 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* -import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.lib.crypto.models.ProxyAmount -import com.tangem.lib.crypto.models.ProxyFee -import com.tangem.lib.crypto.models.ProxyFees import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -57,11 +51,11 @@ import java.math.RoundingMode @Suppress("LargeClass", "LongParameterList") internal class SwapInteractorImpl @AssistedInject constructor( - private val transactionManager: TransactionManager, private val userWalletManager: UserWalletManager, private val repository: SwapRepository, private val allowPermissionsHandler: AllowPermissionsHandler, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, private val createTransferTransactionUseCase: CreateTransferTransactionUseCase, @@ -78,6 +72,8 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val initialToCurrencyResolver: InitialToCurrencyResolver, private val validateTransactionUseCase: ValidateTransactionUseCase, private val estimateFeeUseCase: EstimateFeeUseCase, + private val getFeeUseCase: GetFeeUseCase, + private val getEthSpecificFeeUseCase: GetEthSpecificFeeUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getCurrencyCheckUseCase: GetCurrencyCheckUseCase, private val amountFormatter: AmountFormatter, @@ -88,7 +84,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( GetSelectedAppCurrencyUseCase(appCurrencyRepository) } - private val swapCurrencyConverter = SwapCurrencyConverter() private val hundredPercent = BigInteger("100") private val userWallet @@ -218,10 +213,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } val approveTransaction = createApprovalTransactionUseCase( - fee = getFeeForTransaction( - fee = permissionOptions.txFee, - blockchain = Blockchain.fromId(permissionOptions.fromToken.network.id.value), - ), + fee = permissionOptions.txFee.fee, userWalletId = userWalletId, cryptoCurrency = permissionOptions.fromToken as CryptoCurrency.Token, amount = amount?.value, @@ -338,10 +330,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) - transactionManager.updateWalletManager( - networkId, - fromToken.currency.network.derivationPath.value, - ) + fetchCurrencyStatusUseCase(userWalletId, fromToken.currency.id, true) } return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { provider to loadDexSwapData( @@ -538,7 +527,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( swapData = requireNotNull(swapData), currencyToSendStatus = currencyToSend, currencyToGetStatus = currencyToGet, - fee = fee, + txFee = fee, amountToSwap = amountToSwap, ) } @@ -593,7 +582,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( currencyToSendStatus: CryptoCurrencyStatus, currencyToGetStatus: CryptoCurrencyStatus, amountToSwap: String, - fee: TxFee, + txFee: TxFee, ): SwapTransactionState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val amount = SwapAmount(amountDecimal, currencyToSendStatus.currency.decimals) @@ -603,15 +592,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( val amountToSend = createNativeAmountForDex(swapData.transaction.txValue, currencyToSendStatus.currency.network) val txData = createTransactionUseCase( amount = amountToSend, - fee = getFeeForTransaction( - fee = fee, - blockchain = Blockchain.fromId(currencyToSendStatus.currency.network.id.value), - ), + fee = txFee.fee, memo = null, destination = swapData.transaction.txTo, userWalletId = userWalletId, network = currencyToSendStatus.currency.network, - txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, fee.gasLimit), + txExtras = createDexTxExtras(dataToSign, currencyToSendStatus.currency.network, txFee.fee.getGasLimit()), ).getOrElse { Timber.e(it, "Failed to create swap dex tx data") return SwapTransactionState.Error.UnknownError @@ -706,10 +692,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( val txData = createTransferTransactionUseCase( amount = amount.value.convertToSdkAmount(currencyToSend.currency), - fee = getFeeForTransaction( - fee = txFee, - blockchain = Blockchain.fromId(currencyToSend.currency.network.id.value), - ), + fee = txFee.fee, memo = exchangeDataCex.txExtraId, destination = exchangeDataCex.txTo, userWalletId = userWalletId, @@ -777,235 +760,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } - @Suppress("LongMethod") - private fun getFeeForTransaction(fee: TxFee, blockchain: Blockchain): Fee { - val feeAmountValue = fee.feeValue - val feeAmount = Amount( - value = fee.feeValue, - currencySymbol = fee.cryptoSymbol, - decimals = fee.decimals, - type = AmountType.Coin, - ) - - return if (blockchain.isEvm()) { - val feeAmountWithDecimals = feeAmountValue.movePointRight(fee.decimals) - - Fee.Ethereum.Legacy( - amount = feeAmount, - gasLimit = fee.gasLimit.toBigInteger(), - gasPrice = (feeAmountWithDecimals / fee.gasLimit.toBigDecimal()).toBigInteger(), - ) - } else { - when (blockchain) { - // region Blockchains with their own fees - VeChain, - VeChainTestnet, - -> { - Fee.VeChain( - amount = feeAmount, - gasPriceCoef = Fee.VeChain.getGasPriceCoef(fee.gasLimit.toLong(), fee.feeValue), - gasLimit = fee.gasLimit.toLong(), - ) - } - Aptos, - AptosTestnet, - -> { - val gasUnitPrice = fee.feeValue.divide( - fee.gasLimit.toBigDecimal(), - Aptos.decimals(), - RoundingMode.HALF_UP, - ) - - Fee.Aptos( - amount = feeAmount, - gasUnitPrice = gasUnitPrice - .movePointRight(Aptos.decimals()) - .toLong(), - gasLimit = fee.gasLimit.toLong(), - ) - } - Filecoin, - -> { - val gasUnitPrice = fee.feeValue.divide( - BigDecimal(fee.gasLimit), - Filecoin.decimals(), - RoundingMode.HALF_UP, - ) - val feeParams = requireNotNull(fee.params as? TxFee.Params.Filecoin) - - Fee.Filecoin( - amount = feeAmount, - gasUnitPrice = gasUnitPrice - .movePointRight(Filecoin.decimals()) - .toLong(), - gasLimit = fee.gasLimit.toLong(), - gasPremium = feeParams.gasPremium, - ) - } - Sui, - SuiTestnet, - -> { - val feeParams = requireNotNull(fee.params as? TxFee.Params.Sui) - - Fee.Sui( - amount = feeAmount, - gasPrice = feeParams.gasPrice, - gasBudget = feeParams.gasBudget, - ) - } - Hedera, - HederaTestnet, - -> { - val feeParams = requireNotNull(fee.params as? TxFee.Params.Hedera) - Fee.Hedera(amount = feeAmount, additionalHBARFee = feeParams.additionalHBARFee) - } - // endregion - // region Blockchains with common fees or EVM-like fees - Unknown, - Arbitrum, - ArbitrumTestnet, - Avalanche, - AvalancheTestnet, - Binance, - BinanceTestnet, - BSC, - BSCTestnet, - Bitcoin, - BitcoinTestnet, - BitcoinCash, - BitcoinCashTestnet, - Cardano, - Cosmos, - CosmosTestnet, - Dogecoin, - Ducatus, - Ethereum, - EthereumTestnet, - EthereumClassic, - EthereumClassicTestnet, - Fantom, - FantomTestnet, - Litecoin, - Near, - NearTestnet, - Polkadot, - PolkadotTestnet, - Kava, - KavaTestnet, - Kusama, - Polygon, - PolygonTestnet, - RSK, - Sei, - SeiTestnet, - Stellar, - StellarTestnet, - Solana, - SolanaTestnet, - Tezos, - Tron, - TronTestnet, - XRP, - Gnosis, - Dash, - Optimism, - OptimismTestnet, - Dischain, - EthereumPow, - EthereumPowTestnet, - Kaspa, - KaspaTestnet, - Telos, - TelosTestnet, - TON, - TONTestnet, - Ravencoin, - RavencoinTestnet, - TerraV1, - TerraV2, - Cronos, - AlephZero, - AlephZeroTestnet, - OctaSpace, - OctaSpaceTestnet, - Chia, - ChiaTestnet, - Decimal, - DecimalTestnet, - XDC, - XDCTestnet, - Playa3ull, - Shibarium, - ShibariumTestnet, - Algorand, - AlgorandTestnet, - Aurora, - AuroraTestnet, - Areon, - AreonTestnet, - PulseChain, - PulseChainTestnet, - ZkSyncEra, - ZkSyncEraTestnet, - Nexa, - NexaTestnet, - Moonbeam, - MoonbeamTestnet, - Manta, - MantaTestnet, - PolygonZkEVM, - PolygonZkEVMTestnet, - Radiant, - Fact0rn, - Base, - BaseTestnet, - Moonriver, - MoonriverTestnet, - Mantle, - MantleTestnet, - Flare, - FlareTestnet, - Taraxa, - TaraxaTestnet, - Koinos, - KoinosTestnet, - Joystream, - Bittensor, - Blast, - BlastTestnet, - Cyber, - CyberTestnet, - InternetComputer, - EnergyWebChain, - EnergyWebChainTestnet, - EnergyWebX, - EnergyWebXTestnet, - Casper, - CasperTestnet, - Core, - CoreTestnet, - Xodex, - Canxium, - Chiliz, - ChilizTestnet, - Alephium, - AlephiumTestnet, - Clore, - VanarChain, - VanarChainTestnet, - OdysseyChain, OdysseyChainTestnet, - Bitrock, BitrockTestnet, - Sonic, SonicTestnet, - ApeChain, ApeChainTestnet, - Scroll, ScrollTestnet, - ZkLinkNova, ZkLinkNovaTestnet, - Pepecoin, PepecoinTestnet, - -> Fee.Common(feeAmount) - // endregion - } - } - } - private suspend fun storeSwapTransaction( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, @@ -1418,17 +1172,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( val otherNativeFee = transaction.otherNativeFeeWei ?.movePointLeft(nativeCoinDecimals) ?: BigDecimal.ZERO - val txFeeState = when ( - val feeData = getFeeDataForDexSwap( - networkId = networkId, - transaction = transaction, - fromToken = fromToken.currency, - cardId = userWallet.scanResponse.card.cardId, - ) - ) { - is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) - is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken.currency, otherNativeFee) - } + val txFeeState = getFeeDataForDexSwap( + network = fromToken.currency.network, + transaction = transaction, + fromToken = fromToken.currency, + ) + .patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX) + .toTxFeeState(fromToken.currency, otherNativeFee) + val includeFeeInAmount = IncludeFeeInAmount.Excluded // exclude for dex val feeByPriority = selectFeeByType(feeType = selectedFee, txFeeState = txFeeState) val feeToCheckFunds = feeByPriority + (otherNativeFee ?: BigDecimal.ZERO) @@ -1492,17 +1243,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private suspend fun getFeeDataForDexSwap( - networkId: String, + network: Network, transaction: ExpressTransactionModel.DEX, fromToken: CryptoCurrency, - cardId: String?, - ): ProxyFees { - if (cardId != null && isDemoCardUseCase(cardId)) { - return getDemoFees(fromToken) - } + ): TransactionFee { return try { val nativeBalance = userWalletManager.getNativeTokenBalance( - networkId = networkId, + networkId = network.backendId, derivationPath = fromToken.network.derivationPath.value, ) ?: ProxyAmount.empty() val amountToSend = createNativeAmountForDex(transaction.txValue, fromToken.network) @@ -1510,21 +1257,30 @@ internal class SwapInteractorImpl @AssistedInject constructor( if (nativeBalance.value < amountToSend.value) { error("It's impossible to calculate fee for nativeBalance.value < amountToSend.value") } - transactionManager.getFee( - networkId = networkId, - amountToSend = amountToSend, - currencyToSend = swapCurrencyConverter.convert(fromToken), + + val extras = createTransactionExtrasUseCase( + data = transaction.txData, + network = network, + ).getOrNull() ?: error("unable to create extras") + + val transactionData = TransactionData.Uncompiled( + amount = amountToSend, destinationAddress = transaction.txTo, - increaseBy = INCREASE_GAS_LIMIT_BY, - callData = CompiledSmartContractCallData(transaction.txData.hexToBytes()), - derivationPath = fromToken.network.derivationPath.value, + fee = null, + sourceAddress = transaction.txFrom, + extras = extras, ) + getFeeUseCase( + transactionData = transactionData, + network = network, + userWallet = userWallet, + ).getOrNull() ?: error("unable to calculate fee") } catch (e: IllegalStateException) { - transactionManager.getFeeForGas( - networkId = networkId, - gas = transaction.gas.multiply(INCREASE_GAS_LIMIT_BY.toBigInteger()).divide(100.toBigInteger()), - derivationPath = fromToken.network.derivationPath.value, - ) + getEthSpecificFeeUseCase( + userWallet = userWallet, + cryptoCurrency = fromToken, + gasLimit = transaction.gas, + ).getOrNull() ?: error("can't get fee for getEthSpecificFeeUseCase") } } @@ -1575,7 +1331,11 @@ internal class SwapInteractorImpl @AssistedInject constructor( ): TxFeeState { return txFeeResult?.fold( ifLeft = { TxFeeState.Empty }, - ifRight = { txFee -> txFee.toTxFeeState(fromToken.currency, null) }, + ifRight = { txFee -> + txFee + .patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_SEND) + .toTxFeeState(fromToken.currency, null) + }, ) ?: TxFeeState.Empty } @@ -1625,35 +1385,42 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount = swapAmount.value.convertToSdkAmount(fromToken), blockchain = Blockchain.fromId(fromToken.network.id.value), ) - val cardId = userWallet.scanResponse.card.cardId - val feeData = if (isDemoCardUseCase(cardId)) { - getDemoFees(fromTokenStatus.currency) - } else { - try { - transactionManager.getFee( - networkId = networkId, - amountToSend = createNativeAmountForDex("0", fromToken.network), - currencyToSend = swapCurrencyConverter.convert(repository.getNativeTokenForNetwork(networkId)), - destinationAddress = fromToken.getContractAddress(), - increaseBy = INCREASE_GAS_LIMIT_BY, - callData = callData, - derivationPath = derivationPath, - ) - } catch (e: Exception) { - Timber.e(e, "Failed to get fee") - // it's impossible next steps without fee - return createSwapErrorWith( - fromToken = fromTokenStatus, - amount = swapAmount, - includeFeeInAmount = IncludeFeeInAmount.Excluded, - expressDataError = ExpressDataError.UnknownError, - ) - } - } - val feeState = when (feeData) { - is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(fromToken) - is ProxyFees.SingleFee -> feeData.proxyFeesToFeeState(fromToken) + val feeData = try { + val extras = createTransactionExtrasUseCase( + callData = callData, + network = fromToken.network, + ).getOrNull() ?: error("unable to create extras") + + val fromAddress = requireNotNull( + fromTokenStatus.value.networkAddress?.defaultAddress?.value, + ) { "networkAddress cant be null" } + val transactionData = TransactionData.Uncompiled( + amount = createNativeAmountForDex("0", fromToken.network), + destinationAddress = fromToken.getContractAddress(), + fee = null, + sourceAddress = fromAddress, + extras = extras, + ) + getFeeUseCase( + transactionData = transactionData, + network = fromToken.network, + userWallet = userWallet, + ).getOrNull() ?: error("unable to calculate fee") + } catch (e: Exception) { + Timber.e(e, "Failed to get fee") + // it's impossible next steps without fee + return createSwapErrorWith( + fromToken = fromTokenStatus, + amount = swapAmount, + includeFeeInAmount = IncludeFeeInAmount.Excluded, + expressDataError = ExpressDataError.UnknownError, + ) } + + val feeState = feeData + .patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX) + .toTxFeeState(fromToken, null) + val fee = when (feeState) { TxFeeState.Empty -> BigDecimal.ZERO is TxFeeState.MultipleFeeState -> feeState.normalFee.feeValue @@ -1683,120 +1450,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } - private suspend fun ProxyFees.MultipleFees.proxyFeesToFeeState( - fromToken: CryptoCurrency, - otherNativeFee: BigDecimal? = null, - ): TxFeeState { - val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO - val normalFeeValue = this.minFee.fee.value // in swap for normal use min fee - val normalFeeGas = this.minFee.gasLimit.toInt() - val priorityFeeValue = this.normalFee.fee.value // in swap for priority use normal fee - val priorityFeeGas = this.normalFee.gasLimit.toInt() - // region fees to use - val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue, priorityFeeValue) - val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } - val priorityFiatFee = requireNotNull(feesFiat.getOrNull(1)) { "feesFiat item 1 couldn't be null" } - val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( - amount = normalFeeValue, - decimals = minFee.fee.decimals, - ) - val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( - amount = priorityFeeValue, - decimals = normalFee.fee.decimals, - ) - // endregion - // region fees include otherNativeFee - val feesFiatWithNative = getFormattedFiatFees( - fromToken = fromToken, - normalFeeValue + otherNativeFeeValue, - priorityFeeValue + otherNativeFeeValue, - ) - val normalFiatFeeWithNative = - requireNotNull(feesFiatWithNative.getOrNull(0)) { "feesFiat item 0 couldn't be null" } - val priorityFiatFeeWithNative = - requireNotNull(feesFiatWithNative.getOrNull(1)) { "feesFiat item 1 couldn't be null" } - val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( - amount = normalFeeValue + otherNativeFeeValue, - decimals = minFee.fee.decimals, - ) - val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( - amount = priorityFeeValue + otherNativeFeeValue, - decimals = normalFee.fee.decimals, - ) - // endregion - return TxFeeState.MultipleFeeState( - normalFee = TxFee( - feeValue = normalFeeValue, - gasLimit = normalFeeGas, - feeFiatFormatted = normalFiatFee, - feeCryptoFormatted = normalCryptoFee, - feeIncludeOtherNativeFee = normalFeeValue + otherNativeFeeValue, - feeFiatFormattedWithNative = normalFiatFeeWithNative, - feeCryptoFormattedWithNative = normalCryptoFeeWithNative, - decimals = minFee.fee.decimals, - cryptoSymbol = minFee.fee.currencySymbol, - feeType = FeeType.NORMAL, - params = getSwapFeeParams(minFee), - ), - priorityFee = TxFee( - feeValue = priorityFeeValue, - gasLimit = priorityFeeGas, - feeFiatFormatted = priorityFiatFee, - feeCryptoFormatted = priorityCryptoFee, - feeIncludeOtherNativeFee = priorityFeeValue + otherNativeFeeValue, - feeFiatFormattedWithNative = priorityFiatFeeWithNative, - feeCryptoFormattedWithNative = priorityCryptoFeeWithNative, - decimals = normalFee.fee.decimals, - cryptoSymbol = normalFee.fee.currencySymbol, - feeType = FeeType.PRIORITY, - params = getSwapFeeParams(normalFee), - ), - ) - } - - private suspend fun ProxyFees.SingleFee.proxyFeesToFeeState( - fromToken: CryptoCurrency, - otherNativeFee: BigDecimal? = null, - ): TxFeeState { - val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO - val normalFeeValue = this.singleFee.fee.value - val normalFeeGas = this.singleFee.gasLimit.toInt() - val feesFiat = getFormattedFiatFees(fromToken, normalFeeValue) - val normalFiatFee = requireNotNull(feesFiat.getOrNull(0)) { "feesFiat item 0 couldn't be null" } - val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( - amount = normalFeeValue, - decimals = singleFee.fee.decimals, - ) - // region fees include otherNativeFee - val feesFiatWithNative = getFormattedFiatFees( - fromToken = fromToken, - normalFeeValue + otherNativeFeeValue, - normalFeeValue + otherNativeFeeValue, - ) - val normalFiatFeeWithNative = - requireNotNull(feesFiatWithNative.getOrNull(0)) { "feesFiat item 0 couldn't be null" } - val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( - amount = normalFeeValue + otherNativeFeeValue, - decimals = singleFee.fee.decimals, - ) - // endregion - return TxFeeState.SingleFeeState( - fee = TxFee( - feeValue = normalFeeValue, - gasLimit = normalFeeGas, - feeFiatFormatted = normalFiatFee, - feeCryptoFormatted = normalCryptoFee, - feeIncludeOtherNativeFee = normalFeeValue + otherNativeFeeValue, - feeFiatFormattedWithNative = normalFiatFeeWithNative, - feeCryptoFormattedWithNative = normalCryptoFeeWithNative, - decimals = singleFee.fee.decimals, - cryptoSymbol = singleFee.fee.currencySymbol, - feeType = FeeType.NORMAL, - params = getSwapFeeParams(singleFee), - ), - ) - } - @Suppress("LongMethod") private suspend fun TransactionFee.toTxFeeState( fromToken: CryptoCurrency, @@ -1805,20 +1458,18 @@ internal class SwapInteractorImpl @AssistedInject constructor( val otherNativeFeeValue = otherNativeFee ?: BigDecimal.ZERO return when (this) { is TransactionFee.Choosable -> { - val normalFee = this.normal.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND) - val priorityFee = this.priority.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND) - val feeNormal = normalFee.amount.value ?: BigDecimal.ZERO - val feePriority = priorityFee.amount.value ?: BigDecimal.ZERO + val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO + val feePriority = this.priority.amount.value ?: BigDecimal.ZERO val normalFiatValue = getFormattedFiatFees(fromToken, feeNormal)[0] val priorityFiatValue = getFormattedFiatFees(fromToken, feePriority)[0] val normalCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = feeNormal, - decimals = normalFee.amount.decimals, + decimals = this.normal.amount.decimals, ) val priorityCryptoFee = amountFormatter.formatBigDecimalAmountToUI( amount = feePriority, - decimals = priorityFee.amount.decimals, + decimals = this.priority.amount.decimals, ) // region otherNativeFee @@ -1829,39 +1480,35 @@ internal class SwapInteractorImpl @AssistedInject constructor( val normalCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( amount = normalFeeWithOtherNative, - decimals = normalFee.amount.decimals, + decimals = this.normal.amount.decimals, ) val priorityCryptoFeeWithNative = amountFormatter.formatBigDecimalAmountToUI( amount = priorityFeeWithOtherNative, - decimals = priorityFee.amount.decimals, + decimals = this.priority.amount.decimals, ) // endregion TxFeeState.MultipleFeeState( normalFee = TxFee( feeValue = feeNormal, - gasLimit = normalFee.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, feeIncludeOtherNativeFee = normalFeeWithOtherNative, feeFiatFormattedWithNative = normalFiatValueWithNative, feeCryptoFormattedWithNative = normalCryptoFeeWithNative, - decimals = normalFee.amount.decimals, - cryptoSymbol = normalFee.amount.currencySymbol, + cryptoSymbol = this.normal.amount.currencySymbol, feeType = FeeType.NORMAL, - params = getSwapFeeParams(normalFee), + fee = this.normal, ), priorityFee = TxFee( feeValue = feePriority, - gasLimit = priorityFee.getGasLimit(), feeFiatFormatted = priorityFiatValue, feeCryptoFormatted = priorityCryptoFee, feeIncludeOtherNativeFee = priorityFeeWithOtherNative, feeFiatFormattedWithNative = priorityFiatValueWithNative, feeCryptoFormattedWithNative = priorityCryptoFeeWithNative, - decimals = priorityFee.amount.decimals, - cryptoSymbol = priorityFee.amount.currencySymbol, + cryptoSymbol = this.priority.amount.currencySymbol, feeType = FeeType.PRIORITY, - params = getSwapFeeParams(priorityFee), + fee = this.priority, ), ) } @@ -1884,59 +1531,20 @@ internal class SwapInteractorImpl @AssistedInject constructor( TxFeeState.SingleFeeState( fee = TxFee( feeValue = this.normal.amount.value ?: BigDecimal.ZERO, - gasLimit = this.normal.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, feeIncludeOtherNativeFee = normalFeeWithOtherNative, feeFiatFormattedWithNative = normalFiatValueWithNative, feeCryptoFormattedWithNative = normalCryptoFeeWithNative, - decimals = normal.amount.decimals, cryptoSymbol = normal.amount.currencySymbol, feeType = FeeType.NORMAL, - params = getSwapFeeParams(normal), + fee = this.normal, ), ) } } } - private fun getSwapFeeParams(fee: Fee): TxFee.Params? = when (fee) { - is Fee.Filecoin -> TxFee.Params.Filecoin( - gasPremium = fee.gasPremium, - ) - is Fee.Sui -> TxFee.Params.Sui( - gasPrice = fee.gasPrice, - gasBudget = fee.gasBudget, - ) - is Fee.Hedera -> TxFee.Params.Hedera( - additionalHBARFee = fee.additionalHBARFee, - ) - is Fee.Aptos, - is Fee.Bitcoin, - is Fee.CardanoToken, - is Fee.Common, - is Fee.Ethereum.EIP1559, - is Fee.Ethereum.Legacy, - is Fee.Kaspa, - is Fee.Tron, - is Fee.VeChain, - is Fee.Alephium, - -> null - } - - private fun getSwapFeeParams(proxyFee: ProxyFee): TxFee.Params? = when (proxyFee) { - is ProxyFee.Filecoin -> TxFee.Params.Filecoin( - gasPremium = proxyFee.gasPremium, - ) - is ProxyFee.Sui -> TxFee.Params.Sui( - gasPrice = proxyFee.gasPrice, - gasBudget = proxyFee.gasBudget, - ) - is ProxyFee.CardanoToken, - is ProxyFee.Common, - -> null - } - private fun createNativeAmountForDex(txValueAmount: String, network: Network): Amount { val nativeDecimals = Blockchain.fromNetworkId(network.backendId)?.decimals() ?: error("Blockchain not found") @@ -1950,7 +1558,50 @@ internal class SwapInteractorImpl @AssistedInject constructor( } /** - * Workaround to increase gas limit cause we calculate fee for random address + * We need to increase gasLimit for Ethereum fees for 2 cases + * + * DEX: for dex calculated gasLimit for given data might be changed when transaction processing + * for that case dex providers recommend to increase gasLimit for few percents to ensure transaction completes + * + * CEX: for that case we calculate fee for random generated address and gasLimit might be different for it + * and result address to send. That's why we should increase gasLimit a little + * + */ + private fun TransactionFee.patchTransactionFeeForSwap(increaseBy: Int): TransactionFee { + return when (this) { + is TransactionFee.Choosable -> { + this.copy( + minimum = this.minimum.increaseEthGasLimitInNeeded(increaseBy), + normal = this.normal.increaseEthGasLimitInNeeded(increaseBy), + priority = this.priority.increaseEthGasLimitInNeeded(increaseBy), + ) + } + is TransactionFee.Single -> this.copy(normal = this.normal.increaseEthGasLimitInNeeded(increaseBy)) + } + } + + private fun Fee.increaseEthGasLimitInNeeded(increaseBy: Int): Fee { + return when (this) { + is Fee.Ethereum.EIP1559, + is Fee.Ethereum.Legacy, + -> this.increaseGasLimitBy(increaseBy) + is Fee.Alephium, + is Fee.Aptos, + is Fee.Bitcoin, + is Fee.CardanoToken, + is Fee.Common, + is Fee.Filecoin, + is Fee.Hedera, + is Fee.Kaspa, + is Fee.Sui, + is Fee.Tron, + is Fee.VeChain, + -> this + } + } + + /** + * Increase gasLimit for Fee.Ethereum */ private fun Fee.increaseGasLimitBy(percentage: Int): Fee { if (this !is Fee.Ethereum) return this @@ -1973,13 +1624,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( return cryptoCurrencyStatuses.value.pendingTransactions.any { it.isOutgoing } } - private fun Fee.getGasLimit(): Int { + private fun Fee.getGasLimit(): Int? { return when (this) { is Fee.Ethereum -> gasLimit.toInt() is Fee.VeChain -> gasLimit.toInt() is Fee.Aptos -> gasLimit.toInt() is Fee.Filecoin -> gasLimit.toInt() - else -> 0 + else -> null } } @@ -2163,36 +1814,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( } } - private fun getDemoFees(cryptoCurrency: CryptoCurrency): ProxyFees.MultipleFees { - val demoFee = ProxyAmount( - currencySymbol = cryptoCurrency.symbol, - value = minDemoFee, - decimals = cryptoCurrency.decimals, - ) - return ProxyFees.MultipleFees( - minFee = ProxyFee.Common( - gasLimit = 1.toBigInteger(), - fee = demoFee, - ), - normalFee = ProxyFee.Common( - gasLimit = 1.toBigInteger(), - fee = demoFee.copy(value = normalDemoFee), - ), - priorityFee = ProxyFee.Common( - gasLimit = 1.toBigInteger(), - fee = demoFee.copy(value = priorityDemoFee), - ), - ) - } - companion object { - private const val INCREASE_GAS_LIMIT_BY = 112 // 12% + private const val INCREASE_GAS_LIMIT_FOR_DEX = 112 // 12% private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" - - private val minDemoFee = "0.0001".toBigDecimal() - private val normalDemoFee = "0.0002".toBigDecimal() - private val priorityDemoFee = "0.0003".toBigDecimal() } @AssistedFactory diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 13135efa71..610aa556a8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -3,7 +3,6 @@ package com.tangem.feature.swap.domain.di import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations import com.tangem.feature.swap.domain.* -import com.tangem.lib.crypto.TransactionManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -25,14 +24,6 @@ internal class SwapDomainModule { return factory } - @Provides - @Singleton - fun provideBlockchainInteractor(transactionManager: TransactionManager): BlockchainInteractor { - return DefaultBlockchainInteractor( - transactionManager = transactionManager, - ) - } - @Provides @Singleton fun providesGetCryptoCurrencyStatusUseCase( diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index a07e4897ad..74a34ecd45 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -40,6 +40,8 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.promo) implementation(projects.domain.promo.models) + implementation(projects.domain.txhistory) + implementation(projects.domain.txhistory.models) /** Feature modules */ implementation(projects.features.swap.domain) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index ea78698ede..38779307ed 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -35,12 +35,12 @@ import com.tangem.domain.tokens.* import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.SwapEvents -import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.ExpressException @@ -80,7 +80,6 @@ typealias SuccessLoadedSwapData = Map internal class SwapModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val blockchainInteractor: BlockchainInteractor, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsErrorEventHandler: AnalyticsErrorHandler, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -93,6 +92,7 @@ internal class SwapModel @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val shouldShowStoriesUseCase: ShouldShowStoriesUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -135,7 +135,7 @@ internal class SwapModel @Inject constructor( stateBuilder.createInitialLoadingState( initialCurrencyFrom = initialCurrencyFrom, initialCurrencyTo = initialCurrencyTo, - fromNetworkInfo = blockchainInteractor.getBlockchainInfo(initialCurrencyFrom.network.backendId), + fromNetworkInfo = initialCurrencyFrom.getNetworkInfo(), ), ) private set @@ -261,9 +261,7 @@ internal class SwapModel @Inject constructor( uiState = stateBuilder.createInitialLoadingState( initialCurrencyFrom = initialCurrencyFrom, initialCurrencyTo = initialCurrencyTo, - fromNetworkInfo = blockchainInteractor.getBlockchainInfo( - initialCurrencyFrom.network.backendId, - ), + fromNetworkInfo = initialCurrencyFrom.getNetworkInfo(), ) initTokens(isReverseFromTo) } @@ -603,10 +601,14 @@ internal class SwapModel @Inject constructor( when (it) { is SwapTransactionState.TxSent -> { sendSuccessSwapEvent(fromCurrency.currency, fee.feeType) - val url = blockchainInteractor.getExplorerTransactionLink( - networkId = fromCurrency.currency.network.backendId, + val url = getExplorerTransactionUrlUseCase( txHash = it.txHash, - ) + networkId = fromCurrency.currency.network.id, + ).getOrElse { + Timber.i("tx hash explore not supported") + "" + } + updateWalletBalance() uiState = stateBuilder.createSuccessState( uiState = uiState, @@ -1356,6 +1358,13 @@ internal class SwapModel @Inject constructor( } } + private fun CryptoCurrency.getNetworkInfo(): NetworkInfo { + return NetworkInfo( + name = this.network.name, + blockchainId = this.network.id.value, + ) + } + private companion object { const val INITIAL_AMOUNT = "" const val UPDATE_DELAY = 10000L 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 deleted file mode 100644 index baed3f3725..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ /dev/null @@ -1,50 +0,0 @@ -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 - -interface TransactionManager { - - /** - * Get fee - * - * @param networkId network id of blockchain - * @param amountToSend amount - * @param currencyToSend currency to send in tx - * @param destinationAddress address to send tx - * @param increaseBy percents in format 125 = 25% - * @param callData tx smart contract call data - * @param derivationPath derivation path - * @return - */ - @Suppress("LongParameterList") - @Throws(IllegalStateException::class) - suspend fun getFee( - networkId: String, - amountToSend: Amount, - currencyToSend: Currency, - destinationAddress: String, - increaseBy: Int?, - callData: SmartContractCallData?, - derivationPath: String?, - ): ProxyFees - - @Throws(IllegalStateException::class) - suspend fun getFeeForGas(networkId: String, gas: BigInteger, derivationPath: String?): ProxyFees - - @Throws(IllegalStateException::class) - suspend fun updateWalletManager(networkId: String, derivationPath: String?) - - /** - * In app blockchain id, actual in blockchain sdk, not the same as networkId - * - * workaround till not use backend only and not integrated server vs sdk - */ - @Throws(IllegalStateException::class) - fun getBlockchainInfo(networkId: String): ProxyNetworkInfo - - @Throws(IllegalStateException::class) - fun getExplorerTransactionLink(networkId: String, txAddress: String): String -} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt deleted file mode 100644 index 6cb324fcef..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFee.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.tangem.lib.crypto.models - -import java.math.BigDecimal -import java.math.BigInteger - -sealed interface ProxyFee { - - val gasLimit: BigInteger - val fee: ProxyAmount - - data class Common( - override val gasLimit: BigInteger, - override val fee: ProxyAmount, - ) : ProxyFee - - data class CardanoToken( - override val gasLimit: BigInteger, - override val fee: ProxyAmount, - val minAdaValue: BigDecimal, - ) : ProxyFee - - data class Filecoin( - override val gasLimit: BigInteger, - override val fee: ProxyAmount, - val gasPremium: Long, - ) : ProxyFee - - data class Sui( - override val gasLimit: BigInteger, - override val fee: ProxyAmount, - val gasPrice: Long, - val gasBudget: Long, - ) : ProxyFee -} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt deleted file mode 100644 index b8b3dfa295..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyFees.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.lib.crypto.models - -sealed class ProxyFees { - - data class MultipleFees( - val minFee: ProxyFee, - val normalFee: ProxyFee, - val priorityFee: ProxyFee, - ) : ProxyFees() - - data class SingleFee( - val singleFee: ProxyFee, - ) : ProxyFees() -} \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyNetworkInfo.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyNetworkInfo.kt deleted file mode 100644 index dcbade5046..0000000000 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/models/ProxyNetworkInfo.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.tangem.lib.crypto.models - -data class ProxyNetworkInfo( - val name: String, - val blockchainId: String, -) \ No newline at end of file From c227fd1001d77c7648771cf3c368a620327e6f32 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 17:33:39 +0500 Subject: [PATCH 099/151] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 2 +- core/res/src/main/res/values/strings.xml | 1 - .../core/ui/components/rows/ChainRow.kt | 1 + .../ui/components/rows/model/ChainRowUM.kt | 1 + .../tangem/data/nft/DefaultNFTRepository.kt | 23 +++ .../tangem/domain/nft/models/NFTNetworks.kt | 8 + .../nft/FilterNFTAvailableNetworksUseCase.kt | 16 +- .../nft/GetNFTAvailableNetworksUseCase.kt | 22 --- .../domain/nft/GetNFTNetworksUseCase.kt | 32 ++++ .../domain/nft/repository/NFTRepository.kt | 2 + .../nft/receive/entity/NFTNetworkUM.kt | 5 +- .../nft/receive/entity/NFTReceiveUM.kt | 3 +- .../transformer/UpdateDataStateTransformer.kt | 46 +++-- .../nft/receive/model/NFTReceiveModel.kt | 65 ++++--- .../features/nft/receive/ui/NFTNetwork.kt | 73 -------- .../features/nft/receive/ui/NFTReceive.kt | 16 +- .../receive/ui/NFTReceiveNetworksContent.kt | 168 ++++++++++++++---- .../nft/receive/ui/NFTReceiveNetworksEmpty.kt | 2 + 18 files changed, 299 insertions(+), 187 deletions(-) create mode 100644 domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTNetworks.kt delete mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTAvailableNetworksUseCase.kt create mode 100644 domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt delete mode 100644 features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTNetwork.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 697065bb97..0578e95316 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 @@ -57,7 +57,7 @@ internal object NFTDomainModule { fun providesGetNFTAvailableNetworksUseCase( nftRepository: NFTRepository, currenciesRepository: CurrenciesRepository, - ): GetNFTAvailableNetworksUseCase = GetNFTAvailableNetworksUseCase( + ): GetNFTNetworksUseCase = GetNFTNetworksUseCase( currenciesRepository = currenciesRepository, nftRepository = nftRepository, ) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index bda274d668..94ac2266d6 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -546,7 +546,6 @@ You haven\'t added this network yet. To start receiving NFTs, add it to the main screen! Network not added Not Added - Send NFT Traits %1$d NFTs in %2$d collection Tap here to receive first NFT diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt index 7b8f2c9fff..a9020f35ae 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/ChainRow.kt @@ -42,6 +42,7 @@ fun ChainRow(model: ChainRowUM, modifier: Modifier = Modifier, action: @Composab } else { null }, + isEnabled = model.enabled, accentMainText = true, accentSecondText = false, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt index c0c3764c5e..5b5f0ebd21 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/rows/model/ChainRowUM.kt @@ -9,4 +9,5 @@ data class ChainRowUM( val type: String, val icon: CurrencyIconState, val showCustom: Boolean, + val enabled: Boolean = true, ) \ 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 7fe4317d15..b19fef322d 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 @@ -2,7 +2,9 @@ package com.tangem.data.nft import arrow.core.Either import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.local.nft.NFTPersistenceStore import com.tangem.datasource.local.nft.NFTPersistenceStoreFactory import com.tangem.datasource.local.nft.NFTRuntimeStore @@ -10,6 +12,7 @@ import com.tangem.datasource.local.nft.NFTRuntimeStoreFactory 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.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.models.StatusSource import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTCollection @@ -37,6 +40,8 @@ internal class DefaultNFTRepository @Inject constructor( private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory, private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, + private val excludedBlockchains: ExcludedBlockchains, + private val userWalletsStore: UserWalletsStore, ) : NFTRepository { private val networkJobs = ConcurrentHashMap() @@ -129,6 +134,24 @@ internal class DefaultNFTRepository @Inject constructor( override suspend fun isNFTSupported(network: Network): Boolean = network.canHandleNFTs() + override suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List { + val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { + "User wallet [$userWalletId] not found" + } + return Blockchain + .entries + .filter { it.canHandleNFTs() } + .filter { !it.isTestnet() } + .mapNotNull { + getNetwork( + blockchain = it, + extraDerivationPath = null, + scanResponse = userWallet.scanResponse, + excludedBlockchains = excludedBlockchains, + ) + } + } + override suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? = walletManagersFacade.getNFTExploreUrl( network = network, diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTNetworks.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTNetworks.kt new file mode 100644 index 0000000000..14cb57f3e7 --- /dev/null +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTNetworks.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.nft.models + +import com.tangem.domain.tokens.model.Network + +data class NFTNetworks( + val availableNetworks: List, + val notAvailableNetworks: List, +) \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt index 3308232789..ca8aff4b42 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.nft +import com.tangem.domain.nft.models.NFTNetworks import com.tangem.domain.tokens.model.Network import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -7,11 +8,16 @@ import kotlinx.coroutines.withContext class FilterNFTAvailableNetworksUseCase( private val dispatchers: CoroutineDispatcherProvider, ) { - suspend operator fun invoke(networks: List, searchQuery: String): List = + suspend operator fun invoke(networks: NFTNetworks, searchQuery: String): NFTNetworks = withContext(dispatchers.default) { - networks.filter { - it.name.contains(searchQuery, ignoreCase = true) || - it.currencySymbol.contains(searchQuery, ignoreCase = true) - } + NFTNetworks( + availableNetworks = networks.availableNetworks.filter(searchQuery), + notAvailableNetworks = networks.notAvailableNetworks.filter(searchQuery), + ) } + + private fun List.filter(searchQuery: String) = filter { + it.name.contains(searchQuery, ignoreCase = true) || + it.currencySymbol.contains(searchQuery, ignoreCase = true) + } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTAvailableNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTAvailableNetworksUseCase.kt deleted file mode 100644 index 203f12e012..0000000000 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTAvailableNetworksUseCase.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.domain.nft - -import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.wallets.models.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class GetNFTAvailableNetworksUseCase( - private val currenciesRepository: CurrenciesRepository, - private val nftRepository: NFTRepository, -) { - operator fun invoke(userWalletId: UserWalletId): Flow> = currenciesRepository - .getWalletCurrenciesUpdates(userWalletId) - .map { cryptoCurrencies -> - cryptoCurrencies - .map { cryptoCurrency -> cryptoCurrency.network } - .distinct() - .filter { nftRepository.isNFTSupported(it) } - } -} \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt new file mode 100644 index 0000000000..ca8a8c22bd --- /dev/null +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt @@ -0,0 +1,32 @@ +package com.tangem.domain.nft + +import com.tangem.domain.nft.models.NFTNetworks +import com.tangem.domain.nft.repository.NFTRepository +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +class GetNFTNetworksUseCase( + private val currenciesRepository: CurrenciesRepository, + private val nftRepository: NFTRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow = currenciesRepository + .getWalletCurrenciesUpdates(userWalletId) + .map { cryptoCurrencies -> + val availableNetworks = cryptoCurrencies + .map { cryptoCurrency -> cryptoCurrency.network } + .filter { nftRepository.isNFTSupported(it) } + .sortedBy { it.name } + + val notAvailableNetworks = nftRepository + .getNFTSupportedNetworks(userWalletId) + .filter { supportedNetwork -> availableNetworks.none { it.id == supportedNetwork.id } } + .sortedBy { it.name } + + NFTNetworks( + availableNetworks = availableNetworks, + notAvailableNetworks = notAvailableNetworks, + ) + } +} \ 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 ea99cbca2a..1484b241a3 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 @@ -18,5 +18,7 @@ interface NFTRepository { suspend fun isNFTSupported(network: Network): Boolean + suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List + suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTNetworkUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTNetworkUM.kt index 0ef73aa689..dc3c6ae968 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTNetworkUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTNetworkUM.kt @@ -1,10 +1,9 @@ package com.tangem.features.nft.receive.entity -import androidx.annotation.DrawableRes +import com.tangem.core.ui.components.rows.model.ChainRowUM internal data class NFTNetworkUM( val id: String, - @DrawableRes val iconRes: Int, - val name: String, + val chainRowUM: ChainRowUM, val onItemClick: () -> Unit, ) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt index e31c80c425..eea9a23b79 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt @@ -19,7 +19,8 @@ internal data class NFTReceiveUM( data object Empty : Networks() data class Content( - val items: ImmutableList, + val availableItems: ImmutableList, + val notAvailableItems: ImmutableList, ) : Networks() } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt index 560bf018f7..c113d54905 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt @@ -1,6 +1,9 @@ package com.tangem.features.nft.receive.entity.transformer +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.rows.model.ChainRowUM import com.tangem.core.ui.extensions.getActiveIconRes +import com.tangem.domain.nft.models.NFTNetworks import com.tangem.domain.tokens.model.Network import com.tangem.features.nft.receive.entity.NFTNetworkUM import com.tangem.features.nft.receive.entity.NFTReceiveUM @@ -8,27 +11,44 @@ import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList internal class UpdateDataStateTransformer( - private val networks: List, - private val onNetworkClick: (Network) -> Unit, + private val networks: NFTNetworks, + private val onNetworkClick: (Network, Boolean) -> Unit, ) : Transformer { override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy( networks = when { - networks.isEmpty() -> NFTReceiveUM.Networks.Empty + networks.availableNetworks.isEmpty() && networks.notAvailableNetworks.isEmpty() + -> NFTReceiveUM.Networks.Empty else -> NFTReceiveUM.Networks.Content( - items = networks - .map { it.transform() } + availableItems = networks.availableNetworks + .map { it.transform(true) } + .toPersistentList(), + notAvailableItems = networks.notAvailableNetworks + .map { it.transform(false) } .toPersistentList(), ) }, ) - private fun Network.transform(): NFTNetworkUM = NFTNetworkUM( - id = id.value, - iconRes = getActiveIconRes(id.value), - name = name, - onItemClick = { - onNetworkClick(this) - }, - ) + private fun Network.transform(enabled: Boolean): NFTNetworkUM { + val custom = derivationPath is Network.DerivationPath.Custom + return NFTNetworkUM( + id = id.value + derivationPath.value, + chainRowUM = ChainRowUM( + name = name, + type = "", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = getActiveIconRes(id.value), + isGrayscale = !enabled, + showCustomBadge = custom, + ), + showCustom = custom, + enabled = enabled, + ), + onItemClick = { + onNetworkClick(this, enabled) + }, + ) + } } \ 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 88a9b57851..090d51ff5c 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 @@ -4,14 +4,16 @@ 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.ui.UiMessageSender import com.tangem.core.navigation.share.ShareManager import com.tangem.core.ui.clipboard.ClipboardManager 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.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.nft.FilterNFTAvailableNetworksUseCase -import com.tangem.domain.nft.GetNFTAvailableNetworksUseCase +import com.tangem.domain.nft.GetNFTNetworksUseCase import com.tangem.domain.nft.GetNFTNetworkStatusUseCase import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.tokens.model.Network @@ -34,12 +36,13 @@ import javax.inject.Inject internal class NFTReceiveModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val searchManager: InputManager, - private val getNFTAvailableNetworksUseCase: GetNFTAvailableNetworksUseCase, + private val getNFTNetworksUseCase: GetNFTNetworksUseCase, private val filterNFTAvailableNetworksUseCase: FilterNFTAvailableNetworksUseCase, private val getNFTNetworkStatusUseCase: GetNFTNetworkStatusUseCase, private val clipboardManager: ClipboardManager, private val shareManager: ShareManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val messageSender: UiMessageSender, paramsContainer: ParamsContainer, ) : Model() { @@ -53,7 +56,10 @@ internal class NFTReceiveModel @Inject constructor( formatArgs = wrappedList(params.walletName), ), search = getInitialSearchBar(), - networks = NFTReceiveUM.Networks.Content(persistentListOf()), + networks = NFTReceiveUM.Networks.Content( + availableItems = persistentListOf(), + notAvailableItems = persistentListOf(), + ), bottomSheetConfig = null, ), ) @@ -67,7 +73,7 @@ internal class NFTReceiveModel @Inject constructor( private fun subscribeToNFTAvailableNetworks() { combine( - flow = getNFTAvailableNetworksUseCase(params.userWalletId), + flow = getNFTNetworksUseCase(params.userWalletId), flow2 = searchManager.query.distinctUntilChanged(), ) { networks, query -> filterNFTAvailableNetworksUseCase(networks, query) @@ -113,31 +119,40 @@ internal class NFTReceiveModel @Inject constructor( } } - private fun onNetworkClick(network: Network) { - modelScope.launch { - analyticsEventHandler.send(NFTAnalyticsEvent.Receive.BlockchainChosen(network.name)) + private fun onNetworkClick(network: Network, enabled: Boolean) { + if (!enabled) { + val message = DialogMessage( + title = resourceReference(R.string.nft_receive_unavailable_asset_warning_title), + message = resourceReference(R.string.nft_receive_unavailable_asset_warning_message), + ) - val networkStatus = getNFTNetworkStatusUseCase.invoke( - userWalletId = params.userWalletId, - network = network, - ) ?: return@launch + messageSender.send(message) + } else { + modelScope.launch { + analyticsEventHandler.send(NFTAnalyticsEvent.Receive.BlockchainChosen(network.name)) - when (val value = networkStatus.value) { - is NetworkStatus.Verified -> { - _state.update { - ShowReceiveBottomSheetTransformer( - network = network, - networkAddress = value.address, - onDismissBottomSheet = ::onReceiveBottomSheetDismiss, - onCopyClick = { text -> onCopyClick(text, network) }, - onShareClick = { text -> onShareClick(text, network) }, - ).transform(it) + val networkStatus = getNFTNetworkStatusUseCase.invoke( + userWalletId = params.userWalletId, + network = network, + ) ?: return@launch + + when (val value = networkStatus.value) { + is NetworkStatus.Verified -> { + _state.update { + ShowReceiveBottomSheetTransformer( + network = network, + networkAddress = value.address, + onDismissBottomSheet = ::onReceiveBottomSheetDismiss, + onCopyClick = { text -> onCopyClick(text, network) }, + onShareClick = { text -> onShareClick(text, network) }, + ).transform(it) + } } + is NetworkStatus.MissedDerivation, + is NetworkStatus.NoAccount, + is NetworkStatus.Unreachable, + -> Unit } - is NetworkStatus.MissedDerivation, - is NetworkStatus.NoAccount, - is NetworkStatus.Unreachable, - -> Unit } } } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTNetwork.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTNetwork.kt deleted file mode 100644 index 9b60320b49..0000000000 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTNetwork.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.nft.receive.ui - -import android.content.res.Configuration -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import 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 com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.nft.impl.R -import com.tangem.features.nft.receive.entity.NFTNetworkUM - -@Composable -internal fun NFTNetwork(state: NFTNetworkUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .fillMaxWidth() - .clickable { state.onItemClick() } - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - Image( - modifier = Modifier - .size(TangemTheme.dimens.size24) - .clip(CircleShape), - painter = painterResource(state.iconRes), - contentDescription = null, - ) - Text( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing12), - text = state.name, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } -} - -@Preview(widthDp = 360, showBackground = true) -@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_NFTNetwork(@PreviewParameter(NFTNetworkProvider::class) state: NFTNetworkUM) { - TangemThemePreview { - NFTNetwork( - state = state, - ) - } -} - -private class NFTNetworkProvider : CollectionPreviewParameterProvider( - collection = listOf( - NFTNetworkUM( - id = "item1", - name = "Nethers #0853", - iconRes = R.drawable.img_eth_22, - onItemClick = { }, - ), - ), -) \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt index 47a338514f..4dac3a8d5c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceive.kt @@ -1,7 +1,6 @@ package com.tangem.features.nft.receive.ui import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.* import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable @@ -51,18 +50,9 @@ internal fun NFTReceive(state: NFTReceiveUM, modifier: Modifier = Modifier) { colors = TangemSearchBarDefaults.secondaryTextFieldColors, ) - val contentModifier = Modifier - .padding(TangemTheme.dimens.spacing16) - - AnimatedContent( - targetState = state.networks, - contentKey = { it::class }, - label = "NFT Receive", - ) { - when (val networks = it) { - is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks, contentModifier) - is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty(contentModifier) - } + when (val networks = state.networks) { + is NFTReceiveUM.Networks.Content -> NFTReceiveNetworksContent(networks) + is NFTReceiveUM.Networks.Empty -> NFTReceiveNetworksEmpty() } } }, diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt index 23fab1ef15..8b01fd73b0 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt @@ -1,19 +1,29 @@ package com.tangem.features.nft.receive.ui import android.content.res.Configuration -import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.composed import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.rows.ChainRow +import com.tangem.core.ui.components.rows.model.ChainRowUM +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.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.nft.impl.R @@ -27,40 +37,86 @@ internal fun NFTReceiveNetworksContent(state: NFTReceiveUM.Networks.Content, mod LazyColumn( modifier = modifier - .fillMaxWidth() - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.primary) - .animateContentSize(), + .fillMaxSize() + .padding( + vertical = TangemTheme.dimens.spacing8, + ), state = listState, + contentPadding = PaddingValues( + horizontal = TangemTheme.dimens.spacing16, + ), ) { - item( - key = "title", - ) { - Title() + if (state.availableItems.isNotEmpty()) { + item( + key = "available_title", + ) { + Title(resourceReference(R.string.nft_receive_available_section_title)) + } + networks(state.availableItems) } - items( - items = state.items, - key = NFTNetworkUM::id, - ) { network -> - NFTNetwork( - state = network, - modifier = Modifier.animateItem(), - ) + if (state.notAvailableItems.isNotEmpty()) { + item( + key = "not_available_title", + ) { + Title(resourceReference(R.string.nft_receive_unavailable_section_title)) + } + networks(state.notAvailableItems) } } } +private fun LazyListScope.networks(items: List) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + ) { index, network -> + val isLastItem = index == items.size - 1 + ChainRow( + modifier = Modifier + .composed { + if (isLastItem) { + padding( + bottom = TangemTheme.dimens.spacing8, + ) + .clip( + RoundedCornerShape( + bottomStart = TangemTheme.dimens.radius16, + bottomEnd = TangemTheme.dimens.radius16, + ), + ) + } else { + this + } + } + .background(TangemTheme.colors.background.primary) + .clickable { network.onItemClick() }, + model = network.chainRowUM, + ) + } +} + @Composable -private fun Title(modifier: Modifier = Modifier) { +private fun Title(text: TextReference, modifier: Modifier = Modifier) { Text( modifier = modifier + .fillMaxWidth() + .padding( + top = TangemTheme.dimens.spacing8, + ) + .background( + color = TangemTheme.colors.background.primary, + shape = RoundedCornerShape( + topStart = TangemTheme.dimens.radius16, + topEnd = TangemTheme.dimens.radius16, + ), + ) .padding( start = TangemTheme.dimens.spacing12, top = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing4, ), - text = stringResourceSafe(R.string.nft_receive_choose_network), + text = text.resolveReference(), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) @@ -74,32 +130,84 @@ private fun Preview_NFTReceiveNetworksUM() { TangemThemePreview { NFTReceiveNetworksContent( state = NFTReceiveUM.Networks.Content( - items = persistentListOf( + availableItems = persistentListOf( NFTNetworkUM( id = "item1", - name = "Ethereum", - iconRes = R.drawable.img_eth_22, + chainRowUM = ChainRowUM( + name = "Ethereum", + type = "", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_eth_22, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ), onItemClick = { }, ), NFTNetworkUM( id = "item2", - name = "Polygon", - iconRes = R.drawable.img_polygon_22, + chainRowUM = ChainRowUM( + name = "Polygon", + type = "", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_polygon_22, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ), onItemClick = { }, ), NFTNetworkUM( id = "item3", - name = "BSC", - iconRes = R.drawable.img_bsc_22, + chainRowUM = ChainRowUM( + name = "BSC", + type = "", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_bsc_22, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ), onItemClick = { }, ), NFTNetworkUM( id = "item4", - name = "Avalanche", - iconRes = R.drawable.img_avalanche_22, + chainRowUM = ChainRowUM( + name = "Avalanche", + type = "", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_avalanche_22, + isGrayscale = false, + showCustomBadge = false, + ), + showCustom = false, + ), + onItemClick = { }, + ), + NFTNetworkUM( + id = "item5", + chainRowUM = ChainRowUM( + name = "Avalanche", + type = "", + icon = CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_avalanche_22, + isGrayscale = false, + showCustomBadge = true, + ), + showCustom = true, + ), onItemClick = { }, ), ), + notAvailableItems = persistentListOf(), ), ) } 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 931138a97d..368e7fd8fc 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 @@ -3,6 +3,7 @@ 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.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -16,6 +17,7 @@ internal fun NFTReceiveNetworksEmpty(modifier: Modifier = Modifier) { Box( modifier = modifier .fillMaxSize() + .padding(TangemTheme.dimens.spacing16) .background(TangemTheme.colors.background.secondary), ) { Text( From 0b8ff3e56e2281af7ef0fb6ca6c79920ca924e1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 14:43:35 +0500 Subject: [PATCH 100/151] Updated on 2026-08-14 --- .../kotlin/com/tangem/data/nft/DefaultNFTRepository.kt | 7 ++----- .../kotlin/com/tangem/domain/nft/models/NFTNetworks.kt | 2 +- .../tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt | 2 +- .../kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt | 4 ++-- .../com/tangem/features/nft/receive/entity/NFTReceiveUM.kt | 2 +- .../entity/transformer/UpdateDataStateTransformer.kt | 4 ++-- .../tangem/features/nft/receive/model/NFTReceiveModel.kt | 2 +- .../features/nft/receive/ui/NFTReceiveNetworksContent.kt | 6 +++--- 8 files changed, 13 insertions(+), 16 deletions(-) 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 b19fef322d..aa6b40e845 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 @@ -135,13 +135,10 @@ internal class DefaultNFTRepository @Inject constructor( override suspend fun isNFTSupported(network: Network): Boolean = network.canHandleNFTs() override suspend fun getNFTSupportedNetworks(userWalletId: UserWalletId): List { - val userWallet = requireNotNull(userWalletsStore.getSyncOrNull(userWalletId)) { - "User wallet [$userWalletId] not found" - } + val userWallet = userWalletsStore.getSyncStrict(userWalletId) return Blockchain .entries - .filter { it.canHandleNFTs() } - .filter { !it.isTestnet() } + .filter { it.canHandleNFTs() && !it.isTestnet() } .mapNotNull { getNetwork( blockchain = it, diff --git a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTNetworks.kt b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTNetworks.kt index 14cb57f3e7..2cdbcfa4a3 100644 --- a/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTNetworks.kt +++ b/domain/nft/models/src/main/kotlin/com/tangem/domain/nft/models/NFTNetworks.kt @@ -4,5 +4,5 @@ import com.tangem.domain.tokens.model.Network data class NFTNetworks( val availableNetworks: List, - val notAvailableNetworks: List, + val unavailableNetworks: List, ) \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt index ca8aff4b42..21f06eb54f 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/FilterNFTAvailableNetworksUseCase.kt @@ -12,7 +12,7 @@ class FilterNFTAvailableNetworksUseCase( withContext(dispatchers.default) { NFTNetworks( availableNetworks = networks.availableNetworks.filter(searchQuery), - notAvailableNetworks = networks.notAvailableNetworks.filter(searchQuery), + unavailableNetworks = networks.unavailableNetworks.filter(searchQuery), ) } diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt index ca8a8c22bd..8a87f0ca6f 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTNetworksUseCase.kt @@ -19,14 +19,14 @@ class GetNFTNetworksUseCase( .filter { nftRepository.isNFTSupported(it) } .sortedBy { it.name } - val notAvailableNetworks = nftRepository + val unavailableNetworks = nftRepository .getNFTSupportedNetworks(userWalletId) .filter { supportedNetwork -> availableNetworks.none { it.id == supportedNetwork.id } } .sortedBy { it.name } NFTNetworks( availableNetworks = availableNetworks, - notAvailableNetworks = notAvailableNetworks, + unavailableNetworks = unavailableNetworks, ) } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt index eea9a23b79..6bb4b1f9f2 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/NFTReceiveUM.kt @@ -20,7 +20,7 @@ internal data class NFTReceiveUM( data class Content( val availableItems: ImmutableList, - val notAvailableItems: ImmutableList, + val unavailableItems: ImmutableList, ) : Networks() } } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt index c113d54905..d9f6d6a4b0 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/entity/transformer/UpdateDataStateTransformer.kt @@ -17,13 +17,13 @@ internal class UpdateDataStateTransformer( override fun transform(prevState: NFTReceiveUM): NFTReceiveUM = prevState.copy( networks = when { - networks.availableNetworks.isEmpty() && networks.notAvailableNetworks.isEmpty() + networks.availableNetworks.isEmpty() && networks.unavailableNetworks.isEmpty() -> NFTReceiveUM.Networks.Empty else -> NFTReceiveUM.Networks.Content( availableItems = networks.availableNetworks .map { it.transform(true) } .toPersistentList(), - notAvailableItems = networks.notAvailableNetworks + unavailableItems = networks.unavailableNetworks .map { it.transform(false) } .toPersistentList(), ) 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 090d51ff5c..a093929cf3 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 @@ -58,7 +58,7 @@ internal class NFTReceiveModel @Inject constructor( search = getInitialSearchBar(), networks = NFTReceiveUM.Networks.Content( availableItems = persistentListOf(), - notAvailableItems = persistentListOf(), + unavailableItems = persistentListOf(), ), bottomSheetConfig = null, ), diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt index 8b01fd73b0..26d245c7f2 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/receive/ui/NFTReceiveNetworksContent.kt @@ -54,13 +54,13 @@ internal fun NFTReceiveNetworksContent(state: NFTReceiveUM.Networks.Content, mod } networks(state.availableItems) } - if (state.notAvailableItems.isNotEmpty()) { + if (state.unavailableItems.isNotEmpty()) { item( key = "not_available_title", ) { Title(resourceReference(R.string.nft_receive_unavailable_section_title)) } - networks(state.notAvailableItems) + networks(state.unavailableItems) } } } @@ -207,7 +207,7 @@ private fun Preview_NFTReceiveNetworksUM() { onItemClick = { }, ), ), - notAvailableItems = persistentListOf(), + unavailableItems = persistentListOf(), ), ) } From 9b7bbe25647231c6ab38ceb46967e12ed33fb78b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 11:49:25 +0500 Subject: [PATCH 101/151] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../di/domain/NotificationsDomainModule.kt | 21 +++ .../DefaultNotificationsRepository.kt | 2 +- .../notifications/di/NotificationsModule.kt | 2 +- domain/notifications/build.gradle.kts | 10 +- .../notifications/GetApplicationIdUseCase.kt | 26 +++ .../NotificationsRepository.kt | 2 +- .../GetApplicationIdUseCaseTest.kt | 154 ++++++++++++++++++ 8 files changed, 212 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt rename domain/notifications/src/main/java/com/tangem/domain/notifications/{ => repository}/NotificationsRepository.kt (94%) create mode 100644 domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8120e8c86e..ac8b4dfa53 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -110,6 +110,7 @@ dependencies { implementation(projects.domain.promo.models) implementation(projects.domain.networks) implementation(projects.domain.quotes) + implementation(projects.domain.notifications) implementation(projects.common) implementation(projects.common.routing) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt new file mode 100644 index 0000000000..caad0bce3a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -0,0 +1,21 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.notifications.GetApplicationIdUseCase +import com.tangem.domain.notifications.repository.NotificationsRepository +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 NotificationsDomainModule { + + @Provides + @Singleton + fun providesGetApplicationIdUseCase(notificationsRepository: NotificationsRepository): GetApplicationIdUseCase = + GetApplicationIdUseCase( + notificationsRepository = notificationsRepository, + ) +} \ No newline at end of file diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index 3f8c27c889..8648ace195 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -10,7 +10,7 @@ import com.tangem.utils.info.AppInfoProvider import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.* -import com.tangem.domain.notifications.NotificationsRepository +import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.notifications.models.NotificationsEligibleNetwork import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt index 9358da820e..555ac66802 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/di/NotificationsModule.kt @@ -1,7 +1,7 @@ package com.tangem.data.notifications.di import com.tangem.data.notifications.DefaultNotificationsRepository -import com.tangem.domain.notifications.NotificationsRepository +import com.tangem.domain.notifications.repository.NotificationsRepository import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/domain/notifications/build.gradle.kts b/domain/notifications/build.gradle.kts index 485c40d9ab..ae59c08bd3 100644 --- a/domain/notifications/build.gradle.kts +++ b/domain/notifications/build.gradle.kts @@ -5,8 +5,12 @@ plugins { } dependencies { - implementation(deps.arrow.core) - implementation(deps.kotlin.coroutines) - + implementation(projects.domain.core) implementation(projects.domain.notifications.models) + + /* Tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt new file mode 100644 index 0000000000..026d3a89c6 --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/GetApplicationIdUseCase.kt @@ -0,0 +1,26 @@ +package com.tangem.domain.notifications + +import arrow.core.Either +import com.tangem.domain.notifications.repository.NotificationsRepository +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +class GetApplicationIdUseCase( + private val notificationsRepository: NotificationsRepository, +) { + private val mutex = Mutex() + + suspend operator fun invoke(): Either = Either.catch { + val localApplicationId = notificationsRepository.getApplicationId() + if (localApplicationId != null) return@catch localApplicationId + + mutex.withLock { + val doubleCheckedId = notificationsRepository.getApplicationId() + if (doubleCheckedId != null) return@withLock doubleCheckedId + + val newApplicationId = notificationsRepository.createApplicationId() + notificationsRepository.saveApplicationId(newApplicationId) + newApplicationId + } + } +} \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt similarity index 94% rename from domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt rename to domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index bb42e3efab..5fecc4b7e4 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -1,4 +1,4 @@ -package com.tangem.domain.notifications +package com.tangem.domain.notifications.repository import com.tangem.domain.notifications.models.NotificationsEligibleNetwork diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt new file mode 100644 index 0000000000..8f562e98b7 --- /dev/null +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt @@ -0,0 +1,154 @@ +package com.tangem.domain.notifications + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.notifications.repository.NotificationsRepository +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.coVerifyOrder +import io.mockk.mockk +import kotlinx.coroutines.* +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.net.SocketTimeoutException + +class GetApplicationIdUseCaseTest { + + private val notificationsRepository: NotificationsRepository = mockk() + private val useCase = GetApplicationIdUseCase(notificationsRepository) + + @Test + fun `GIVEN local application ID exists WHEN invoke THEN return local application ID`() = runTest { + // GIVEN + val expectedApplicationId = "test-app-id" + coEvery { notificationsRepository.getApplicationId() } returns expectedApplicationId + + // WHEN + val result = useCase() + + // THEN + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isEqualTo(expectedApplicationId) + coVerify(exactly = 1) { notificationsRepository.getApplicationId() } + coVerify(inverse = true) { + notificationsRepository.createApplicationId() + notificationsRepository.saveApplicationId(any()) + } + } + + @Test + fun `GIVEN local application ID does not exist WHEN invoke THEN create and save new application ID`() = runTest { + // GIVEN + val newApplicationId = "new-app-id" + coEvery { notificationsRepository.getApplicationId() } returns null + coEvery { notificationsRepository.createApplicationId() } returns newApplicationId + coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + + // WHEN + val result = useCase() + + // THEN + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isEqualTo(newApplicationId) + coVerifyOrder { + notificationsRepository.getApplicationId() + notificationsRepository.getApplicationId() + notificationsRepository.createApplicationId() + notificationsRepository.saveApplicationId(newApplicationId) + } + } + + @Test + fun `GIVEN repository throws exception WHEN invoke THEN return Either Left with error`() = runTest { + // GIVEN + val expectedError = SocketTimeoutException("Test error") + coEvery { notificationsRepository.getApplicationId() } throws expectedError + + // WHEN + val result = useCase() + + // THEN + assertThat(result).isInstanceOf(Either.Left::class.java) + assertThat((result as Either.Left).value).isEqualTo(expectedError) + coVerify(exactly = 1) { notificationsRepository.getApplicationId() } + coVerify(inverse = true) { + notificationsRepository.createApplicationId() + notificationsRepository.saveApplicationId(any()) + } + } + + @Test + fun `GIVEN no local application ID WHEN multiple concurrent invokes THEN create only one application ID`() = + runTest { + // GIVEN + val newApplicationId = "new-app-id" + var isIdCreated = false + + coEvery { notificationsRepository.getApplicationId() } answers { + if (!isIdCreated) null else newApplicationId + } + coEvery { notificationsRepository.createApplicationId() } answers { + isIdCreated = true + newApplicationId + } + coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + + // WHEN + val results = coroutineScope { + List(PARALLEL_COUNT) { + async { + delay(100) + useCase() + } + }.awaitAll() + } + + // THEN + results.forEach { result -> + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isEqualTo(newApplicationId) + } + coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() } + coVerify(exactly = 1) { notificationsRepository.createApplicationId() } + coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) } + } + + @Test + fun `GIVEN no local application ID WHEN multiple concurrent invokes with delay THEN create only one application ID`() = runTest { + // GIVEN + val newApplicationId = "new-app-id" + var isIdCreated = false + + coEvery { notificationsRepository.getApplicationId() } answers { + if (!isIdCreated) null else newApplicationId + } + coEvery { notificationsRepository.createApplicationId() } answers { + isIdCreated = true + newApplicationId + } + coEvery { notificationsRepository.saveApplicationId(newApplicationId) } returns Unit + + // WHEN + val results = coroutineScope { + List(PARALLEL_COUNT) { + async { + delay(100) + useCase() + } + }.awaitAll() + } + + // THEN + results.forEach { result -> + assertThat(result).isInstanceOf(Either.Right::class.java) + assertThat((result as Either.Right).value).isEqualTo(newApplicationId) + } + coVerify(exactly = PARALLEL_COUNT + 1) { notificationsRepository.getApplicationId() } + coVerify(exactly = 1) { notificationsRepository.createApplicationId() } + coVerify(exactly = 1) { notificationsRepository.saveApplicationId(newApplicationId) } + } + + companion object { + private const val PARALLEL_COUNT = 100 + } +} \ No newline at end of file From a1970699ccfdc2cc6a1d9f8b9f8abebc087d9adb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 22 Apr 2025 12:37:48 +0500 Subject: [PATCH 102/151] Updated on 2026-08-14 --- .../models/response/OnrampStatusResponse.kt | 6 ++++ .../domain/onramp/model/OnrampStatus.kt | 8 +++++ .../onramp/main/ui/OnrampAmountContent.kt | 9 ++---- .../SetOnrampSuccessContentConverter.kt | 10 ++++++ ...nDetailsOnrampTransactionStateConverter.kt | 32 +++++++++++++------ .../factory/express/OnrampStatusFactory.kt | 2 ++ .../wallet/domain/OnrampStatusFactory.kt | 2 ++ .../SingleWalletOnrampTransactionConverter.kt | 27 ++++++++++------ 8 files changed, 70 insertions(+), 26 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt index c701137912..6f5ec14d6a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/onramp/models/response/OnrampStatusResponse.kt @@ -91,4 +91,10 @@ enum class Status { @Json(name = "paused") Paused, + + @Json(name = "refund-in-progress") + RefundInProgress, + + @Json(name = "refunded") + Refunded, } \ No newline at end of file diff --git a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt index e116c0c085..1b7952e042 100644 --- a/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt +++ b/domain/onramp/models/src/main/kotlin/com/tangem/domain/onramp/model/OnrampStatus.kt @@ -31,6 +31,8 @@ data class OnrampStatus( Paid(order = 8), Sending(order = 9), Finished(order = 10), + RefundInProgress(order = 11), + Refunded(order = 12), ; val isTerminal: Boolean @@ -39,6 +41,7 @@ data class OnrampStatus( Failed, Paused, Finished, + Refunded, -> true Created, WaitingForPayment, @@ -46,6 +49,7 @@ data class OnrampStatus( Verifying, Paid, Sending, + RefundInProgress, -> false } @@ -62,6 +66,8 @@ data class OnrampStatus( Verifying, Paid, Sending, + RefundInProgress, + Refunded, -> false } @@ -78,6 +84,8 @@ data class OnrampStatus( Paid, Sending, Finished, + RefundInProgress, + Refunded, -> false } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index eec3a23193..608587c822 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -42,16 +42,13 @@ internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier horizontalAlignment = Alignment.CenterHorizontally, ) { OnrampCurrencyIcon(currencyUM = state.currencyUM) - OnrampAmountField( - amountField = state.amountFieldModel, - isLoading = state.secondaryFieldModel is OnrampAmountSecondaryFieldUM.Loading, - ) + OnrampAmountField(amountField = state.amountFieldModel) OnrampAmountSecondary(state = state.secondaryFieldModel) } } @Composable -private fun OnrampAmountField(amountField: AmountFieldModel, isLoading: Boolean) { +private fun OnrampAmountField(amountField: AmountFieldModel) { val decimalFormat = rememberDecimalFormat() val requester = remember { FocusRequester() } AmountTextField( @@ -70,7 +67,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel, isLoading: Boolean) color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ), - isEnabled = !amountField.isError && !isLoading, + isEnabled = !amountField.isError, isAutoResize = true, isValuePasted = amountField.isValuePasted, onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt index 834e4fb850..424213fc4b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/success/entity/conterter/SetOnrampSuccessContentConverter.kt @@ -148,6 +148,10 @@ internal class SetOnrampSuccessContentConverter( this == OnrampStatus.Status.Paid -> { resourceReference(R.string.express_status_buying_active, wrappedList(cryptoCurrency.name)) } + this == OnrampStatus.Status.RefundInProgress || + this == OnrampStatus.Status.Refunded -> { + resourceReference(R.string.express_exchange_status_failed) + } else -> { resourceReference(R.string.express_status_bought, wrappedList(cryptoCurrency.name)) } @@ -166,6 +170,12 @@ internal class SetOnrampSuccessContentConverter( wrappedList(cryptoCurrency.name), ) } + this == OnrampStatus.Status.RefundInProgress -> { + resourceReference(R.string.express_exchange_status_refunding) + } + this == OnrampStatus.Status.Refunded -> { + resourceReference(R.string.express_exchange_status_refunded) + } else -> { resourceReference(R.string.express_exchange_status_sent, wrappedList(cryptoCurrency.name)) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt index c585e6e3cd..93ae9d4370 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsOnrampTransactionStateConverter.kt @@ -1,9 +1,6 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory -import com.tangem.common.ui.expressStatus.state.ExpressLinkUM -import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState -import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM -import com.tangem.common.ui.expressStatus.state.ExpressStatusUM +import com.tangem.common.ui.expressStatus.state.* import com.tangem.common.ui.notifications.ExpressNotificationsUM import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -23,9 +20,6 @@ import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.Provider @@ -129,8 +123,12 @@ internal class TokenDetailsOnrampTransactionStateConverter( private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM { return when (status) { - OnrampStatus.Status.Verifying -> ExpressTransactionStateIconUM.Warning - OnrampStatus.Status.Failed -> ExpressTransactionStateIconUM.Error + OnrampStatus.Status.RefundInProgress, + OnrampStatus.Status.Verifying, + -> ExpressTransactionStateIconUM.Warning + OnrampStatus.Status.Refunded, + OnrampStatus.Status.Failed, + -> ExpressTransactionStateIconUM.Error else -> ExpressTransactionStateIconUM.None } } @@ -213,6 +211,10 @@ internal class TokenDetailsOnrampTransactionStateConverter( wrappedList(cryptoCurrency.name), ) } + this == OnrampStatus.Status.RefundInProgress || + this == OnrampStatus.Status.Refunded -> { + resourceReference(R.string.express_exchange_status_failed) + } else -> { resourceReference(R.string.express_status_bought, wrappedList(cryptoCurrency.name)) } @@ -234,6 +236,12 @@ internal class TokenDetailsOnrampTransactionStateConverter( wrappedList(cryptoCurrency.name), ) } + this == OnrampStatus.Status.RefundInProgress -> { + resourceReference(R.string.express_exchange_status_refunding) + } + this == OnrampStatus.Status.Refunded -> { + resourceReference(R.string.express_exchange_status_refunded) + } else -> { resourceReference( R.string.express_exchange_status_sent, @@ -241,7 +249,11 @@ internal class TokenDetailsOnrampTransactionStateConverter( ) } }, - state = getStatusState(OnrampStatus.Status.Sending), + state = when { + this == OnrampStatus.Status.RefundInProgress -> ExpressStatusItemState.Active + this == OnrampStatus.Status.Refunded -> ExpressStatusItemState.Error + else -> getStatusState(OnrampStatus.Status.Sending) + }, ) private fun getStatusLink(status: OnrampStatus.Status, externalTxUrl: String?): ExpressLinkUM { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt index b790b61b5d..cea16cfd27 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/OnrampStatusFactory.kt @@ -134,10 +134,12 @@ internal class OnrampStatusFactory @AssistedInject constructor( PaymentProcessing, Paid, Sending, + RefundInProgress, -> ExpressAnalyticsStatus.InProgress Verifying -> ExpressAnalyticsStatus.KYC Failed -> ExpressAnalyticsStatus.Fail Finished -> ExpressAnalyticsStatus.Done + Refunded -> ExpressAnalyticsStatus.Refunded null -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt index e60f42ad67..9874d658bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/OnrampStatusFactory.kt @@ -94,10 +94,12 @@ internal class OnrampStatusFactory @Inject constructor( PaymentProcessing, Paid, Sending, + RefundInProgress, -> ExpressAnalyticsStatus.InProgress Verifying -> ExpressAnalyticsStatus.KYC Failed -> ExpressAnalyticsStatus.Fail Finished -> ExpressAnalyticsStatus.Done + Refunded -> ExpressAnalyticsStatus.Refunded null -> null } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt index c506948924..ce57d2ac3a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletOnrampTransactionConverter.kt @@ -1,9 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter -import com.tangem.common.ui.expressStatus.state.ExpressLinkUM -import com.tangem.common.ui.expressStatus.state.ExpressStatusItemState -import com.tangem.common.ui.expressStatus.state.ExpressStatusItemUM -import com.tangem.common.ui.expressStatus.state.ExpressStatusUM +import com.tangem.common.ui.expressStatus.state.* import com.tangem.common.ui.notifications.ExpressNotificationsUM import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -22,11 +19,8 @@ import com.tangem.domain.onramp.model.OnrampStatus import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.analytics.TokenOnrampAnalyticsEvent -import com.tangem.feature.wallet.impl.R -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateIconUM -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateInfoUM -import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.impl.R import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.persistentListOf @@ -142,8 +136,11 @@ internal class SingleWalletOnrampTransactionConverter( private fun getIconState(status: OnrampStatus.Status): ExpressTransactionStateIconUM { return when (status) { - OnrampStatus.Status.Verifying -> ExpressTransactionStateIconUM.Warning - OnrampStatus.Status.Failed -> ExpressTransactionStateIconUM.Error + OnrampStatus.Status.Verifying, + -> ExpressTransactionStateIconUM.Warning + OnrampStatus.Status.Refunded, + OnrampStatus.Status.Failed, + -> ExpressTransactionStateIconUM.Error else -> ExpressTransactionStateIconUM.None } } @@ -226,6 +223,10 @@ internal class SingleWalletOnrampTransactionConverter( wrappedList(currency.name), ) } + this == OnrampStatus.Status.RefundInProgress || + this == OnrampStatus.Status.Refunded -> { + resourceReference(R.string.express_exchange_status_failed) + } else -> { resourceReference(R.string.express_status_bought, wrappedList(currency.name)) } @@ -247,6 +248,12 @@ internal class SingleWalletOnrampTransactionConverter( wrappedList(currency.name), ) } + this == OnrampStatus.Status.RefundInProgress -> { + resourceReference(R.string.express_exchange_status_refunding) + } + this == OnrampStatus.Status.Refunded -> { + resourceReference(R.string.express_exchange_status_refunded) + } else -> { resourceReference( R.string.express_exchange_status_sent, From 3e2f1b978f0c9217bb0fceda8bb6a2a7ec06c552 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 08:36:22 +0000 Subject: [PATCH 103/151] 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 76affe35f0..a0bc01da42 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-1043" +tangemBlockchainSdk = "develop-1042" #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 632b0dd663e7f0b68cc6e5ab0b27e2416c04f43e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 13:02:08 +0400 Subject: [PATCH 104/151] Updated on 2026-08-14 --- .../tangem/data/blockaid/BlockAidMapper.kt | 2 +- .../blockaid/DefaultBlockAidRepository.kt | 6 +-- .../blockaid/di/BlockAidDataInternalModule.kt | 6 +-- .../data/blockaid/BlockAidMapperTest.kt | 2 +- .../blockaid/DefaultBlockAidRepositoryTest.kt | 4 +- .../BlockAidEthereumVerificationDelegate.kt | 50 +++++++++++++++++++ .../DefaultWcEthMessageSignUseCase.kt | 3 ++ .../DefaultWcEthSendTransactionUseCase.kt | 3 ++ .../DefaultWcEthSignTransactionUseCase.kt | 3 ++ .../network/ethereum/WcEthNetwork.kt | 3 +- .../WcBlockAidEligibleTransactionUseCase.kt | 10 ++++ .../ethereum/WcEthMessageSignUseCase.kt | 4 +- .../ethereum/WcEthTransactionUseCase.kt | 3 ++ 13 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/BlockAidEthereumVerificationDelegate.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/blockaid/WcBlockAidEligibleTransactionUseCase.kt diff --git a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt index a36dea8518..3b31a9b9a7 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/BlockAidMapper.kt @@ -15,7 +15,7 @@ private const val SUCCESS_STATUS = "Success" private const val DOMAIN_CHECKED_STATUS = "hit" private const val VALIDATION_SAFE_STATUS = "Benign" -internal class BlockAidMapper { +internal object BlockAidMapper { fun mapToDomain(from: DomainScanResponse): CheckDAppResult { return when { 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 index 8659672ffe..66483ed2df 100644 --- a/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt +++ b/data/blockaid/src/main/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepository.kt @@ -12,19 +12,19 @@ import kotlinx.coroutines.withContext internal class DefaultBlockAidRepository( private val api: BlockAidApi, - private val dispatcherProvider: CoroutineDispatcherProvider, + private val dispatchers: CoroutineDispatcherProvider, private val mapper: BlockAidMapper, ) : BlockAidRepository { override suspend fun verifyDAppDomain(data: DAppData): CheckDAppResult { - val response = withContext(dispatcherProvider.io) { + val response = withContext(dispatchers.io) { api.scanDomain(DomainScanRequest(data.url)) } return mapper.mapToDomain(response) } override suspend fun verifyTransaction(data: TransactionData): CheckTransactionResult { - val response = withContext(dispatcherProvider.io) { + val response = withContext(dispatchers.io) { when (data.params) { is TransactionParams.Evm -> { api.scanJsonRpc(mapper.mapToEvmRequest(data)) 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 index 4f4ff71605..1ca90326c6 100644 --- 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 @@ -17,11 +17,11 @@ internal object BlockAidDataInternalModule { @Provides @Singleton - fun provideRepository(api: BlockAidApi, dispatcherProvider: CoroutineDispatcherProvider): BlockAidRepository { + fun provideRepository(api: BlockAidApi, dispatchers: CoroutineDispatcherProvider): BlockAidRepository { return DefaultBlockAidRepository( api = api, - dispatcherProvider = dispatcherProvider, - mapper = BlockAidMapper(), + dispatchers = dispatchers, + mapper = BlockAidMapper, ) } } \ 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 index faaba4926c..175cc033b9 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/BlockAidMapperTest.kt @@ -11,7 +11,7 @@ import java.math.BigDecimal class BlockAidMapperTest { - private val mapper = BlockAidMapper() + private val mapper = BlockAidMapper @Test fun `when status hit and is malicious false then map to domain returns safe`() { 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 index 036ed43815..d125246bce 100644 --- a/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt +++ b/data/blockaid/src/test/kotlin/com/tangem/data/blockaid/DefaultBlockAidRepositoryTest.kt @@ -29,12 +29,12 @@ class DefaultBlockAidRepositoryTest { private lateinit var repository: DefaultBlockAidRepository - private val testDispatcherProvider = TestingCoroutineDispatcherProvider() + private val dispatchers = TestingCoroutineDispatcherProvider() @Before fun setup() { MockKAnnotations.init(this) - repository = DefaultBlockAidRepository(api, testDispatcherProvider, mapper) + repository = DefaultBlockAidRepository(api, dispatchers, mapper) } @Test diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/BlockAidEthereumVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/BlockAidEthereumVerificationDelegate.kt new file mode 100644 index 0000000000..ba45ffbdcd --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/BlockAidEthereumVerificationDelegate.kt @@ -0,0 +1,50 @@ +package com.tangem.data.walletconnect.network.ethereum + +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.domain.blockaid.models.transaction.TransactionData +import com.domain.blockaid.models.transaction.TransactionParams +import com.tangem.domain.blockaid.BlockAidVerifier +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import timber.log.Timber +import javax.inject.Inject + +internal class BlockAidEthereumVerificationDelegate @Inject constructor( + private val blockAidVerifier: BlockAidVerifier, +) { + + fun getSecurityStatus( + network: Network, + method: WcEthMethod, + rawSdkRequest: WcSdkSessionRequest, + session: WcSession, + ): Flow> = flow { + emit(Lce.Loading(partialContent = null)) + blockAidVerifier.verifyTransaction( + TransactionData( + chain = network.name, + accountAddress = when (method) { + is WcEthMethod.MessageSign -> method.account + is WcEthMethod.SendTransaction -> method.transaction.from + is WcEthMethod.SignTransaction -> method.transaction.from + }, + method = rawSdkRequest.request.method, + domainUrl = session.sdkModel.appMetaData.url, + params = TransactionParams.Evm(rawSdkRequest.request.params), + ), + ).fold( + ifLeft = { + Timber.e("Failed to verify transaction: ${it.localizedMessage}") + emit(Lce.Error(it)) + }, + ifRight = { + emit(Lce.Content(it)) + }, + ) + } +} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt index bbbd62d234..ed0c48b91b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt @@ -30,9 +30,12 @@ internal class DefaultWcEthMessageSignUseCase @AssistedInject constructor( @Assisted private val method: WcEthMethod.MessageSign, private val walletManagersFacade: WalletManagersFacade, private val signUseCase: SignUseCase, + blockAidDelegate: BlockAidEthereumVerificationDelegate, ) : BaseWcSignUseCase(), WcEthMessageSignUseCase { + override val securityStatus = blockAidDelegate.getSecurityStatus(network, method, rawSdkRequest, session) + override suspend fun SignCollector.onSign( state: WcSignState, ) { 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 index c923e955fd..00f485605d 100644 --- 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 @@ -25,9 +25,12 @@ internal class DefaultWcEthSendTransactionUseCase @AssistedInject constructor( @Assisted private val method: WcEthMethod.SendTransaction, override val respondService: WcRespondService, private val sendTransaction: SendTransactionUseCase, + blockAidDelegate: BlockAidEthereumVerificationDelegate, ) : BaseWcSignUseCase(), WcEthSendTransactionUseCase { + override val securityStatus = blockAidDelegate.getSecurityStatus(network, method, rawSdkRequest, session) + private val converter = EthTransactionParamsConverter(context) override suspend fun SignCollector.onSign(state: WcSignState) { 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 index 70b1c7b3a3..a4ad2df2a1 100644 --- 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 @@ -27,9 +27,12 @@ internal class DefaultWcEthSignTransactionUseCase @AssistedInject constructor( private val prepareForSend: PrepareForSendUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted private val method: WcEthMethod.SignTransaction, + blockAidDelegate: BlockAidEthereumVerificationDelegate, ) : BaseWcSignUseCase(), WcEthSignTransactionUseCase { + override val securityStatus = blockAidDelegate.getSecurityStatus(network, method, rawSdkRequest, session) + private val converter = EthTransactionParamsConverter(context) override suspend fun SignCollector.onSign(state: WcSignState) { 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 171bb54185..4f05896657 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 @@ -18,7 +18,7 @@ import com.tangem.domain.walletconnect.usecase.WcMethodUseCase import com.tangem.domain.wallets.models.UserWallet import jakarta.inject.Inject -internal class WcEthNetwork constructor( +internal class WcEthNetwork( private val moshi: Moshi, private val excludedBlockchains: ExcludedBlockchains, private val sessionsManager: WcSessionsManager, @@ -34,7 +34,6 @@ internal class WcEthNetwork constructor( 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.MessageSign -> factories.messageSign.create(context, method) is WcEthMethod.SendTransaction -> factories.sendTransaction.create(context, method) diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/blockaid/WcBlockAidEligibleTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/blockaid/WcBlockAidEligibleTransactionUseCase.kt new file mode 100644 index 0000000000..8cf1497baf --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/blockaid/WcBlockAidEligibleTransactionUseCase.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.walletconnect.usecase.blockaid + +import com.domain.blockaid.models.transaction.CheckTransactionResult +import com.tangem.domain.core.lce.Lce +import kotlinx.coroutines.flow.Flow + +interface WcBlockAidEligibleTransactionUseCase { + + val securityStatus: Flow> +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMessageSignUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMessageSignUseCase.kt index 0895241c36..c2ab17f065 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMessageSignUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/ethereum/WcEthMessageSignUseCase.kt @@ -1,10 +1,12 @@ package com.tangem.domain.walletconnect.usecase.ethereum +import com.tangem.domain.walletconnect.usecase.blockaid.WcBlockAidEligibleTransactionUseCase import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase interface WcEthMessageSignUseCase : WcSignUseCase, - WcSignUseCase.SimpleRun { + WcSignUseCase.SimpleRun, + WcBlockAidEligibleTransactionUseCase { 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 index 48f74153de..406abcdaba 100644 --- 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 @@ -6,11 +6,13 @@ 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.WcMutableFee +import com.tangem.domain.walletconnect.usecase.blockaid.WcBlockAidEligibleTransactionUseCase import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase interface WcEthSendTransactionUseCase : WcSignUseCase, WcSignUseCase.SimpleRun, + WcBlockAidEligibleTransactionUseCase, WcMutableFee { override val session: WcSession @@ -25,6 +27,7 @@ interface WcEthSendTransactionUseCase : interface WcEthSignTransactionUseCase : WcSignUseCase, WcSignUseCase.SimpleRun, + WcBlockAidEligibleTransactionUseCase, WcMutableFee { override val session: WcSession From 4a840b127fdbd1a68fccf4cc0205166d79942047 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 12:34:12 +0300 Subject: [PATCH 105/151] Updated on 2026-08-14 --- .../common/ui/notifications/NotificationUM.kt | 5 ++++ .../state/converters/BalanceItemConverter.kt | 11 +------ .../AddStakingNotificationsTransformer.kt | 29 ++++++++++++++++++- .../StakingInfoNotificationsFactory.kt | 23 +++++++++++++-- 4 files changed, 54 insertions(+), 14 deletions(-) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt index d546442104..0a79dbba45 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/NotificationUM.kt @@ -73,6 +73,11 @@ sealed class NotificationUM(val config: NotificationConfig) { ), ) + data object TonStakingExtraFeeError : Error( + title = resourceReference(R.string.staking_notification_ton_extra_reserve_title), + subtitle = resourceReference(R.string.staking_notification_ton_extra_reserve_is_required), + ) + data class TokenExceedsBalance( val networkIconId: Int, val currencyName: String, 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 da6dc0fcff..edd38e289e 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 @@ -17,7 +17,6 @@ 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 @@ -66,7 +65,7 @@ internal class BalanceItemConverter( ), rawCurrencyId = value.rawCurrencyId, pendingActions = value.pendingActions.toPersistentList(), - isClickable = value.isClickable(), + isClickable = value.type.isClickable() && !value.isPending, isPending = value.isPending, ) } @@ -84,14 +83,6 @@ internal class BalanceItemConverter( } } - 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, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index f301a048e9..4774ef7850 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.common.ui.notifications.NotificationsFactory.addRentExemptionN import com.tangem.common.ui.notifications.NotificationsFactory.addReserveAmountErrorNotification import com.tangem.common.ui.notifications.NotificationsFactory.addTransactionLimitErrorNotification import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.staking.model.stakekit.StakingError @@ -22,6 +23,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.warnings.CryptoCurrencyCheck import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.presentation.state.FeeState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -29,6 +31,7 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount import com.tangem.features.staking.impl.presentation.state.utils.checkFeeCoverage import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.lib.crypto.BlockchainUtils.isTon import com.tangem.utils.Provider import com.tangem.utils.extensions.orZero import com.tangem.utils.transformer.Transformer @@ -175,6 +178,9 @@ internal class AddStakingNotificationsTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, onClick = prevState.clickIntents::openTokenDetails, ) + addTonUnstakeNotification( + actionType = prevState.actionType, + ) addExceedsBalanceNotification( cryptoCurrencyWarning = currencyWarning, cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -246,7 +252,7 @@ internal class AddStakingNotificationsTransformer( cryptoCurrencyStatus: CryptoCurrencyStatus, onClick: (CryptoCurrency) -> Unit, ) { - val balance = cryptoCurrencyStatus.value.amount ?: BigDecimal.ZERO + val balance = cryptoCurrencyStatus.value.amount.orZero() if (!isSubtractionAvailable) return val showNotification = sendingAmount + feeAmount > balance @@ -269,4 +275,25 @@ internal class AddStakingNotificationsTransformer( add(notification) } } + + private fun MutableList.addTonUnstakeNotification(actionType: StakingActionCommonType) { + val amount = cryptoCurrencyStatusProvider().value.amount.orZero() + val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.id.value + + if (isTon(cryptoCurrencyNetworkIdValue) && actionType !is StakingActionCommonType.Enter) { + val notification = if (amount < TON_BALANCE_EXTRA_FEE_THRESHOLD) { + NotificationUM.Error.TonStakingExtraFeeError + } else { + StakingNotification.Info.Ordinary( + title = resourceReference(R.string.staking_notification_ton_extra_reserve_title), + text = resourceReference(R.string.staking_notification_ton_extra_reserve_info), + ) + } + add(notification) + } + } + + private companion object { + val TON_BALANCE_EXTRA_FEE_THRESHOLD = BigDecimal(0.2) + } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index dbf10dc2b0..2fe83cd25c 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -11,11 +11,13 @@ import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType 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.InnerYieldBalanceState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.lib.crypto.BlockchainUtils.isCardano import com.tangem.lib.crypto.BlockchainUtils.isCosmos +import com.tangem.lib.crypto.BlockchainUtils.isTon import com.tangem.lib.crypto.BlockchainUtils.isTron import com.tangem.utils.Provider import com.tangem.utils.extensions.isZero @@ -46,7 +48,7 @@ internal class StakingInfoNotificationsFactory( when (prevState.actionType) { is StakingActionCommonType.Enter -> addEnterInfoNotifications(sendingAmount, feeValue) - is StakingActionCommonType.Exit -> addExitInfoNotifications() + is StakingActionCommonType.Exit -> addExitInfoNotifications(prevState) is StakingActionCommonType.Pending -> { addCardanoRestakeMinimumAmountNotification(feeValue) addPendingInfoNotifications(prevState) @@ -54,13 +56,15 @@ internal class StakingInfoNotificationsFactory( } } - private fun MutableList.addExitInfoNotifications() { + private fun MutableList.addExitInfoNotifications(prevState: StakingUiState) { val cooldownPeriodDays = yield.metadata.cooldownPeriod?.days + + val cryptoCurrencyNetworkIdValue = cryptoCurrencyStatusProvider().currency.network.id.value if (cooldownPeriodDays != null) { add( StakingNotification.Info.Unstake( cooldownPeriodDays = cooldownPeriodDays, - subtitleRes = if (isCosmos(cryptoCurrencyStatusProvider().currency.network.id.value)) { + subtitleRes = if (isCosmos(cryptoCurrencyNetworkIdValue)) { R.string.staking_notification_unstake_cosmos_text } else { R.string.staking_notification_unstake_text @@ -68,6 +72,19 @@ internal class StakingInfoNotificationsFactory( ), ) } + + val initialInfoState = prevState.initialInfoState as? StakingStates.InitialInfoState.Data + val stakingBalances = (initialInfoState?.yieldBalance as? InnerYieldBalanceState.Data)?.balances + val activeStakesCount = stakingBalances.orEmpty().filter { it.type == BalanceType.STAKED }.size + + if (isTon(cryptoCurrencyNetworkIdValue) && activeStakesCount > 1) { + add( + StakingNotification.Info.Ordinary( + title = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_title), + text = resourceReference(R.string.staking_notification_ton_have_to_unstake_all_text), + ), + ) + } } private fun MutableList.addEnterInfoNotifications( From ba65a2f344dec2b99326db7eb13b92ca93fd5fa6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 13:43:21 +0400 Subject: [PATCH 106/151] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ac8b4dfa53..7d7888942f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -155,6 +155,7 @@ dependencies { implementation(projects.data.onramp) implementation(projects.data.networks) implementation(projects.data.quotes) + implementation(projects.data.blockaid) /** Features */ implementation(projects.features.onboarding) diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt index 5698f8421b..a2dd9abd9e 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt @@ -8,5 +8,5 @@ import com.tangem.domain.wallets.models.UserWalletId data class WcSessionDTO( val topic: String, val walletId: UserWalletId, - val securityStatus: CheckDAppResult, + val securityStatus: CheckDAppResult = CheckDAppResult.FAILED_TO_VERIFY, ) \ No newline at end of file From 93ff2d98c9bc1ec26ba2b835b77ddf030cb107c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Apr 2025 16:36:39 +0300 Subject: [PATCH 107/151] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 3 +++ .../tap/di/domain/MarketsDomainModule.kt | 3 +++ .../tap/di/domain/StakingDomainModule.kt | 8 ++++++- .../tap/di/domain/TokensDomainModule.kt | 14 +++++++++++ .../configs/feature_toggles_config.json | 2 +- .../managetokens/SaveManagedTokensUseCase.kt | 22 +++++++++++++---- .../domain/markets/SaveMarketTokensUseCase.kt | 22 +++++++++++++---- .../FetchStakingYieldBalanceUseCase.kt | 24 +++++++++++++++---- .../tokens/AddCryptoCurrenciesUseCase.kt | 23 ++++++++++++++---- .../tokens/FetchCardTokenListUseCase.kt | 20 ++++++++++++---- .../tokens/FetchCurrencyStatusUseCase.kt | 21 ++++++++++++---- .../domain/tokens/FetchTokenListUseCase.kt | 20 ++++++++++++---- .../CachedCurrenciesStatusesOperations.kt | 16 ++++++++++++- ...PrimaryCurrencyStatusUpdatesUseCaseTest.kt | 1 + .../state/helpers/StakingBalanceUpdater.kt | 3 +++ 15 files changed, 167 insertions(+), 35 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 07f1c7e97f..b55ef9a8f7 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 @@ -6,6 +6,7 @@ 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.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -75,6 +76,7 @@ internal object ManageTokensDomainModule { quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, + multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): SaveManagedTokensUseCase { return SaveManagedTokensUseCase( @@ -87,6 +89,7 @@ internal object ManageTokensDomainModule { quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteFetcher = multiQuoteFetcher, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index b6efd9bd4f..7666729a36 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 @@ -7,6 +7,7 @@ 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.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -74,6 +75,7 @@ object MarketsDomainModule { quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, + multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): SaveMarketTokensUseCase { return SaveMarketTokensUseCase( @@ -85,6 +87,7 @@ object MarketsDomainModule { quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteFetcher = multiQuoteFetcher, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index cb6976b9b8..22f61dc1f1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -1,7 +1,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* -import com.tangem.domain.staking.repositories.* +import com.tangem.domain.staking.repositories.StakingActionRepository +import com.tangem.domain.staking.repositories.StakingErrorResolver +import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.repositories.StakingTransactionHashRepository +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.walletmanager.WalletManagersFacade import dagger.Module import dagger.Provides @@ -92,10 +96,12 @@ internal object StakingDomainModule { fun provideFetchStakingYieldBalanceUseCase( stakingRepository: StakingRepository, stakingErrorResolver: StakingErrorResolver, + singleYieldBalanceFetcher: SingleYieldBalanceFetcher, ): FetchStakingYieldBalanceUseCase { return FetchStakingYieldBalanceUseCase( stakingRepository = stakingRepository, stakingErrorResolver = stakingErrorResolver, + singleYieldBalanceFetcher = singleYieldBalanceFetcher, ) } 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 075ab6d9f7..ff23a2c9e2 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 @@ -10,7 +10,9 @@ 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.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.staking.single.SingleYieldBalanceSupplier import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrenciesStatusesOperations @@ -40,6 +42,7 @@ internal object TokensDomainModule { quotesRepository: QuotesRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, + singleYieldBalanceFetcher: SingleYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): AddCryptoCurrenciesUseCase { return AddCryptoCurrenciesUseCase( @@ -49,6 +52,7 @@ internal object TokensDomainModule { quotesRepository = quotesRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteFetcher = multiQuoteFetcher, + singleYieldBalanceFetcher = singleYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -62,6 +66,7 @@ internal object TokensDomainModule { stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, + multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchTokenListUseCase { return FetchTokenListUseCase( @@ -71,6 +76,7 @@ internal object TokensDomainModule { stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteFetcher = multiQuoteFetcher, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -177,6 +183,7 @@ internal object TokensDomainModule { stakingRepository: StakingRepository, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, + singleYieldBalanceFetcher: SingleYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchCurrencyStatusUseCase { return FetchCurrencyStatusUseCase( @@ -186,6 +193,7 @@ internal object TokensDomainModule { stakingRepository = stakingRepository, singleNetworkStatusFetcher = singleNetworkStatusFetcher, multiQuoteFetcher = multiQuoteFetcher, + singleYieldBalanceFetcher = singleYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -199,6 +207,7 @@ internal object TokensDomainModule { stakingRepository: StakingRepository, multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteFetcher: MultiQuoteFetcher, + multiYieldBalanceFetcher: MultiYieldBalanceFetcher, tokensFeatureToggles: TokensFeatureToggles, ): FetchCardTokenListUseCase { return FetchCardTokenListUseCase( @@ -208,6 +217,7 @@ internal object TokensDomainModule { stakingRepository = stakingRepository, multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteFetcher = multiQuoteFetcher, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -411,6 +421,7 @@ internal object TokensDomainModule { multiQuoteFetcher: MultiQuoteFetcher, singleQuoteSupplier: SingleQuoteSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + multiYieldBalanceFetcher: MultiYieldBalanceFetcher, ): BaseCurrenciesStatusesOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -424,6 +435,7 @@ internal object TokensDomainModule { multiQuoteFetcher = multiQuoteFetcher, singleQuoteSupplier = singleQuoteSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } @@ -443,6 +455,7 @@ internal object TokensDomainModule { multiQuoteFetcher: MultiQuoteFetcher, singleQuoteSupplier: SingleQuoteSupplier, singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + multiYieldBalanceFetcher: MultiYieldBalanceFetcher, ): BaseCurrencyStatusOperations { return CachedCurrenciesStatusesOperations( currenciesRepository = currenciesRepository, @@ -456,6 +469,7 @@ internal object TokensDomainModule { multiQuoteFetcher = multiQuoteFetcher, singleQuoteSupplier = singleQuoteSupplier, singleYieldBalanceSupplier = singleYieldBalanceSupplier, + multiYieldBalanceFetcher = multiYieldBalanceFetcher, tokensFeatureToggles = tokensFeatureToggles, ) } 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 109fa60297..aa583d661b 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 @@ -73,6 +73,6 @@ }, { "name": "STAKING_LOADING_REFACTORING_ENABLED", - "version": "undefined" + "version": "5.25.0" } ] 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 8379c39929..ba16d9ceb5 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 @@ -7,6 +7,8 @@ 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.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency @@ -28,6 +30,7 @@ class SaveManagedTokensUseCase( private val quotesRepository: QuotesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -118,11 +121,20 @@ class SaveManagedTokensUseCase( userWalletId: UserWalletId, existingCurrencies: List, ) { - stakingRepository.fetchMultiYieldBalance( - userWalletId = userWalletId, - cryptoCurrencies = existingCurrencies, - refresh = true, - ) + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + multiYieldBalanceFetcher( + params = YieldBalanceFetcherParams.Multi( + userWalletId = userWalletId, + currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) + } else { + stakingRepository.fetchMultiYieldBalance( + userWalletId = userWalletId, + cryptoCurrencies = existingCurrencies, + refresh = true, + ) + } } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { 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 06b6a81591..1d0215f044 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 @@ -5,6 +5,8 @@ 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.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.model.CryptoCurrency @@ -33,6 +35,7 @@ class SaveMarketTokensUseCase( private val quotesRepository: QuotesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -100,11 +103,20 @@ class SaveMarketTokensUseCase( userWalletId: UserWalletId, existingCurrencies: List, ) { - stakingRepository.fetchMultiYieldBalance( - userWalletId = userWalletId, - cryptoCurrencies = existingCurrencies, - refresh = true, - ) + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + multiYieldBalanceFetcher( + params = YieldBalanceFetcherParams.Multi( + userWalletId = userWalletId, + currencyIdWithNetworkMap = existingCurrencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) + } else { + stakingRepository.fetchMultiYieldBalance( + userWalletId = userWalletId, + cryptoCurrencies = existingCurrencies, + refresh = true, + ) + } } private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index e09a9309c7..88800654a2 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -3,30 +3,44 @@ package com.tangem.domain.staking import arrow.core.Either import arrow.core.raise.catch import arrow.core.raise.either +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId class FetchStakingYieldBalanceUseCase( private val stakingRepository: StakingRepository, private val stakingErrorResolver: StakingErrorResolver, + private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, ) { suspend operator fun invoke( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, + isRefactoringEnabled: Boolean, refresh: Boolean = false, ): Either { return either { catch( block = { - stakingRepository.fetchSingleYieldBalance( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - refresh = refresh, - ) + if (isRefactoringEnabled) { + singleYieldBalanceFetcher( + params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ), + ) + } else { + stakingRepository.fetchSingleYieldBalance( + userWalletId = userWalletId, + cryptoCurrency = cryptoCurrency, + refresh = refresh, + ) + } }, catch = { stakingErrorResolver.resolve(it) }, ) 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 e2354e1813..ca0fecd294 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 @@ -6,7 +6,9 @@ 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.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -32,6 +34,7 @@ class AddCryptoCurrenciesUseCase( private val quotesRepository: QuotesRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteFetcher: MultiQuoteFetcher, + private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -159,11 +162,21 @@ class AddCryptoCurrenciesUseCase( } private suspend fun refreshUpdatedYieldBalances(userWalletId: UserWalletId, addedCurrency: CryptoCurrency) { - stakingRepository.fetchSingleYieldBalance( - userWalletId = userWalletId, - cryptoCurrency = addedCurrency, - refresh = true, - ) + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + singleYieldBalanceFetcher( + params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = addedCurrency.id, + network = addedCurrency.network, + ), + ) + } else { + stakingRepository.fetchSingleYieldBalance( + userWalletId = userWalletId, + cryptoCurrency = addedCurrency, + refresh = true, + ) + } } private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { 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 20e19dfaf0..7d56e24a4b 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 @@ -6,6 +6,8 @@ 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.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -26,6 +28,7 @@ class FetchCardTokenListUseCase( private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -110,9 +113,18 @@ class FetchCardTokenListUseCase( currencies: List, refresh: Boolean, ) { - catch( - block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, - catch = { /* Ignore error */ }, - ) + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + multiYieldBalanceFetcher( + params = YieldBalanceFetcherParams.Multi( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) + } else { + catch( + block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, + catch = { /* Ignore error */ }, + ) + } } } \ No newline at end of file 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 a38ae046d5..e9775c5d32 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 @@ -6,7 +6,9 @@ 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.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.repositories.StakingRepository +import com.tangem.domain.staking.single.SingleYieldBalanceFetcher import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network @@ -36,6 +38,7 @@ class FetchCurrencyStatusUseCase( private val stakingRepository: StakingRepository, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, private val multiQuoteFetcher: MultiQuoteFetcher, + private val singleYieldBalanceFetcher: SingleYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -158,10 +161,20 @@ class FetchCurrencyStatusUseCase( cryptoCurrency: CryptoCurrency, refresh: Boolean, ) { - catch( - block = { stakingRepository.fetchSingleYieldBalance(userWalletId, cryptoCurrency, refresh) }, - ) { - raise(CurrencyStatusError.DataError(it)) + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + singleYieldBalanceFetcher( + params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = cryptoCurrency.id, + network = cryptoCurrency.network, + ), + ) + } else { + catch( + block = { stakingRepository.fetchSingleYieldBalance(userWalletId, cryptoCurrency, refresh) }, + ) { + raise(CurrencyStatusError.DataError(it)) + } } } } \ No newline at end of file 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 4602529c72..e0300eb83a 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 @@ -8,6 +8,8 @@ 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.fetcher.YieldBalanceFetcherParams +import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency @@ -38,6 +40,7 @@ class FetchTokenListUseCase( private val stakingRepository: StakingRepository, private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteFetcher: MultiQuoteFetcher, + private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) { @@ -152,10 +155,19 @@ class FetchTokenListUseCase( currencies: List, refresh: Boolean, ) { - catch( - block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, - catch = { /* Ignore error */ }, - ) + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + multiYieldBalanceFetcher( + params = YieldBalanceFetcherParams.Multi( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) + } else { + catch( + block = { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies, refresh) }, + catch = { /* Ignore error */ }, + ) + } } /** 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 c222b79fec..8814392d0a 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 @@ -18,8 +18,10 @@ 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 +import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList +import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.single.SingleYieldBalanceProducer import com.tangem.domain.staking.single.SingleYieldBalanceSupplier @@ -49,6 +51,7 @@ class CachedCurrenciesStatusesOperations( private val multiQuoteFetcher: MultiQuoteFetcher, private val singleQuoteSupplier: SingleQuoteSupplier, private val singleYieldBalanceSupplier: SingleYieldBalanceSupplier, + private val multiYieldBalanceFetcher: MultiYieldBalanceFetcher, private val tokensFeatureToggles: TokensFeatureToggles, ) : BaseCurrenciesStatusesOperations, BaseCurrencyStatusOperations( @@ -200,7 +203,18 @@ class CachedCurrenciesStatusesOperations( quotesRepository.fetchQuotes(rawCurrenciesIds) } }, - async { stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) }, + async { + if (tokensFeatureToggles.isStakingLoadingRefactoringEnabled) { + multiYieldBalanceFetcher( + params = YieldBalanceFetcherParams.Multi( + userWalletId = userWalletId, + currencyIdWithNetworkMap = currencies.associateTo(hashMapOf()) { it.id to it.network }, + ), + ) + } else { + stakingRepository.fetchMultiYieldBalance(userWalletId, currencies) + } + }, ) } .map { } 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 e084456c8c..be180baa2f 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 @@ -190,6 +190,7 @@ internal class GetPrimaryCurrencyStatusUpdatesUseCaseTest { singleQuoteSupplier = mockk(), quotesRepositoryV2 = mockk(), singleYieldBalanceSupplier = mockk(), + multiYieldBalanceFetcher = mockk(), ), dispatchers = dispatchers, ) diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt index d86e6cc5f3..02db8ad802 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakingBalanceUpdater.kt @@ -5,6 +5,7 @@ import com.tangem.domain.staking.FetchStakingYieldBalanceUseCase import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus import com.tangem.domain.tokens.FetchPendingTransactionsUseCase +import com.tangem.domain.tokens.TokensFeatureToggles import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase @@ -28,6 +29,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor( private val fetchActionsUseCase: FetchActionsUseCase, private val txHistoryFeatureToggles: TxHistoryFeatureToggles, private val txHistoryContentUpdateEmitter: TxHistoryContentUpdateEmitter, + private val tokensFeatureToggles: TokensFeatureToggles, @DelayedWork private val coroutineScope: CoroutineScope, @Assisted private val userWallet: UserWallet, @Assisted private val cryptoCurrencyStatus: CryptoCurrencyStatus, @@ -89,6 +91,7 @@ internal class StakingBalanceUpdater @AssistedInject constructor( stakingYieldBalanceUseCase( userWalletId = userWallet.walletId, cryptoCurrency = cryptoCurrencyStatus.currency, + isRefactoringEnabled = tokensFeatureToggles.isStakingLoadingRefactoringEnabled, refresh = true, ) } From bbe35fc044ade1dfa36ed81bac26372dc6cf3493 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 17:18:02 +0300 Subject: [PATCH 108/151] Updated on 2026-08-14 --- .../data/visa/DefaultVisaActivationRepository.kt | 13 ++++++++++++- .../com/tangem/data/visa/config/VisaConfig.kt | 8 +++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index bf72bc979e..3f2c1d8a90 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -2,6 +2,9 @@ package com.tangem.data.visa import com.tangem.data.visa.config.VisaLibLoader import com.tangem.data.visa.converter.VisaActivationStatusConverterWithState +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.visa.TangemVisaApi @@ -30,6 +33,7 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( private val visaAuthTokenStorage: VisaAuthTokenStorage, private val visaAuthRepository: VisaAuthRepository, private val visaLibLoader: VisaLibLoader, + private val apiConfigsManager: ApiConfigsManager, ) : VisaActivationRepository { override suspend fun getActivationRemoteState(): VisaActivationRemoteState = withContext(dispatcherProvider.io) { @@ -158,7 +162,14 @@ internal class DefaultVisaActivationRepository @AssistedInject constructor( } override suspend fun getPinCodeRsaEncryptionPublicKey(): String { - return visaLibLoader.getOrCreateConfig().rsaPublicKey + val env = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.TangemVisa).environment + val rsaPublicKey = visaLibLoader.getOrCreateConfig().rsaPublicKey + return when (env) { + ApiEnvironment.DEV, + ApiEnvironment.STAGE, + -> rsaPublicKey.dev + ApiEnvironment.PROD -> rsaPublicKey.prod + } } private suspend fun request(requestBlock: suspend () -> T): T { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/config/VisaConfig.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/config/VisaConfig.kt index 2dd3b5566c..cf76110918 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/config/VisaConfig.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/config/VisaConfig.kt @@ -12,7 +12,7 @@ internal data class VisaConfig( @Json(name = "txHistoryAPIAdditionalHeaders") val header: Header, @Json(name = "rsaPublicKey") - val rsaPublicKey: String, + val rsaPublicKey: RsaPublicKey, ) { @JsonClass(generateAdapter = true) @@ -28,4 +28,10 @@ internal data class VisaConfig( @Json(name = "x-asn") val xAsn: String, ) + + @JsonClass(generateAdapter = true) + data class RsaPublicKey( + @Json(name = "prod") val prod: String, + @Json(name = "dev") val dev: String, + ) } \ No newline at end of file From 0dd5b23b643a9d127cd03230964486521e9576a0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 17:40:50 +0300 Subject: [PATCH 109/151] Updated on 2026-08-14 --- .../datasource/api/common/config/ApiConfig.kt | 2 -- .../api/common/config/TangemVisa.kt | 9 +++++- .../api/common/config/TangemVisaAuth.kt | 26 --------------- .../datasource/api/visa/TangemVisaApi.kt | 32 +++++++++++++++++++ .../datasource/api/visa/TangemVisaAuthApi.kt | 29 ----------------- .../tangem/datasource/di/ApiConfigsModule.kt | 5 --- .../com/tangem/datasource/di/NetworkModule.kt | 24 -------------- .../managers/ProdApiConfigsManagerTest.kt | 18 +---------- .../data/visa/DefaultVisaAuthRepository.kt | 4 +-- .../data/visa/utils/VisaApiRequestMaker.kt | 4 +-- 10 files changed, 45 insertions(+), 108 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt index 78db8380c3..b6bc222e76 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/ApiConfig.kt @@ -24,7 +24,6 @@ sealed class ApiConfig { Express, TangemTech, StakeKit, - TangemVisaAuth, TangemVisa, TangemCardSdk, BlockAid, @@ -35,7 +34,6 @@ sealed class ApiConfig { is Express -> ID.Express is TangemTech -> ID.TangemTech is StakeKit -> ID.StakeKit - 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/TangemVisa.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt index 5310f5b308..b46a599b2d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisa.kt @@ -7,14 +7,21 @@ internal class TangemVisa( private val appVersionProvider: AppVersionProvider, ) : ApiConfig() { - override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD + override val defaultEnvironment: ApiEnvironment = ApiEnvironment.DEV override val environmentConfigs = listOf( createProdEnvironment(), + createDevEnvironment(), ) private fun createProdEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( environment = ApiEnvironment.PROD, + baseUrl = "https://api.paera.com/bff/", + headers = createHeaders(), + ) + + private fun createDevEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( + environment = ApiEnvironment.DEV, baseUrl = "[REDACTED_ENV_URL]", headers = createHeaders(), ) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt deleted file mode 100644 index 833cde2111..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemVisaAuth.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.datasource.api.common.config - -import com.tangem.utils.ProviderSuspend -import com.tangem.utils.version.AppVersionProvider - -internal class TangemVisaAuth( - private val appVersionProvider: AppVersionProvider, -) : ApiConfig() { - - override val defaultEnvironment: ApiEnvironment = ApiEnvironment.STAGE - - override val environmentConfigs = listOf( - createStageEnvironment(), - ) - - private fun createStageEnvironment(): ApiEnvironmentConfig = ApiEnvironmentConfig( - environment = ApiEnvironment.STAGE, - baseUrl = "[REDACTED_ENV_URL]", - headers = createHeaders(), - ) - - private fun createHeaders() = mapOf( - "version" to ProviderSuspend { appVersionProvider.versionName }, - "platform" to ProviderSuspend { "Android" }, - ) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt index 16e643329b..346e7a74fe 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaApi.kt @@ -4,8 +4,14 @@ import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.visa.models.request.ActivationByCardWalletRequest import com.tangem.datasource.api.visa.models.request.ActivationByCustomerWalletRequest import com.tangem.datasource.api.visa.models.request.ActivationStatusRequest +import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardIdRequest +import com.tangem.datasource.api.visa.models.request.GenerateNoneByCardWalletRequest +import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardIdRequest +import com.tangem.datasource.api.visa.models.request.GetAccessTokenByCardWalletRequest import com.tangem.datasource.api.visa.models.request.GetCardWalletAcceptanceRequest import com.tangem.datasource.api.visa.models.request.GetCustomerWalletAcceptanceRequest +import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardIdRequest +import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest import com.tangem.datasource.api.visa.models.request.SetPinCodeRequest import com.tangem.datasource.api.visa.models.response.* import retrofit2.http.Body @@ -16,6 +22,30 @@ import retrofit2.http.Query interface TangemVisaApi { + // region: auth + + @POST("v1/auth/challenge") + suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse + + @POST("v1/auth/challenge") + suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse + + @POST("v1/auth/token") + suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse + + @POST("v1/auth/token") + suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse + + @POST("v1/auth/token/refresh") + suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse + + @POST("v1/auth/token/refresh") + suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse + + // endregion + + // region: activation + @POST("v1/activation/status") suspend fun getRemoteActivationStatus( @Header("Authorization") authHeader: String, @@ -52,6 +82,8 @@ interface TangemVisaApi { @Body body: SetPinCodeRequest, ): ApiResponse + // endregion + @GET("customer/info") suspend fun getCustomerInfo( @Header("Authorization") authHeader: String, diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt deleted file mode 100644 index 57b91ca1c3..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/visa/TangemVisaAuthApi.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.datasource.api.visa - -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.visa.models.request.* -import com.tangem.datasource.api.visa.models.response.GenerateNonceResponse -import com.tangem.datasource.api.visa.models.response.JWTResponse -import retrofit2.http.Body -import retrofit2.http.POST - -interface TangemVisaAuthApi { - - @POST("v1/auth/challenge") - suspend fun generateNonceByCardId(@Body request: GenerateNoneByCardIdRequest): GenerateNonceResponse - - @POST("v1/auth/challenge") - suspend fun generateNonceByCardWallet(@Body request: GenerateNoneByCardWalletRequest): GenerateNonceResponse - - @POST("v1/auth/token") - suspend fun getAccessTokenByCardId(@Body request: GetAccessTokenByCardIdRequest): JWTResponse - - @POST("v1/auth/token") - suspend fun getAccessTokenByCardWallet(@Body request: GetAccessTokenByCardWalletRequest): JWTResponse - - @POST("v1/auth/token/refresh") - suspend fun refreshCardIdAccessToken(@Body request: RefreshTokenByCardIdRequest): ApiResponse - - @POST("v1/auth/token/refresh") - suspend fun refreshCardWalletAccessToken(@Body request: RefreshTokenByCardWalletRequest): ApiResponse -} \ 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 1a90974c75..62107738e3 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 @@ -37,11 +37,6 @@ internal object ApiConfigsModule { fun provideTangemTechConfig(appVersionProvider: AppVersionProvider, authProvider: AuthProvider): ApiConfig = TangemTech(appVersionProvider, authProvider) - @Provides - @IntoSet - fun provideTangemAuthVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = - TangemVisaAuth(appVersionProvider) - @Provides @IntoSet fun provideTangemVisaConfig(appVersionProvider: AppVersionProvider): ApiConfig = TangemVisa(appVersionProvider) 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 e05ca4cf1a..a4b0eb363a 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 @@ -18,7 +18,6 @@ import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.TangemTechApiV2 import com.tangem.datasource.api.visa.TangemVisaApi -import com.tangem.datasource.api.visa.TangemVisaAuthApi import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.utils.* @@ -192,29 +191,6 @@ internal object NetworkModule { ) } - @Provides - @Singleton - fun provideTangemVisaAuthApi( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - analyticsErrorHandler: AnalyticsErrorHandler, - apiConfigsManager: ApiConfigsManager, - appLogsStore: AppLogsStore, - ): TangemVisaAuthApi { - return createApi( - id = ApiConfig.ID.TangemVisaAuth, - moshi = moshi, - context = context, - apiConfigsManager = apiConfigsManager, - analyticsErrorHandler = analyticsErrorHandler, - clientBuilder = { - addInterceptor( - NetworkLogsSaveInterceptor(appLogsStore), - ).applyTimeoutAnnotations() - }, - ) - } - @Provides @Singleton fun provideTangemVisaApi( 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 2624ca1707..116ddea237 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 @@ -35,7 +35,6 @@ private val API_CONFIGS = setOf( Express(configManager, expressAuthProvider, appVersionProvider), TangemTech(appVersionProvider, appAuthProvider), StakeKit(stakeKitAuthProvider), - TangemVisaAuth(appVersionProvider), TangemVisa(appVersionProvider), ) @@ -86,7 +85,6 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { is Express -> createExpressModel() is TangemTech -> createTangemTechModel() is StakeKit -> createStakeKitModel() - is TangemVisaAuth -> createVisaAuthModel() is TangemVisa -> createVisaModel() is TangemCardSdk -> createTangemCardSdkModel() is BlockAid -> createBlockAidSdkModel() @@ -177,25 +175,11 @@ internal class ProdApiConfigsManagerTest(private val model: Model) { ) } - private fun createVisaAuthModel(): Model { - return Model( - id = ApiConfig.ID.TangemVisaAuth, - expected = ApiEnvironmentConfig( - environment = ApiEnvironment.STAGE, - baseUrl = "[REDACTED_ENV_URL]", - headers = mapOf( - "version" to ProviderSuspend { VERSION_NAME }, - "platform" to ProviderSuspend { "Android" }, - ), - ), - ) - } - private fun createVisaModel(): Model { return Model( id = ApiConfig.ID.TangemVisa, expected = ApiEnvironmentConfig( - environment = ApiEnvironment.PROD, + environment = ApiEnvironment.DEV, baseUrl = "[REDACTED_ENV_URL]", headers = mapOf( "version" to ProviderSuspend { VERSION_NAME }, diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt index 9e3be2327c..daa8bd755d 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaAuthRepository.kt @@ -1,7 +1,7 @@ package com.tangem.data.visa import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.visa.TangemVisaAuthApi +import com.tangem.datasource.api.visa.TangemVisaApi import com.tangem.datasource.api.visa.models.request.* import com.tangem.domain.visa.model.VisaAuthChallenge import com.tangem.domain.visa.model.VisaAuthSession @@ -14,7 +14,7 @@ import javax.inject.Inject @Suppress("UnusedPrivateMember") internal class DefaultVisaAuthRepository @Inject constructor( - private val visaAuthApi: TangemVisaAuthApi, + private val visaAuthApi: TangemVisaApi, private val dispatchers: CoroutineDispatcherProvider, ) : VisaAuthRepository { diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt index 2c7cae2eb4..73b1f217b9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/utils/VisaApiRequestMaker.kt @@ -5,7 +5,7 @@ import com.tangem.data.visa.model.AccessCodeData import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow -import com.tangem.datasource.api.visa.TangemVisaAuthApi +import com.tangem.datasource.api.visa.TangemVisaApi import com.tangem.datasource.api.visa.models.request.RefreshTokenByCardWalletRequest import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.common.util.cardTypesResolver @@ -24,7 +24,7 @@ typealias VisaAuthorizationHeader = String internal class VisaApiRequestMaker @Inject constructor( private val userWalletsStore: UserWalletsStore, - private val visaAuthApi: TangemVisaAuthApi, + private val visaAuthApi: TangemVisaApi, private val accessCodeDataConverter: AccessCodeDataConverter, private val dispatcherProvider: CoroutineDispatcherProvider, ) { From 0b6bcd7998fb4fcab705307338e961afb59244df Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 18:01:01 +0000 Subject: [PATCH 110/151] 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 8c55070de7..a0bc01da42 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-1048" +tangemBlockchainSdk = "develop-1042" #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 e1f5d41f27d5d9da9ee36773ffa23cc96ca0d2a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 12:15:06 +0500 Subject: [PATCH 111/151] Updated on 2026-08-14 --- .../modal/TangemModalBottomSheet.kt | 3 - .../components/ConnectionsComponent.kt | 33 +++ .../components/WcAppInfoComponent.kt | 30 +-- .../components/WcAppInfoContainerComponent.kt | 107 ++++++++ .../connections/entity/WcAppInfoUM.kt | 35 ++- .../entity/WcPrimaryButtonConfig.kt | 3 + .../connections/model/WcAppInfoModel.kt | 40 +++ .../connections/model/WcConnectionsModel.kt | 9 +- .../connections/routes/WcAppInfoRoutes.kt | 15 ++ .../routes/WcConnectionsBottomSheetRoutes.kt | 9 + .../connections/ui/InternalComponents.kt | 4 +- .../connections/ui/WcAppInfoContent.kt | 229 +++++++++++++++--- .../di/WalletConnectModelModule.kt | 6 + 13 files changed, 447 insertions(+), 76 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcPrimaryButtonConfig.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetRoutes.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt index f97a4f5daf..ff085bffb5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/modal/TangemModalBottomSheet.kt @@ -145,8 +145,6 @@ inline fun BasicModalBottomSheet( ) { val model = config.content as? T ?: return - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - val bsContent: @Composable ColumnScope.() -> Unit = { val maxHeight = LocalConfiguration.current.screenHeightDp * MODAL_SHEET_MAX_HEIGHT @@ -157,7 +155,6 @@ inline fun BasicModalBottomSheet( .clip(TangemTheme.shapes.roundedCornersLarge) .background(containerColor) .heightIn(max = maxHeight.dp) - .padding(bottom = bottomBarHeight) .fillMaxWidth(), ) { Box(modifier = Modifier.fillMaxWidth()) { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt index 09c4b03ff5..79b63e953b 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt @@ -4,10 +4,18 @@ 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.walletconnect.usecase.pair.WcPairUseCase import com.tangem.features.walletconnect.connections.model.WcConnectionsModel +import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetRoutes import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent internal class ConnectionsComponent( @@ -15,10 +23,35 @@ internal class ConnectionsComponent( ) : AppComponentContext by appComponentContext, ComposableContentComponent { private val model: WcConnectionsModel = getOrCreateModel() + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = null, + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() WcConnectionsContent(modifier = modifier, state = state) + + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + config: WcConnectionsBottomSheetRoutes, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent { + return when (config) { + is WcConnectionsBottomSheetRoutes.AppInfo -> WcAppInfoContainerComponent( + appComponentContext = childByContext(componentContext), + params = WcAppInfoContainerComponent.Params( + wcUrl = config.wcUrl, + source = WcPairUseCase.Source.QR, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) + } } } \ No newline at end of file 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 index bba2e61463..87a06ece5b 100644 --- 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 @@ -1,31 +1,27 @@ package com.tangem.features.walletconnect.connections.components +import androidx.compose.foundation.layout.padding 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.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.walletconnect.connections.model.WcAppInfoModel 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() - } + private val model: WcAppInfoModel, +) : AppComponentContext by appComponentContext, ComposableContentComponent { @Composable - override fun BottomSheet() { + override fun Content(modifier: Modifier) { + val state by model.appInfoUiState.collectAsStateWithLifecycle() WcAppInfoModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = ::dismiss, - content = TangemBottomSheetConfigContent.Empty, // todo(wc) will be fixed in next PR's - ), + modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16), + state = state, ) } - - 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/components/WcAppInfoContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt new file mode 100644 index 0000000000..83f3853ca6 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt @@ -0,0 +1,107 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.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.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.features.walletconnect.connections.model.WcAppInfoModel +import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes +import com.tangem.features.walletconnect.impl.R + +internal class WcAppInfoContainerComponent( + private val appComponentContext: AppComponentContext, + params: Params, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + private val model: WcAppInfoModel = getOrCreateModel(params = params) + private val contentNavigation = StackNavigation() + private val contentStack = childStack( + source = contentNavigation, + serializer = WcAppInfoRoutes.serializer(), + initialConfiguration = WcAppInfoRoutes.AppInfo, + childFactory = ::screenChild, + ) + + override fun dismiss() { + model.dismiss() + } + + @Composable + override fun BottomSheet() { + val content by contentStack.subscribeAsState() + TangemModalBottomSheet( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = content.active.configuration, + ), + containerColor = TangemTheme.colors.background.tertiary, + title = { config -> Title(route = config) }, + content = { + Children(stack = content, animation = stackAnimation()) { child -> + child.instance.Content(modifier = Modifier) + } + }, + ) + } + + @Composable + private fun Title(route: WcAppInfoRoutes) { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_wallet_connect), + startIconRes = when (route) { + is WcAppInfoRoutes.AppInfo, is WcAppInfoRoutes.Alert -> null + is WcAppInfoRoutes.SelectNetworks, is WcAppInfoRoutes.SelectWallet -> R.drawable.ic_back_24 + }, + onStartClick = when (route) { + is WcAppInfoRoutes.AppInfo, is WcAppInfoRoutes.Alert -> null + is WcAppInfoRoutes.SelectNetworks, is WcAppInfoRoutes.SelectWallet -> ::contentBack + }, + endIconRes = when (route) { + is WcAppInfoRoutes.AppInfo, is WcAppInfoRoutes.Alert -> R.drawable.ic_close_24 + is WcAppInfoRoutes.SelectNetworks, is WcAppInfoRoutes.SelectWallet -> null + }, + onEndClick = when (route) { + is WcAppInfoRoutes.AppInfo -> ::dismiss + is WcAppInfoRoutes.Alert -> ::contentBack + is WcAppInfoRoutes.SelectNetworks, + is WcAppInfoRoutes.SelectWallet, + -> null + }, + ) + } + + private fun contentBack() { + contentNavigation.pop() + } + + private fun screenChild(config: WcAppInfoRoutes, componentContext: ComponentContext): ComposableContentComponent = + when (config) { + is WcAppInfoRoutes.AppInfo -> WcAppInfoComponent( + appComponentContext = childByContext(componentContext), + model = model, + ) + is WcAppInfoRoutes.Alert -> TODO() + WcAppInfoRoutes.SelectNetworks -> TODO() + WcAppInfoRoutes.SelectWallet -> TODO() + } + + data class Params(val wcUrl: String, val source: WcPairUseCase.Source, 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 index a588d1ddd0..401b13e42d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -6,20 +6,31 @@ 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 +internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { + + abstract val onDismiss: () -> Unit + abstract val connectButtonConfig: WcPrimaryButtonConfig + + data class Loading( + override val onDismiss: () -> Unit, + override val connectButtonConfig: WcPrimaryButtonConfig, + ) : WcAppInfoUM() + + data class Content( + val appName: String, + val appIcon: String, + val isVerified: Boolean, + val appSubtitle: String, + val notificationUM: NotificationUM?, + val walletName: String, + val networksInfo: WcNetworksInfo, + override val connectButtonConfig: WcPrimaryButtonConfig, + override val onDismiss: () -> Unit, + ) : WcAppInfoUM() +} @Immutable internal sealed class WcNetworksInfo { - data class MissingRequiredNetworkInfo(val network: String) : WcNetworksInfo() + data class MissingRequiredNetworkInfo(val networks: 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/entity/WcPrimaryButtonConfig.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcPrimaryButtonConfig.kt new file mode 100644 index 0000000000..8f549ebb8b --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcPrimaryButtonConfig.kt @@ -0,0 +1,3 @@ +package com.tangem.features.walletconnect.connections.entity + +data class WcPrimaryButtonConfig(val showProgress: Boolean, val enabled: Boolean, val onClick: () -> Unit) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt new file mode 100644 index 0000000000..cb7aa69d4a --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt @@ -0,0 +1,40 @@ +package com.tangem.features.walletconnect.connections.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.features.walletconnect.connections.components.WcAppInfoContainerComponent +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import javax.inject.Inject + +@Stable +@ModelScoped +internal class WcAppInfoModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params: WcAppInfoContainerComponent.Params = paramsContainer.require() + val appInfoUiState: StateFlow + field = MutableStateFlow(createLoadingState()) + + fun dismiss() { + params.onDismiss() + } + + private fun onConnect() { + // TODO: [REDACTED_JIRA] + } + + private fun createLoadingState(): WcAppInfoUM.Loading { + return WcAppInfoUM.Loading( + onDismiss = ::dismiss, + connectButtonConfig = WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = ::onConnect), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index a0b6c6aa91..69216f06c3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -1,6 +1,8 @@ package com.tangem.features.walletconnect.connections.model import arrow.core.getOrElse +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -18,6 +20,7 @@ import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsTransformer +import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetRoutes import com.tangem.features.walletconnect.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update @@ -37,6 +40,7 @@ internal class WcConnectionsModel @Inject constructor( val uiState: StateFlow field = MutableStateFlow(getInitialState()) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { listenQrUpdates() @@ -46,10 +50,7 @@ internal class WcConnectionsModel @Inject constructor( private fun listenQrUpdates() { listenToQrScanningUseCase(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .onEach { - // TODO: [REDACTED_JIRA] - Timber.d(it) - } + .onEach { wcUrl -> bottomSheetNavigation.activate(WcConnectionsBottomSheetRoutes.AppInfo(wcUrl)) } .launchIn(modelScope) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt new file mode 100644 index 0000000000..8c0d1fcc2c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -0,0 +1,15 @@ +package com.tangem.features.walletconnect.connections.routes + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.features.walletconnect.connections.components.AlertsComponent +import kotlinx.serialization.Serializable + +@Serializable +@Immutable +internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent { + data object AppInfo : WcAppInfoRoutes() + data object SelectWallet : WcAppInfoRoutes() + data object SelectNetworks : WcAppInfoRoutes() + data class Alert(val alertType: AlertsComponent.AlertType) : WcAppInfoRoutes() +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetRoutes.kt new file mode 100644 index 0000000000..66f2dc4f1e --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetRoutes.kt @@ -0,0 +1,9 @@ +package com.tangem.features.walletconnect.connections.routes + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed class WcConnectionsBottomSheetRoutes { + @Serializable + data class AppInfo(val wcUrl: String) : WcConnectionsBottomSheetRoutes() +} \ 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 index ca17e5f44f..2f583ee287 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt @@ -1,6 +1,5 @@ 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 @@ -31,8 +30,7 @@ internal fun WcAppInfoItem( AsyncImage( modifier = Modifier .size(TangemTheme.dimens.size48) - .clip(RoundedCornerShape(TangemTheme.dimens.radius8)) - .background(Color.Black), + .clip(RoundedCornerShape(TangemTheme.dimens.radius8)), model = iconUrl, contentDescription = title, ) 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 index 5ce783ebb7..540be9a66f 100644 --- 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 @@ -9,6 +9,7 @@ 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.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -17,6 +18,7 @@ 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.StrokeCap import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -27,11 +29,7 @@ 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.* import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.resourceReference @@ -41,29 +39,23 @@ 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.connections.entity.WcPrimaryButtonConfig 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) }, - ) +internal fun WcAppInfoModalBottomSheet(state: WcAppInfoUM, modifier: Modifier = Modifier) { + when (state) { + is WcAppInfoUM.Content -> WcAppInfoModalBottomSheetContent(state, modifier) + is WcAppInfoUM.Loading -> WcAppInfoModalBottomSheetLoading(state, modifier) + } } +// region Content state @Composable -private fun WcAppInfoModalBottomSheetContent(state: WcAppInfoUM) { - Column(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16)) { +private fun WcAppInfoModalBottomSheetContent(state: WcAppInfoUM.Content, modifier: Modifier = Modifier) { + Column(modifier = modifier) { val blocksModifier = Modifier .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) .background(color = TangemTheme.colors.background.action) @@ -85,13 +77,13 @@ private fun WcAppInfoModalBottomSheetContent(state: WcAppInfoUM) { WcAppInfoButtons( modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), onDismiss = state.onDismiss, - onConnect = state.onConnect, + connectButtonConfig = state.connectButtonConfig, ) } } @Composable -private fun WcAppInfoFirstBlock(state: WcAppInfoUM, modifier: Modifier = Modifier) { +private fun WcAppInfoFirstBlock(state: WcAppInfoUM.Content, modifier: Modifier = Modifier) { var connectionRequestExpanded by remember { mutableStateOf(false) } Column(modifier = modifier.animateContentSize()) { WcAppInfoItem( @@ -213,7 +205,7 @@ private fun ConnectionRequestDescriptionRow( } @Composable -private fun WcAppInfoSecondBlock(state: WcAppInfoUM, modifier: Modifier = Modifier) { +private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier = Modifier) { Column(modifier = modifier) { val itemsModifier = Modifier .fillMaxWidth() @@ -234,7 +226,7 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM, modifier: Modifier = Modifi title = resourceReference(R.string.wc_missing_required_network_title), subtitle = resourceReference( R.string.wc_missing_required_network_description, - wrappedList(state.networksInfo.network), + wrappedList(state.networksInfo.networks), ), ), iconTint = TangemTheme.colors.icon.attention, @@ -374,7 +366,11 @@ private fun NetworkIcons(icons: ImmutableList, modifier: Modifier = Modifie } @Composable -private fun WcAppInfoButtons(onDismiss: () -> Unit, onConnect: () -> Unit, modifier: Modifier = Modifier) { +private fun WcAppInfoButtons( + onDismiss: () -> Unit, + connectButtonConfig: WcPrimaryButtonConfig, + modifier: Modifier = Modifier, +) { Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { SecondaryButton( modifier = Modifier @@ -388,7 +384,166 @@ private fun WcAppInfoButtons(onDismiss: () -> Unit, onConnect: () -> Unit, modif .fillMaxWidth() .weight(1f), text = stringResourceSafe(R.string.wc_common_connect), - onClick = onConnect, + onClick = connectButtonConfig.onClick, + showProgress = connectButtonConfig.showProgress, + enabled = connectButtonConfig.enabled, + ) + } +} +// endregion + +// region Loading state +@Composable +private fun WcAppInfoModalBottomSheetLoading(state: WcAppInfoUM.Loading, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + val blocksModifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + WcAppInfoFirstLoadingBlock(blocksModifier) + WcAppInfoSecondLoadingBlock( + modifier = Modifier + .padding(top = 14.dp) + .then(blocksModifier), + ) + WcAppInfoButtons( + modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing16), + onDismiss = state.onDismiss, + connectButtonConfig = state.connectButtonConfig, + ) + } +} + +@Composable +private fun WcAppInfoFirstLoadingBlock(modifier: Modifier = Modifier) { + Column(modifier = modifier) { + WcLoadingAppInfoItem() + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + Row( + modifier = Modifier.padding(all = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = TangemTheme.colors.icon.accent, + strokeWidth = 2.dp, + strokeCap = StrokeCap.Square, + ) + Text( + text = "Connecting...", + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +private fun WcAppInfoSecondLoadingBlock(modifier: Modifier = Modifier) { + Column(modifier) { + Row(modifier = Modifier.padding(all = 12.dp), 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 = 8.dp) + .weight(1f), + text = stringResourceSafe(R.string.wc_common_wallet), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + TextShimmer(style = TangemTheme.typography.body1, radius = 8.dp, text = "Wallet 2.0") + } + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + Row(modifier = Modifier.padding(all = 12.dp), 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 = 8.dp) + .weight(1f), + text = stringResourceSafe(R.string.wc_common_networks), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + WcAppInfoLoadingNetworkIcons() + } + } +} + +@Suppress("MagicNumber") +@Composable +private fun WcAppInfoLoadingNetworkIcons(modifier: Modifier = Modifier) { + Box(modifier = modifier.wrapContentWidth()) { + for (index in 0..3) { + CircleShimmer( + 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), + ) + } + } +} + +@Composable +private fun WcLoadingAppInfoItem(modifier: Modifier = Modifier) { + Row( + modifier = modifier.padding(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + RectangleShimmer(modifier = Modifier.size(TangemTheme.dimens.size48), radius = 16.dp) + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), + ) { + TextShimmer( + modifier = Modifier + .width(106.dp) + .height(24.dp), + style = TangemTheme.typography.h3, + radius = 8.dp, + ) + TextShimmer( + modifier = Modifier + .width(168.dp) + .height(20.dp), + style = TangemTheme.typography.body2, + radius = 8.dp, + ) + } + } +} +// endregion + +@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() { + TangemThemePreview { + WcAppInfoModalBottomSheetLoading( + state = WcAppInfoUM.Loading( + onDismiss = {}, + WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {}), + ), + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = TangemTheme.dimens.spacing16), ) } } @@ -396,21 +551,21 @@ private fun WcAppInfoButtons(onDismiss: () -> Unit, onConnect: () -> Unit, modif @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) { +private fun WcAppInfoBottomSheetPreview(@PreviewParameter(WcAppInfoStateProvider::class) state: WcAppInfoUM.Content) { TangemThemePreview { WcAppInfoModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = state, - ), + state = state, + modifier = Modifier + .background(TangemTheme.colors.background.tertiary) + .padding(horizontal = TangemTheme.dimens.spacing16), ) } } private class WcAppInfoStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WcAppInfoUM( + WcAppInfoUM.Loading(onDismiss = {}, WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {})), + WcAppInfoUM.Content( appName = "React App", appIcon = "", isVerified = true, @@ -427,9 +582,9 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider Date: Fri, 25 Apr 2025 10:27:39 +0300 Subject: [PATCH 112/151] Updated on 2026-08-14 --- .../tangem/core/ui/message/dialog/Dialogs.kt | 6 ++++-- .../core/ui/utils/UiMessageSenderUtils.kt | 4 ++-- .../model/OnboardingVisaInProgressModel.kt | 17 +++++++++++++++-- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt index b8cac08019..da674fa0d6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/message/dialog/Dialogs.kt @@ -42,13 +42,15 @@ object Dialogs { /** * Universal error dialog */ - fun universalErrorDialog(universalError: UniversalError, onOkClick: () -> Unit): DialogMessage { + fun universalErrorDialog(universalError: UniversalError, onDismiss: () -> Unit): DialogMessage { return DialogMessage( message = resourceReference(R.string.universal_error, wrappedList(universalError.errorCode)), firstAction = EventMessageAction( title = resourceReference(R.string.common_ok), - onClick = onOkClick, + onClick = {}, ), + dismissOnFirstAction = true, + onDismissRequest = onDismiss, ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/UiMessageSenderUtils.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/UiMessageSenderUtils.kt index d237f52daa..3655a3ffd9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/UiMessageSenderUtils.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/UiMessageSenderUtils.kt @@ -4,6 +4,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.error.UniversalError import com.tangem.core.ui.message.dialog.Dialogs -fun UiMessageSender.showErrorDialog(universalError: UniversalError) { - send(Dialogs.universalErrorDialog(universalError) {}) +fun UiMessageSender.showErrorDialog(universalError: UniversalError, onDismiss: () -> Unit = {}) { + send(Dialogs.universalErrorDialog(universalError, onDismiss = onDismiss)) } \ 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 c0fdbbd57d..790b909fda 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 @@ -72,14 +72,20 @@ internal class OnboardingVisaInProgressModel @Inject constructor( is VisaActivationRemoteState.CardWalletSignatureRequired, VisaActivationRemoteState.BlockedForActivation, -> { - uiMessageSender.showErrorDialog(VisaActivationError.InconsistentRemoteState) + uiMessageSender.showErrorDialog(VisaActivationError.InconsistentRemoteState) { + retryShortPolling() + } + analyticsEventHandler.send( VisaAnalyticsEvent.ErrorOnboarding(VisaActivationError.InconsistentRemoteState), ) return@launch } VisaActivationRemoteState.Failed -> { - uiMessageSender.showErrorDialog(VisaActivationError.FailedRemoteState) + uiMessageSender.showErrorDialog(VisaActivationError.FailedRemoteState) { + retryShortPolling() + } + analyticsEventHandler.send( VisaAnalyticsEvent.ErrorOnboarding(VisaActivationError.FailedRemoteState), ) @@ -106,6 +112,13 @@ internal class OnboardingVisaInProgressModel @Inject constructor( } } + private fun retryShortPolling() { + modelScope.launch { + delay(timeMillis = 10000) + runShortPolling() + } + } + private suspend inline fun navigateToPinCodeIfNeeded( remoteState: VisaActivationRemoteState.AwaitingPinCode, complete: () -> Unit, From b2616969b48ffc1b2af659b5e11d4760565cd78c Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 15:22:35 +0700 Subject: [PATCH 113/151] Updated on 2026-08-14 --- .../DefaultLegacyWalletConnectRepository.kt | 87 +++++++++++++++++++ .../di/WalletConnectInteractorModule.kt | 19 +++- ... => DefaultWalletConnectFeatureToggles.kt} | 7 +- .../tangem/tap/routing/utils/ChildFactory.kt | 4 +- .../components/WalletConnectFeatureToggles.kt | 5 ++ 5 files changed, 113 insertions(+), 9 deletions(-) rename app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/{WalletConnectFeatureToggles.kt => DefaultWalletConnectFeatureToggles.kt} (55%) create mode 100644 features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectFeatureToggles.kt diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 5fe143f6c7..25d7dffa0a 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -10,6 +10,7 @@ import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.pair.unsupportedDApps import com.tangem.domain.walletconnect.model.legacy.Account +import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository @@ -21,8 +22,94 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction.O import kotlinx.coroutines.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.emptyFlow import timber.log.Timber +internal class DefaultLegacyWalletConnectRepositoryFacade constructor( + private val stub: LegacyWalletConnectRepositoryStub, + private val legacy: DefaultLegacyWalletConnectRepository, + private val walletConnectFeatureToggles: WalletConnectFeatureToggles, +) : LegacyWalletConnectRepository { + private val isNewWc by lazy { + walletConnectFeatureToggles.isRedesignedWalletConnectEnabled + } + override val events: Flow by lazy { + if (isNewWc) stub.events else legacy.events + } + override val activeSessions: Flow> by lazy { + if (isNewWc) stub.activeSessions else legacy.activeSessions + } + override val currentSessions: List by lazy { + if (isNewWc) stub.currentSessions else legacy.currentSessions + } + + override fun init(projectId: String) { + if (isNewWc) stub.init(projectId) else legacy.init(projectId) + } + + override fun setUserNamespaces(userNamespaces: Map>) { + if (isNewWc) stub.setUserNamespaces(userNamespaces) else legacy.setUserNamespaces(userNamespaces) + } + + override fun updateSessions() { + if (isNewWc) stub.updateSessions() else legacy.updateSessions() + } + + override fun pair(uri: String, source: SourceType) { + if (isNewWc) stub.pair(uri, source) else legacy.pair(uri, source) + } + + override fun disconnect(topic: String) { + if (isNewWc) stub.disconnect(topic) else legacy.disconnect(topic) + } + + override fun approve(userNamespaces: Map>, blockchainNames: List) { + if (isNewWc) stub.approve(userNamespaces, blockchainNames) else legacy.approve(userNamespaces, blockchainNames) + } + + override fun reject() { + if (isNewWc) stub.reject() else legacy.reject() + } + + override fun sendRequest(requestData: RequestData, result: String) { + if (isNewWc) stub.sendRequest(requestData, result) else legacy.sendRequest(requestData, result) + } + + override fun rejectRequest(requestData: RequestData, error: WalletConnectError) { + if (isNewWc) stub.rejectRequest(requestData, error) else legacy.rejectRequest(requestData, error) + } + + override fun cancelRequest(topic: String, id: Long, message: String) { + if (isNewWc) stub.cancelRequest(topic, id, message) else legacy.cancelRequest(topic, id, message) + } +} + +internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository { + override val events: Flow = emptyFlow() + override val activeSessions: Flow> = emptyFlow() + override val currentSessions: List = listOf() + + override fun init(projectId: String) = Unit + + override fun setUserNamespaces(userNamespaces: Map>) = Unit + + override fun updateSessions() = Unit + + override fun pair(uri: String, source: SourceType) = Unit + + override fun disconnect(topic: String) = Unit + + override fun approve(userNamespaces: Map>, blockchainNames: List) = Unit + + override fun reject() = Unit + + override fun sendRequest(requestData: RequestData, result: String) = Unit + + override fun rejectRequest(requestData: RequestData, error: WalletConnectError) = Unit + + override fun cancelRequest(topic: String, id: Long, message: String) = Unit +} + @Suppress("LargeClass") internal class DefaultLegacyWalletConnectRepository( private val application: Application, diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 3e47ce122d..265fe31e47 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -10,14 +10,18 @@ import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.domain.walletconnect.WalletConnectSdkHelper import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper import com.tangem.tap.domain.walletconnect2.app.WalletConnectEventsHandlerImpl import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.data.DefaultLegacyWalletConnectRepositoryFacade import com.tangem.tap.domain.walletconnect2.data.DefaultWalletConnectSessionsRepository -import com.tangem.tap.domain.walletconnect2.domain.* +import com.tangem.tap.domain.walletconnect2.data.LegacyWalletConnectRepositoryStub +import com.tangem.tap.domain.walletconnect2.domain.LegacyWalletConnectRepository +import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.domain.walletconnect2.domain.WcJrpcRequestsDeserializer -import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles +import com.tangem.tap.domain.walletconnect2.toggles.DefaultWalletConnectFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -60,7 +64,7 @@ internal object WalletConnectModule { @Provides @Singleton fun provideWalletConnectFeatureToggles(featureTogglesManager: FeatureTogglesManager): WalletConnectFeatureToggles { - return WalletConnectFeatureToggles(featureTogglesManager) + return DefaultWalletConnectFeatureToggles(featureTogglesManager) } @Provides @@ -69,12 +73,19 @@ internal object WalletConnectModule { application: Application, wcRequestDeserializer: WcJrpcRequestsDeserializer, analyticsHandler: AnalyticsEventHandler, + walletConnectFeatureToggles: WalletConnectFeatureToggles, ): LegacyWalletConnectRepository { - return DefaultLegacyWalletConnectRepository( + val legacy = DefaultLegacyWalletConnectRepository( application = application, wcRequestDeserializer = wcRequestDeserializer, analyticsHandler = analyticsHandler, ) + val stub = LegacyWalletConnectRepositoryStub() + return DefaultLegacyWalletConnectRepositoryFacade( + stub, + legacy, + walletConnectFeatureToggles, + ) } @Provides diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/DefaultWalletConnectFeatureToggles.kt similarity index 55% rename from app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt rename to app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/DefaultWalletConnectFeatureToggles.kt index 48b84481db..03cb9b878d 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/WalletConnectFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/toggles/DefaultWalletConnectFeatureToggles.kt @@ -1,11 +1,12 @@ package com.tangem.tap.domain.walletconnect2.toggles import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles -internal class WalletConnectFeatureToggles( +internal class DefaultWalletConnectFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, -) { +) : WalletConnectFeatureToggles { - val isRedesignedWalletConnectEnabled: Boolean + override val isRedesignedWalletConnectEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("WALLET_CONNECT_REDESIGN_ENABLED") } \ No newline at end of file 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 189f0f09e5..c829c5ad78 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,7 +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.* +import com.tangem.features.nft.component.NFTComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent import com.tangem.features.onramp.component.* import com.tangem.features.pushnotifications.api.PushNotificationsComponent @@ -24,7 +24,7 @@ import com.tangem.features.swap.SwapComponent import com.tangem.features.tester.api.TesterRouter import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.wallet.WalletEntryComponent -import com.tangem.tap.domain.walletconnect2.toggles.WalletConnectFeatureToggles +import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.features.details.ui.appcurrency.api.AppCurrencySelectorComponent import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent diff --git a/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectFeatureToggles.kt b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectFeatureToggles.kt new file mode 100644 index 0000000000..3b9e35b56a --- /dev/null +++ b/features/walletconnect/api/src/main/kotlin/com/tangem/features/walletconnect/components/WalletConnectFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.walletconnect.components + +interface WalletConnectFeatureToggles { + val isRedesignedWalletConnectEnabled: Boolean +} \ No newline at end of file From 868fe24c7563725127f7c15479a584fe748b8eea Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 13:35:10 +0500 Subject: [PATCH 114/151] Updated on 2026-08-14 --- .../features/nft/details/entity/NFTAssetUM.kt | 1 + .../entity/factory/NFTDetailsUMFactory.kt | 1 + .../features/nft/details/ui/NFTDetailsAsset.kt | 17 +++++++++++------ .../nft/traits/ui/NFTAssetTraitsContent.kt | 3 +++ 4 files changed, 16 insertions(+), 6 deletions(-) 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 22243c075c..ded848d638 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 @@ -12,6 +12,7 @@ data class NFTAssetUM( val media: Media, val topInfo: TopInfo, val traits: ImmutableList, + val showAllTraitsButton: Boolean, val baseInfoItems: ImmutableList, ) { 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 f23446ab57..afe6f46072 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 @@ -70,6 +70,7 @@ internal class NFTDetailsUMFactory( value = it.value, ) }.toImmutableList(), + showAllTraitsButton = traits.size > MAX_TRAITS_COUNT, baseInfoItems = buildBaseInfoItems(), ) 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 233c56d281..3edf28fee7 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 @@ -70,12 +70,16 @@ internal fun NFTDetailsAsset( .padding(top = TangemTheme.dimens.spacing12), items = state.traits, title = resourceReference(R.string.nft_details_traits), - action = { - NFTBlocksGroupAction( - text = resourceReference(R.string.common_see_all), - startIcon = { }, - onClick = onSeeAllTraitsClick, - ) + action = if (state.showAllTraitsButton) { + { + NFTBlocksGroupAction( + text = resourceReference(R.string.common_see_all), + startIcon = { }, + onClick = onSeeAllTraitsClick, + ) + } + } else { + null }, ) NFTDetailsBlocksGroup( @@ -162,6 +166,7 @@ private class NFTAssetProvider : CollectionPreviewParameterProvider( value = "DJ Dragoon", ), ), + showAllTraitsButton = true, baseInfoItems = persistentListOf( NFTAssetUM.BlockItem( title = resourceReference(R.string.nft_details_token_standard), 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 index c51f428b21..06530835a6 100644 --- 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 @@ -2,6 +2,7 @@ package com.tangem.features.nft.traits.ui import android.content.res.Configuration import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -23,6 +24,7 @@ import kotlinx.collections.immutable.persistentListOf internal fun NFTAssetTraitsContent(state: NFTAssetTraitsUM, modifier: Modifier = Modifier) { InformationBlock( modifier = modifier + .fillMaxWidth() .padding(TangemTheme.dimens.spacing16), title = null, contentHorizontalPadding = 0.dp, @@ -35,6 +37,7 @@ internal fun NFTAssetTraitsContent(state: NFTAssetTraitsUM, modifier: Modifier = key(trait.id) { NFTAssetTrait( modifier = Modifier + .fillMaxWidth() .padding( horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing8, From 0f9ba0ce7e6ec88a06548feede0b389f6c08a738 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 15:44:00 +0700 Subject: [PATCH 115/151] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 24 +++++++----------- .../initialize/DefaultWcInitializeUseCase.kt | 8 +++++- .../pair/DefaultWcPairService.kt | 17 +++++++++++++ .../pair/DefaultWcPairUseCase.kt | 19 +++++++++----- .../walletconnect/DefaultWcPairUseCaseTest.kt | 25 ++++++++++++------- domain/wallet-connect/models/build.gradle.kts | 4 +++ .../walletconnect/model/WcPairRequest.kt | 11 ++++++++ .../model/sdkcopy/WcSdkSessionRequest.kt | 4 +++ .../domain/walletconnect/WcPairService.kt | 10 ++++++++ .../usecase/initialize/WcInitializeUseCase.kt | 2 +- .../usecase/pair/WcPairUseCase.kt | 11 ++++---- .../components/ConnectionsComponent.kt | 4 +-- .../components/WcAppInfoContainerComponent.kt | 4 +-- 13 files changed, 101 insertions(+), 42 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcPairService.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 3c2b20e4f5..5035e927cd 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 @@ -9,6 +9,7 @@ import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork 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.DefaultWcPairService import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.request.DefaultWcRequestService @@ -18,10 +19,11 @@ 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 +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore -import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WalletConnectRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager @@ -58,32 +60,24 @@ internal object WalletConnectDataModule { sessionsManager: DefaultWcSessionsManager, networkService: DefaultWcRequestService, pairSdkDelegate: WcPairSdkDelegate, + environmentConfigStorage: EnvironmentConfigStorage, + dispatchers: CoroutineDispatcherProvider, ): WcInitializeUseCase = DefaultWcInitializeUseCase( application = application, sessionsManager = sessionsManager, networkService = networkService, pairSdkDelegate = pairSdkDelegate, + environmentConfigStorage = environmentConfigStorage, + dispatchers = dispatchers, ) @Provides @Singleton - fun defaultWcPairUseCase( - sessionsManager: WcSessionsManager, - associateNetworksDelegate: AssociateNetworksDelegate, - caipNamespaceDelegate: CaipNamespaceDelegate, - sdkDelegate: WcPairSdkDelegate, - blockAidVerifier: BlockAidVerifier, - ): DefaultWcPairUseCase = DefaultWcPairUseCase( - sessionsManager = sessionsManager, - associateNetworksDelegate = associateNetworksDelegate, - caipNamespaceDelegate = caipNamespaceDelegate, - sdkDelegate = sdkDelegate, - blockAidVerifier = blockAidVerifier, - ) + fun defaultWcPairUseCase(): WcPairService = DefaultWcPairService() @Provides @Singleton - fun wcPairUseCase(default: DefaultWcPairUseCase): WcPairUseCase = default + fun wcPairUseCaseFactory(default: DefaultWcPairUseCase.Factory): WcPairUseCase.Factory = default @Provides @Singleton 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 91db31c6c8..64ea5578f6 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 @@ -10,7 +10,10 @@ 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 +import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.withContext import timber.log.Timber internal class DefaultWcInitializeUseCase( @@ -18,6 +21,8 @@ internal class DefaultWcInitializeUseCase( private val sessionsManager: DefaultWcSessionsManager, private val networkService: DefaultWcRequestService, private val pairSdkDelegate: WcPairSdkDelegate, + private val environmentConfigStorage: EnvironmentConfigStorage, + private val dispatchers: CoroutineDispatcherProvider, ) : WcInitializeUseCase { private val wcSdkObservers = mutableSetOf( @@ -26,7 +31,8 @@ internal class DefaultWcInitializeUseCase( pairSdkDelegate, ) - override fun init(projectId: String) { + override suspend fun init() = withContext(dispatchers.io) { + val projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId val relayUrl = "relay.walletconnect.com" val serverUrl = "wss://$relayUrl?projectId=$projectId" val connectionType = ConnectionType.AUTOMATIC diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt new file mode 100644 index 0000000000..bb0c819b4b --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairService.kt @@ -0,0 +1,17 @@ +package com.tangem.data.walletconnect.pair + +import com.tangem.domain.walletconnect.WcPairService +import com.tangem.domain.walletconnect.model.WcPairRequest +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.receiveAsFlow +import javax.inject.Inject + +class DefaultWcPairService @Inject constructor() : WcPairService { + private val _pairFlow: Channel = Channel(Channel.BUFFERED) + override val pairFlow: Flow = _pairFlow.receiveAsFlow() + + override fun pair(request: WcPairRequest) { + _pairFlow.trySend(request) + } +} \ No newline at end of file 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 107fb0289f..fc91e5eefe 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 @@ -9,15 +9,15 @@ import com.domain.blockaid.models.dapp.DAppData import com.reown.walletkit.client.Wallet import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.blockaid.BlockAidVerifier -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 com.tangem.domain.walletconnect.model.* 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.UserWallet +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first @@ -27,17 +27,19 @@ import timber.log.Timber val unsupportedDApps = listOf("dYdX", "dYdX v4", "Apex Pro", "The Sandbox") -internal class DefaultWcPairUseCase( +internal class DefaultWcPairUseCase @AssistedInject constructor( private val sessionsManager: WcSessionsManager, private val associateNetworksDelegate: AssociateNetworksDelegate, private val caipNamespaceDelegate: CaipNamespaceDelegate, private val sdkDelegate: WcPairSdkDelegate, private val blockAidVerifier: BlockAidVerifier, + @Assisted private val pairRequest: WcPairRequest, ) : WcPairUseCase { private val onCallTerminalAction = Channel() - override fun pairFlow(uri: String, source: WcPairUseCase.Source): Flow { + override operator fun invoke(): Flow { + val (uri: String, source: WcPairRequest.Source) = pairRequest return flow { emit(WcPairState.Loading) @@ -151,4 +153,9 @@ internal class DefaultWcPairUseCase( data class Approve(val sessionForApprove: WcSessionApprove) : TerminalAction data object Reject : TerminalAction } + + @AssistedFactory + interface Factory : WcPairUseCase.Factory { + override fun create(pairRequest: WcPairRequest): DefaultWcPairUseCase + } } \ No newline at end of file diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 2a6827c8c9..1792340585 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -15,11 +15,11 @@ import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.walletconnect.model.WcPairError +import com.tangem.domain.walletconnect.model.WcPairRequest 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 @@ -37,7 +37,7 @@ internal class DefaultWcPairUseCaseTest { private val blockAidVerifier: BlockAidVerifier = mockk() private val url = "testUrl" - private val source = WcPairUseCase.Source.QR + private val source = WcPairRequest.Source.QR private val loading = WcPairState.Loading private val sdkProposal: Wallet.Model.SessionProposal get() = Wallet.Model.SessionProposal( @@ -93,12 +93,13 @@ internal class DefaultWcPairUseCaseTest { securityStatus = CheckDAppResult.SAFE, ) - private val useCase = DefaultWcPairUseCase( + private fun useCaseFactory() = DefaultWcPairUseCase( sessionsManager = sessionsManager, associateNetworksDelegate = associateNetworksDelegate, caipNamespaceDelegate = caipNamespaceDelegate, sdkDelegate = sdkDelegate, blockAidVerifier = blockAidVerifier, + pairRequest = WcPairRequest(url, source), ) @Before @@ -118,7 +119,8 @@ internal class DefaultWcPairUseCaseTest { coEvery { sdkDelegate.pair(url) } returns sdkProposal.right() coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } - useCase.pairFlow(url, source).test { + val useCase = useCaseFactory() + useCase.invoke().test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) @@ -140,7 +142,8 @@ internal class DefaultWcPairUseCaseTest { coEvery { sessionsManager.saveSession(sessionForSave) } returns Unit coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } - useCase.pairFlow(url, source).test { + val useCase = useCaseFactory() + useCase.invoke().test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) @@ -169,7 +172,8 @@ internal class DefaultWcPairUseCaseTest { coEvery { sdkDelegate.rejectSession(proposerPublicKey) } returns Unit coEvery { blockAidVerifier.verifyDApp(any()) } returns Either.catch { CheckDAppResult.SAFE } - useCase.pairFlow(url, source).test { + val useCase = useCaseFactory() + useCase.invoke().test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) @@ -189,7 +193,8 @@ internal class DefaultWcPairUseCaseTest { coEvery { sdkDelegate.pair(url) } returns unsupportedSdkProposal.right() val unsupportedDAppError = WcPairState.Error(WcPairError.UnsupportedDApp) - useCase.pairFlow(url, source).test { + val useCase = useCaseFactory() + useCase.invoke().test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) @@ -205,7 +210,8 @@ internal class DefaultWcPairUseCaseTest { coEvery { sdkDelegate.pair(url) } returns error.left() val errorState = WcPairState.Error(error) - useCase.pairFlow(url, source).test { + val useCase = useCaseFactory() + useCase.invoke().test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) @@ -225,7 +231,8 @@ internal class DefaultWcPairUseCaseTest { val errorResult = WcPairState.Approving.Result(sessionForApprove, error) - useCase.pairFlow(url, source).test { + val useCase = useCaseFactory() + useCase.invoke().test { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) diff --git a/domain/wallet-connect/models/build.gradle.kts b/domain/wallet-connect/models/build.gradle.kts index 4fd593d3d4..f7455512a0 100644 --- a/domain/wallet-connect/models/build.gradle.kts +++ b/domain/wallet-connect/models/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.kotlin.jvm) alias(deps.plugins.ksp) + alias(deps.plugins.kotlin.serialization) id("configuration") } dependencies { @@ -13,4 +14,7 @@ dependencies { /* Other */ implementation(deps.moshi) ksp(deps.moshi.kotlin.codegen) + + /* Utils */ + implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt new file mode 100644 index 0000000000..1c1c3f3674 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcPairRequest.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.walletconnect.model + +import kotlinx.serialization.Serializable + +@Serializable +data class WcPairRequest( + val uri: String, + val source: Source, +) { + enum class Source { QR, DEEPLINK, CLIPBOARD, ETC } +} \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt index 7b2ed2ff37..02240181d0 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/sdkcopy/WcSdkSessionRequest.kt @@ -1,14 +1,18 @@ package com.tangem.domain.walletconnect.model.sdkcopy +import kotlinx.serialization.Serializable + /** * copy of [com.reown.walletkit.client.Wallet.Model.SessionRequest] */ +@Serializable data class WcSdkSessionRequest( val topic: String, val chainId: String?, val request: JSONRPCRequest, ) { + @Serializable data class JSONRPCRequest( val id: Long, val method: String, diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcPairService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcPairService.kt new file mode 100644 index 0000000000..e996aa25ac --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcPairService.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.walletconnect + +import com.tangem.domain.walletconnect.model.WcPairRequest +import kotlinx.coroutines.flow.Flow + +interface WcPairService { + val pairFlow: Flow + + fun pair(request: WcPairRequest) +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt index 71f465a43f..0e00484903 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt @@ -1,5 +1,5 @@ package com.tangem.domain.walletconnect.usecase.initialize interface WcInitializeUseCase { - fun init(projectId: String) + suspend fun init() } \ 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 b926213140..1097cfc7a5 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,20 +1,19 @@ 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 com.tangem.domain.walletconnect.model.* import kotlinx.coroutines.flow.Flow interface WcPairUseCase { - fun pairFlow(uri: String, source: Source): Flow + operator fun invoke(): Flow fun approve(sessionForApprove: WcSessionApprove) fun reject() - enum class Source { QR, DEEPLINK, CLIPBOARD, ETC } + interface Factory { + fun create(pairRequest: WcPairRequest): WcPairUseCase + } } sealed interface WcPairState { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt index 79b63e953b..0c311f6f4a 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt @@ -13,7 +13,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.features.walletconnect.connections.model.WcConnectionsModel import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetRoutes import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent @@ -48,7 +48,7 @@ internal class ConnectionsComponent( appComponentContext = childByContext(componentContext), params = WcAppInfoContainerComponent.Params( wcUrl = config.wcUrl, - source = WcPairUseCase.Source.QR, + source = WcPairRequest.Source.QR, onDismiss = model.bottomSheetNavigation::dismiss, ), ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt index 83f3853ca6..39c9b6740d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcAppInfoContainerComponent.kt @@ -20,7 +20,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.features.walletconnect.connections.model.WcAppInfoModel import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.features.walletconnect.impl.R @@ -103,5 +103,5 @@ internal class WcAppInfoContainerComponent( WcAppInfoRoutes.SelectWallet -> TODO() } - data class Params(val wcUrl: String, val source: WcPairUseCase.Source, val onDismiss: () -> Unit) + data class Params(val wcUrl: String, val source: WcPairRequest.Source, val onDismiss: () -> Unit) } \ No newline at end of file From ba9113019a5e25a8112da3b349b98efeaeb57cf4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 14:01:08 +0500 Subject: [PATCH 116/151] Updated on 2026-08-14 --- .../FirebasePushNotificationsTokenProvider.kt | 11 +++ .../tap/di/data/PushNotificationsModule.kt | 18 +++++ .../di/domain/NotificationsDomainModule.kt | 14 ++++ .../PushNotificationsTokenProvider.kt | 6 ++ domain/notifications/build.gradle.kts | 1 + .../notifications/SendPushTokenUseCase.kt | 19 +++++ .../GetApplicationIdUseCaseTest.kt | 1 - .../notifications/SendPushTokenUseCaseTest.kt | 79 +++++++++++++++++++ 8 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt create mode 100644 app/src/main/java/com/tangem/tap/di/data/PushNotificationsModule.kt create mode 100644 core/utils/src/main/java/com/tangem/utils/notifications/PushNotificationsTokenProvider.kt create mode 100644 domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt create mode 100644 domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt diff --git a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt new file mode 100644 index 0000000000..eaf9730f21 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt @@ -0,0 +1,11 @@ +package com.tangem.tap.data + +import com.google.firebase.messaging.FirebaseMessaging +import com.tangem.utils.notifications.PushNotificationsTokenProvider +import kotlinx.coroutines.tasks.await + +class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { + override suspend fun getToken(): String { + return FirebaseMessaging.getInstance().token.await() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/data/PushNotificationsModule.kt b/app/src/main/java/com/tangem/tap/di/data/PushNotificationsModule.kt new file mode 100644 index 0000000000..063efaf3dc --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/data/PushNotificationsModule.kt @@ -0,0 +1,18 @@ +package com.tangem.tap.di.data + +import com.tangem.tap.data.FirebasePushNotificationsTokenProvider +import com.tangem.utils.notifications.PushNotificationsTokenProvider +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 PushNotificationsModule { + + @Binds + @Singleton + fun bindPushNotificationsTokenProvider(impl: FirebasePushNotificationsTokenProvider): PushNotificationsTokenProvider +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt index caad0bce3a..fe5ea6d52f 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NotificationsDomainModule.kt @@ -1,7 +1,9 @@ package com.tangem.tap.di.domain import com.tangem.domain.notifications.GetApplicationIdUseCase +import com.tangem.domain.notifications.SendPushTokenUseCase import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.utils.notifications.PushNotificationsTokenProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -18,4 +20,16 @@ internal object NotificationsDomainModule { GetApplicationIdUseCase( notificationsRepository = notificationsRepository, ) + + @Provides + @Singleton + fun providesSendPushTokenUseCase( + notificationsRepository: NotificationsRepository, + getApplicationIdUseCase: GetApplicationIdUseCase, + pushNotificationsTokenProvider: PushNotificationsTokenProvider, + ): SendPushTokenUseCase = SendPushTokenUseCase( + notificationsRepository = notificationsRepository, + getApplicationIdUseCase = getApplicationIdUseCase, + pushNotificationsTokenProvider = pushNotificationsTokenProvider, + ) } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/notifications/PushNotificationsTokenProvider.kt b/core/utils/src/main/java/com/tangem/utils/notifications/PushNotificationsTokenProvider.kt new file mode 100644 index 0000000000..2d14d8c9ad --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/notifications/PushNotificationsTokenProvider.kt @@ -0,0 +1,6 @@ +package com.tangem.utils.notifications + +interface PushNotificationsTokenProvider { + + suspend fun getToken(): String +} \ No newline at end of file diff --git a/domain/notifications/build.gradle.kts b/domain/notifications/build.gradle.kts index ae59c08bd3..eba62986ce 100644 --- a/domain/notifications/build.gradle.kts +++ b/domain/notifications/build.gradle.kts @@ -5,6 +5,7 @@ plugins { } dependencies { + implementation(projects.core.utils) implementation(projects.domain.core) implementation(projects.domain.notifications.models) diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt new file mode 100644 index 0000000000..45617cf6e4 --- /dev/null +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/SendPushTokenUseCase.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.notifications + +import arrow.core.Either +import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.utils.notifications.PushNotificationsTokenProvider + +class SendPushTokenUseCase( + private val notificationsRepository: NotificationsRepository, + private val getApplicationIdUseCase: GetApplicationIdUseCase, + private val pushNotificationsTokenProvider: PushNotificationsTokenProvider, +) { + + suspend operator fun invoke(): Either = Either.catch { + val applicationId = getApplicationIdUseCase().getOrNull() + ?: error("Application ID not found") + val token = pushNotificationsTokenProvider.getToken() + notificationsRepository.sendPushToken(applicationId, token) + } +} \ No newline at end of file diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt index 8f562e98b7..350f5bad73 100644 --- a/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/GetApplicationIdUseCaseTest.kt @@ -97,7 +97,6 @@ class GetApplicationIdUseCaseTest { val results = coroutineScope { List(PARALLEL_COUNT) { async { - delay(100) useCase() } }.awaitAll() diff --git a/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt new file mode 100644 index 0000000000..aeb18310a1 --- /dev/null +++ b/domain/notifications/src/test/java/com/tangem/domain/notifications/SendPushTokenUseCaseTest.kt @@ -0,0 +1,79 @@ +package com.tangem.domain.notifications + +import arrow.core.Either +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.utils.notifications.PushNotificationsTokenProvider +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test + +class SendPushTokenUseCaseTest { + + private lateinit var notificationsRepository: NotificationsRepository + private lateinit var getApplicationIdUseCase: GetApplicationIdUseCase + private lateinit var pushNotificationsTokenProvider: PushNotificationsTokenProvider + private lateinit var sendPushTokenUseCase: SendPushTokenUseCase + + @Before + fun setup() { + notificationsRepository = mockk() + getApplicationIdUseCase = mockk() + pushNotificationsTokenProvider = mockk() + sendPushTokenUseCase = SendPushTokenUseCase( + notificationsRepository = notificationsRepository, + getApplicationIdUseCase = getApplicationIdUseCase, + pushNotificationsTokenProvider = pushNotificationsTokenProvider, + ) + } + + @Test + fun `GIVEN valid application ID and token WHEN invoke THEN token is sent successfully`() = runTest { + // GIVEN + val applicationId = "test-app-id" + val token = "test-token" + coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) + coEvery { pushNotificationsTokenProvider.getToken() } returns token + coEvery { notificationsRepository.sendPushToken(applicationId, token) } returns Unit + + // WHEN + val result = sendPushTokenUseCase() + + // THEN + assertThat(result).isEqualTo(Either.Right(Unit)) + coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } + } + + @Test + fun `GIVEN application ID is not found WHEN invoke THEN throws error`() = runTest { + // GIVEN + coEvery { getApplicationIdUseCase() } returns Either.Left(Throwable("Application ID not found")) + + // WHEN & THEN + val result = sendPushTokenUseCase() + assertThat(result.isLeft()).isTrue() + assertThat(result.fold({ it.message }, { null })).isEqualTo("Application ID not found") + coVerify(exactly = 0) { notificationsRepository.sendPushToken(any(), any()) } + } + + @Test + fun `GIVEN repository throws error WHEN invoke THEN returns error`() = runTest { + // GIVEN + val applicationId = "test-app-id" + val token = "test-token" + val expectedError = RuntimeException("Network error") + coEvery { getApplicationIdUseCase() } returns Either.Right(applicationId) + coEvery { pushNotificationsTokenProvider.getToken() } returns token + coEvery { notificationsRepository.sendPushToken(applicationId, token) } throws expectedError + + // WHEN + val result = sendPushTokenUseCase() + + // THEN + assertThat(result).isEqualTo(Either.Left(expectedError)) + coVerify(exactly = 1) { notificationsRepository.sendPushToken(applicationId, token) } + } +} \ No newline at end of file From 0bd68de818da88836b355195b34441a3afc9628e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 13:03:02 +0500 Subject: [PATCH 117/151] Updated on 2026-08-14 --- .../assets/configs/feature_toggles_config.json | 8 ++++++++ data/nft/build.gradle.kts | 3 +++ .../com/tangem/data/nft/DefaultNFTRepository.kt | 15 +++++++++++++-- .../com/tangem/features/nft/NFTFeatureToggles.kt | 2 ++ .../features/nft/DefaultNFTFeatureToggles.kt | 6 ++++++ 5 files changed, 32 insertions(+), 2 deletions(-) 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 aa583d661b..eb8297c289 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 @@ -27,6 +27,14 @@ "name": "NFT_ENABLED", "version": "undefined" }, + { + "name": "NFT_EVM_ENABLED", + "version": "undefined" + }, + { + "name": "NFT_SOLANA_ENABLED", + "version": "undefined" + }, { "name": "STAKING_CARDANO_ENABLED", "version": "undefined" diff --git a/data/nft/build.gradle.kts b/data/nft/build.gradle.kts index 8d141c90e8..2e0f7cb455 100644 --- a/data/nft/build.gradle.kts +++ b/data/nft/build.gradle.kts @@ -30,6 +30,9 @@ dependencies { implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) + /** Feature Api modules */ + implementation(projects.features.nft.api) + /** Libs - Other */ implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) 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 aa6b40e845..fb01e242d4 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 @@ -22,6 +22,7 @@ import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.features.nft.NFTFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -34,7 +35,7 @@ import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset import com.tangem.blockchain.nft.models.NFTCollection as SdkNFTCollection -@Suppress("LargeClass") +@Suppress("LargeClass", "LongParameterList") internal class DefaultNFTRepository @Inject constructor( private val nftPersistenceStoreFactory: NFTPersistenceStoreFactory, private val nftRuntimeStoreFactory: NFTRuntimeStoreFactory, @@ -42,6 +43,7 @@ internal class DefaultNFTRepository @Inject constructor( private val dispatchers: CoroutineDispatcherProvider, private val excludedBlockchains: ExcludedBlockchains, private val userWalletsStore: UserWalletsStore, + private val nftFeatureToggles: NFTFeatureToggles, ) : NFTRepository { private val networkJobs = ConcurrentHashMap() @@ -479,5 +481,14 @@ internal class DefaultNFTRepository @Inject constructor( return walletId.stringValue + "_" + network.id.value + "_" + network.derivationPath.value } - private fun Network.canHandleNFTs(): Boolean = Blockchain.fromNetworkId(backendId)?.canHandleNFTs() == true + private fun Network.canHandleNFTs(): Boolean { + val blockchain = Blockchain.fromNetworkId(backendId) + return when { + blockchain == null -> false + blockchain.isEvm() && !nftFeatureToggles.isNFTEVMEnabled -> false + (blockchain == Blockchain.Solana || blockchain == Blockchain.SolanaTestnet) && + !nftFeatureToggles.isNFTSolanaEnabled -> false + else -> blockchain.canHandleNFTs() + } + } } \ No newline at end of file diff --git a/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt b/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt index a3b0ad1cb5..8ae8a9216e 100644 --- a/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt +++ b/features/nft/api/src/main/kotlin/com/tangem/features/nft/NFTFeatureToggles.kt @@ -2,4 +2,6 @@ package com.tangem.features.nft interface NFTFeatureToggles { val isNFTEnabled: Boolean + val isNFTEVMEnabled: Boolean + val isNFTSolanaEnabled: Boolean } \ No newline at end of file diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt index 7697217e52..a5b51c50e3 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/DefaultNFTFeatureToggles.kt @@ -7,4 +7,10 @@ internal class DefaultNFTFeatureToggles( ) : NFTFeatureToggles { override val isNFTEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(name = "NFT_ENABLED") + + override val isNFTEVMEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "NFT_EVM_ENABLED") + + override val isNFTSolanaEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(name = "NFT_SOLANA_ENABLED") } \ No newline at end of file From bec02844f409d844b3e324b15fba492d11092c60 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 09:21:37 +0000 Subject: [PATCH 118/151] 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 8c55070de7..a0bc01da42 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-1048" +tangemBlockchainSdk = "develop-1042" #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 546a9decb08b3ae9e1bfe0ff407047c29c963f78 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 14:28:29 +0500 Subject: [PATCH 119/151] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 2 +- .../local/nft/DefaultNFTRuntimeStore.kt | 30 +++++++++++++------ .../tangem/domain/nft/RefreshAllNFTUseCase.kt | 3 +- features/nft/impl/build.gradle.kts | 1 + ...hangeCollectionExpandedStateTransformer.kt | 6 ++-- .../transformer/UpdateDataStateTransformer.kt | 9 +++--- .../collections/model/NFTCollectionsModel.kt | 11 +++++-- 7 files changed, 42 insertions(+), 20 deletions(-) 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 0578e95316..090a030029 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 @@ -37,7 +37,7 @@ internal object NFTDomainModule { @Provides @Singleton - fun providesRefreshAllUseCase( + fun providesRefreshAllNFTUseCase( currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, ): RefreshAllNFTUseCase = RefreshAllNFTUseCase( diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt index cf693aab39..31a46a4a12 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/DefaultNFTRuntimeStore.kt @@ -96,21 +96,33 @@ internal class DefaultNFTRuntimeStore( private fun NFTCollections.Content.Collections.mergeWithPrices(prices: Map) = copy( - collections = this.collections?.map { data -> - data.copy( - assets = when (val assets = data.assets) { + collections = this.collections + ?.map { data -> + val assets = when (val assets = data.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Loading, is NFTCollection.Assets.Failed, -> assets is NFTCollection.Assets.Value -> assets.copy( - items = assets.items.map { asset -> - asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id)) - }, + items = assets.items + .filter { !it.name.isNullOrEmpty() } + .sortedBy { it.name } + .map { asset -> + asset.mergeWithPrice(prices[asset.id] ?: NFTSalePrice.Empty(asset.id)) + }, ) - }, - ) - }, + } + val assetsCount = when (assets) { + is NFTCollection.Assets.Value -> assets.items.size + else -> data.count + } + data.copy( + assets = assets, + count = assetsCount, + ) + } + ?.filter { it.count > 0 } + ?.sortedBy { it.name }, source = this.source, ) diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt index 2e365e88a8..b99c2c7cad 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/RefreshAllNFTUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.nft +import arrow.core.Either import com.tangem.domain.nft.repository.NFTRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.wallets.models.UserWalletId @@ -9,7 +10,7 @@ class RefreshAllNFTUseCase( private val nftRepository: NFTRepository, ) { - suspend operator fun invoke(userWalletId: UserWalletId) { + suspend operator fun invoke(userWalletId: UserWalletId): Either = Either.catch { val currencies = currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) nftRepository.refreshAll(userWalletId, currencies.map { it.network }.distinct()) } diff --git a/features/nft/impl/build.gradle.kts b/features/nft/impl/build.gradle.kts index df09cb6d6a..b30b854ca8 100644 --- a/features/nft/impl/build.gradle.kts +++ b/features/nft/impl/build.gradle.kts @@ -63,6 +63,7 @@ dependencies { implementation(deps.androidx.datastore) /** Other libraries */ + implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.kotlin.serialization) implementation(deps.timber) diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt index 867c3d2303..56ebea5c6b 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/entity/transformer/ChangeCollectionExpandedStateTransformer.kt @@ -7,7 +7,8 @@ import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList internal class ChangeCollectionExpandedStateTransformer( - private val collectionId: NFTCollection.Identifier, + private val collection: NFTCollection, + private val collectionIdProvider: NFTCollection.() -> String, private val onFirstExpanded: () -> Unit, ) : Transformer { @@ -19,7 +20,8 @@ internal class ChangeCollectionExpandedStateTransformer( -> prevState.content is NFTCollectionsUM.Content -> prevState.content.copy( collections = prevState.content.collections.map { - if (it.id == collectionId.toString()) { + val collectionId = collection.collectionIdProvider() + if (it.id == collectionId) { if (!it.isExpanded) { onFirstExpanded() } 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 bb684cf18b..a73a5aba58 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 @@ -23,6 +23,7 @@ internal class UpdateDataStateTransformer( private val onRetryAssetsClick: (NFTCollection) -> Unit, private val onAssetClick: (NFTAsset, String) -> Unit, private val initialSearchBarFactory: () -> SearchBarUM, + private val collectionIdProvider: NFTCollection.() -> String, ) : Transformer { override fun transform(prevState: NFTCollectionsStateUM): NFTCollectionsStateUM { @@ -47,9 +48,7 @@ internal class UpdateDataStateTransformer( else -> { NFTCollectionsUM.Content( search = if (prevState.content is NFTCollectionsUM.Content) { - prevState.content.search.copy( - query = searchQuery, - ) + prevState.content.search } else { initialSearchBarFactory() }, @@ -100,7 +99,7 @@ internal class UpdateDataStateTransformer( if (collectionFulfillQuery || assetsFulfillQuery) { NFTCollectionUM( - id = it.id.toString(), + id = it.collectionIdProvider(), networkIconId = getActiveIconRes(it.network.id.value), name = it.name, description = TextReference.PluralRes( @@ -165,7 +164,7 @@ internal class UpdateDataStateTransformer( private fun NFTCollection.isExpanded(state: NFTCollectionsStateUM): Boolean = (state.content as? NFTCollectionsUM.Content) ?.collections - ?.firstOrNull { it.id == id.toString() } + ?.firstOrNull { it.id == this.collectionIdProvider() } ?.isExpanded ?: false } \ 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 c35c9fc81f..72b200907a 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 @@ -23,6 +23,7 @@ import com.tangem.features.nft.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -57,9 +58,12 @@ internal class NFTCollectionsModel @Inject constructor( ), ), ) - val state: StateFlow get() = _state + private val collectionIdProvider: NFTCollection.() -> String = { + "${network.name}_${network.derivationPath.value}_$id" + } + init { subscribeToNFTCollections() } @@ -83,6 +87,7 @@ internal class NFTCollectionsModel @Inject constructor( params.onAssetClick(asset, collectionName) }, initialSearchBarFactory = ::getInitialSearchBar, + collectionIdProvider = collectionIdProvider, ).transform(it) } } @@ -95,6 +100,7 @@ internal class NFTCollectionsModel @Inject constructor( _state.update { ChangeRefreshingStateTransformer(true).transform(it) } try { refreshAllNFTUseCase(params.userWalletId) + .onLeft { Timber.e(it) } } finally { _state.update { ChangeRefreshingStateTransformer(false).transform(it) } } @@ -126,7 +132,8 @@ internal class NFTCollectionsModel @Inject constructor( private fun onExpandCollectionClick(collection: NFTCollection) { _state.update { ChangeCollectionExpandedStateTransformer( - collectionId = collection.id, + collection = collection, + collectionIdProvider = collectionIdProvider, onFirstExpanded = { onFirstExpanded(collection) }, ).transform(it) } From 150026e2bd06c0424d8afef121f50b0360406bdb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 16:36:36 +0700 Subject: [PATCH 120/151] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 21 +++++++--- .../network/ethereum/WcEthNetwork.kt | 39 +++++++++---------- .../network/solana/WcSolanaNetwork.kt | 27 +++++++------ .../request/DefaultWcRequestService.kt | 23 +++++------ .../request/DefaultWcRequestUseCaseFactory.kt | 17 ++++++++ .../request/WcRequestToUseCaseConverter.kt | 2 + .../walletconnect/model/WcMethodName.kt | 22 +++++++++++ .../domain/walletconnect/WcRequestService.kt | 9 +++++ .../walletconnect/WcRequestUseCaseFactory.kt | 9 +++++ 9 files changed, 115 insertions(+), 54 deletions(-) create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt create mode 100644 domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcRequestService.kt create mode 100644 domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcRequestUseCaseFactory.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 5035e927cd..0afaeb4dd7 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 @@ -12,7 +12,9 @@ import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate import com.tangem.data.walletconnect.pair.DefaultWcPairService import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.pair.WcPairSdkDelegate +import com.tangem.data.walletconnect.pair.* import com.tangem.data.walletconnect.request.DefaultWcRequestService +import com.tangem.data.walletconnect.request.DefaultWcRequestUseCaseFactory import com.tangem.data.walletconnect.request.WcRequestToUseCaseConverter import com.tangem.data.walletconnect.respond.DefaultWcRespondService import com.tangem.data.walletconnect.respond.WcRespondService @@ -24,6 +26,8 @@ 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.WcPairService +import com.tangem.domain.walletconnect.WcRequestService +import com.tangem.domain.walletconnect.WcRequestUseCaseFactory import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository import com.tangem.domain.walletconnect.repository.WalletConnectRepository import com.tangem.domain.walletconnect.repository.WcSessionsManager @@ -107,14 +111,13 @@ internal object WalletConnectDataModule { @Provides @Singleton - fun defaultWcRequestService( - dispatchers: CoroutineDispatcherProvider, - diHelperBox: DiHelperBox, - ): DefaultWcRequestService { - val scope = CoroutineScope(SupervisorJob() + dispatchers.io) + fun wcRequestService(default: DefaultWcRequestService): WcRequestService = default + + @Provides + @Singleton + fun defaultWcRequestService(diHelperBox: DiHelperBox): DefaultWcRequestService { return DefaultWcRequestService( requestConverters = diHelperBox.handlers, - scope = scope, ) } @@ -185,6 +188,12 @@ internal object WalletConnectDataModule { ), ) + @Provides + @Singleton + fun wcRequestUseCaseFactory(diHelperBox: DiHelperBox): WcRequestUseCaseFactory { + return DefaultWcRequestUseCaseFactory(diHelperBox.handlers) + } + internal class DiHelperBox( val converters: Set, val handlers: Set, 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 4f05896657..0611504329 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 @@ -11,6 +11,7 @@ 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.WcEthMethodName import com.tangem.domain.walletconnect.model.WcEthTransactionParams import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager @@ -27,9 +28,14 @@ internal class WcEthNetwork( override val namespaceKey: NamespaceKey = NamespaceKey("eip155") - override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { + override fun toWcMethodName(request: WcSdkSessionRequest): WcEthMethodName? { val methodKey = request.request.method - val name = Name.entries.find { it.raw == methodKey } ?: return null + val name = WcEthMethodName.entries.find { it.raw == methodKey } ?: return null + return name + } + + override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { + val name = toWcMethodName(request) ?: 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 @@ -41,26 +47,26 @@ internal class WcEthNetwork( } } - private fun Name.toMethod(request: WcSdkSessionRequest): WcEthMethod? { + private fun WcEthMethodName.toMethod(request: WcSdkSessionRequest): WcEthMethod? { val rawParams = request.request.params return when (this) { - Name.EthSign, - Name.PersonalSign, + WcEthMethodName.EthSign, + WcEthMethodName.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 accountIndex = if (this == WcEthMethodName.EthSign) 0 else 1 + val messageIndex = if (this == WcEthMethodName.EthSign) 1 else 0 val account = list.getOrNull(accountIndex) ?: return@let null val message = list.getOrNull(messageIndex) ?: return@let null WcEthMethod.MessageSign(account = account, message = message) } - Name.SignTypeData -> TODO() - Name.SignTypeDataV4 -> TODO() - Name.SignTransaction, - Name.SendTransaction, + WcEthMethodName.SignTypeData -> TODO() + WcEthMethodName.SignTypeDataV4 -> TODO() + WcEthMethodName.SignTransaction, + WcEthMethodName.SendTransaction, -> moshi.fromJson>(rawParams) ?.firstOrNull() ?.let { - if (this == Name.SignTransaction) { + if (this == WcEthMethodName.SignTransaction) { WcEthMethod.SignTransaction(transaction = it) } else { WcEthMethod.SendTransaction(transaction = it) @@ -69,15 +75,6 @@ internal class WcEthNetwork( } } - enum class Name(val raw: String) { - EthSign("eth_sign"), - PersonalSign("personal_sign"), - SignTypeData("eth_signTypedData"), - SignTypeDataV4("eth_signTypedData_v4"), - SignTransaction("eth_signTransaction"), - SendTransaction("eth_sendTransaction"), - } - override fun toNetwork(chainId: String, wallet: UserWallet): Network? { return toNetwork(chainId, wallet, excludedBlockchains) } 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 d376a58417..fd4e3b5832 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 @@ -11,6 +11,7 @@ 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.WcSolanaMethod +import com.tangem.domain.walletconnect.model.WcSolanaMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.repository.WcSessionsManager import com.tangem.domain.walletconnect.usecase.WcMethodUseCase @@ -26,9 +27,14 @@ internal class WcSolanaNetwork constructor( override val namespaceKey: NamespaceKey = NamespaceKey("solana") - override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { + override fun toWcMethodName(request: WcSdkSessionRequest): WcSolanaMethodName? { val methodKey = request.request.method - val name = Name.entries.find { it.raw == methodKey } ?: return null + val name = WcSolanaMethodName.entries.find { it.raw == methodKey } ?: return null + return name + } + + override suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? { + val name = toWcMethodName(request) ?: return null val method: WcSolanaMethod = name.toMethod(request) ?: return null val session = sessionsManager.findSessionByTopic(request.topic) ?: return null val network = toNetwork(request.chainId.orEmpty(), session.wallet) ?: return null @@ -67,27 +73,20 @@ internal class WcSolanaNetwork constructor( ) } - private fun Name.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? { + private fun WcSolanaMethodName.toMethod(request: WcSdkSessionRequest): WcSolanaMethod? { val rawParams = request.request.params return when (this) { - Name.SignMessage -> moshi.fromJson(rawParams)?.let { request -> + WcSolanaMethodName.SignMessage -> moshi.fromJson(rawParams)?.let { request -> WcSolanaMethod.SignMessage(pubKey = request.publicKey, message = request.message) } - Name.SignTransaction -> moshi.fromJson(rawParams)?.let { request -> - WcSolanaMethod.SignTransaction(request.transaction) - } - Name.SendAllTransaction -> moshi.fromJson>(rawParams)?.let { list -> + WcSolanaMethodName.SignTransaction -> moshi.fromJson(rawParams) + ?.let { request -> WcSolanaMethod.SignTransaction(request.transaction) } + WcSolanaMethodName.SendAllTransaction -> moshi.fromJson>(rawParams)?.let { list -> WcSolanaMethod.SignAllTransaction(list) } } } - private enum class Name(val raw: String) { - SignMessage("solana_signMessage"), - SignTransaction("solana_signTransaction"), - SendAllTransaction("solana_signAllTransactions"), - } - internal class Factories @Inject constructor( val signTransaction: DefaultWcSolanaSignTransactionUseCase.Factory, val signAllTransaction: DefaultWcSolanaSignAllTransactionUseCase.Factory, 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 d4b9fdbf33..cb39b52a5e 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 @@ -3,20 +3,19 @@ package com.tangem.data.walletconnect.request import com.reown.walletkit.client.Wallet import com.tangem.data.walletconnect.utils.WcSdkObserver import com.tangem.data.walletconnect.utils.WcSdkSessionRequestConverter -import com.tangem.domain.walletconnect.usecase.WcMethodUseCase -import com.tangem.domain.walletconnect.usecase.WcUseCasesFlowProvider -import kotlinx.coroutines.CoroutineScope +import com.tangem.domain.walletconnect.WcRequestService +import com.tangem.domain.walletconnect.model.WcMethodName +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.launch internal class DefaultWcRequestService( private val requestConverters: Set, - private val scope: CoroutineScope, -) : WcSdkObserver, WcUseCasesFlowProvider { +) : WcSdkObserver, WcRequestService { - private val _useCases: Channel = Channel(Channel.BUFFERED) - override val useCases = _useCases.receiveAsFlow() + private val _wcRequest: Channel> = Channel(Channel.BUFFERED) + override val wcRequest: Flow> = _wcRequest.receiveAsFlow() override fun onSessionRequest( sessionRequest: Wallet.Model.SessionRequest, @@ -24,10 +23,8 @@ internal class DefaultWcRequestService( ) { // Triggered when a Dapp sends SessionRequest to sign a transaction or a message val sr = WcSdkSessionRequestConverter.convert(sessionRequest) - scope.launch { - val useCase = requestConverters.firstNotNullOfOrNull { it.toUseCase(sr) } - ?: return@launch // todo(wc) handle unsupported - _useCases.trySend(useCase) - } + val name = requestConverters.firstNotNullOfOrNull { it.toWcMethodName(sr) } + ?: WcMethodName.Unsupported(sr.request.method) + _wcRequest.trySend(name to sr) } } \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt new file mode 100644 index 0000000000..c0bb10ffcc --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/request/DefaultWcRequestUseCaseFactory.kt @@ -0,0 +1,17 @@ +package com.tangem.data.walletconnect.request + +import com.tangem.domain.walletconnect.WcRequestUseCaseFactory +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.usecase.WcMethodUseCase +import javax.inject.Inject + +internal class DefaultWcRequestUseCaseFactory @Inject constructor( + private val requestConverters: Set, +) : WcRequestUseCaseFactory { + + @Suppress("UNCHECKED_CAST") + override suspend fun createUseCase(request: WcSdkSessionRequest): T { + return requestConverters.firstNotNullOf { it.toUseCase(request) } as? T + ?: error("Should check request before in WcRouting") + } +} \ 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 index 624440c0df..d4503c8f73 100644 --- 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 @@ -2,10 +2,12 @@ package com.tangem.data.walletconnect.request import com.squareup.moshi.Moshi import com.squareup.moshi.adapter +import com.tangem.domain.walletconnect.model.WcMethodName import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest import com.tangem.domain.walletconnect.usecase.WcMethodUseCase interface WcRequestToUseCaseConverter { + fun toWcMethodName(request: WcSdkSessionRequest): WcMethodName? suspend fun toUseCase(request: WcSdkSessionRequest): WcMethodUseCase? companion object { diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt new file mode 100644 index 0000000000..7b557f0223 --- /dev/null +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcMethodName.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.walletconnect.model + +sealed interface WcMethodName { + val raw: String + + data class Unsupported(override val raw: String) : WcMethodName +} + +enum class WcEthMethodName(override val raw: String) : WcMethodName { + EthSign("eth_sign"), + PersonalSign("personal_sign"), + SignTypeData("eth_signTypedData"), + SignTypeDataV4("eth_signTypedData_v4"), + SignTransaction("eth_signTransaction"), + SendTransaction("eth_sendTransaction"), +} + +enum class WcSolanaMethodName(override val raw: String) : WcMethodName { + SignMessage("solana_signMessage"), + SignTransaction("solana_signTransaction"), + SendAllTransaction("solana_signAllTransactions"), +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcRequestService.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcRequestService.kt new file mode 100644 index 0000000000..ea6a1dbd17 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcRequestService.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect + +import com.tangem.domain.walletconnect.model.WcMethodName +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import kotlinx.coroutines.flow.Flow + +interface WcRequestService { + val wcRequest: Flow> +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcRequestUseCaseFactory.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcRequestUseCaseFactory.kt new file mode 100644 index 0000000000..4b6858edd6 --- /dev/null +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/WcRequestUseCaseFactory.kt @@ -0,0 +1,9 @@ +package com.tangem.domain.walletconnect + +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import com.tangem.domain.walletconnect.usecase.WcMethodUseCase + +interface WcRequestUseCaseFactory { + + suspend fun createUseCase(request: WcSdkSessionRequest): T +} \ No newline at end of file From cf05b371efd3137c2fc614e3f6cbcf86103e9905 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 14:37:38 +0500 Subject: [PATCH 121/151] Updated on 2026-08-14 --- .../com/tangem/data/nft/DefaultNFTRepository.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) 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 fb01e242d4..28b4ba6492 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 @@ -30,6 +30,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch +import java.lang.UnsupportedOperationException import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset @@ -85,8 +86,6 @@ internal class DefaultNFTRepository @Inject constructor( ) = coroutineScope { launch(dispatchers.io) { Either.catch { - expireAssets(userWalletId, network, collectionId) - val sdkCollectionId = NFTSdkCollectionIdentifierConverter.convertBack(collectionId) val assets = walletManagersFacade.getNFTAssets( @@ -95,6 +94,8 @@ internal class DefaultNFTRepository @Inject constructor( collectionIdentifier = sdkCollectionId, ) + expireAssets(userWalletId, network, collectionId) + assets.forEach { val assetId = NFTSdkAssetIdentifierConverter.convert(it.identifier) val price = getNFTRuntimeStore(userWalletId, network).getSalePriceSync(assetId) @@ -125,11 +126,13 @@ internal class DefaultNFTRepository @Inject constructor( ) } }.onLeft { - saveFailedStateInRuntime( - userWalletId = userWalletId, - network = network, - error = it, - ) + if (it !is UnsupportedOperationException) { + saveFailedStateInRuntime( + userWalletId = userWalletId, + network = network, + error = it, + ) + } } }.saveIn(getCollectionJobHolder(collectionId)).join() } From e080401183ed510a498488ea0e535d4415e8f9ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 13:04:54 +0300 Subject: [PATCH 122/151] Updated on 2026-08-14 --- app/build.gradle.kts | 1 - .../main/java/com/tangem/tap/MainActivity.kt | 16 +- .../com/tangem/tap/common/DialogManager.kt | 15 +- .../com/tangem/tap/common/redux/AppReducer.kt | 8 - .../com/tangem/tap/common/redux/AppState.kt | 16 - .../tap/common/redux/global/GlobalAction.kt | 24 - .../tap/common/redux/global/GlobalReducer.kt | 16 - .../common/redux/legacy/LegacyMiddleware.kt | 11 - .../tangem/tap/di/domain/MnemonicModule.kt | 24 - .../com/tangem/tap/domain/TapWalletManager.kt | 2 - .../domain/scanCard/LegacyScanProcessor.kt | 38 +- .../chains/CheckForOnboardingChain.kt | 42 +- .../tangem/tap/features/demo/DemoHelper.kt | 10 - .../demo/DemoOnboardingNoteMiddleware.kt | 65 - .../cardsettings/model/CardSettingsModel.kt | 28 +- .../tangem/tap/features/main/MainViewModel.kt | 12 + .../features/onboarding/OnboardingDialog.kt | 3 - .../features/onboarding/OnboardingHelper.kt | 242 +- .../features/onboarding/OnboardingManager.kt | 137 +- .../onboarding/OnboardingMenuProvider.kt | 47 - .../products/BaseOnboardingFragment.kt | 54 - .../products/note/OnboardingNoteFragment.kt | 228 -- .../note/redux/OnboardingNoteAction.kt | 39 - .../note/redux/OnboardingNoteMiddleware.kt | 197 -- .../note/redux/OnboardingNoteReducer.kt | 47 - .../note/redux/OnboardingNoteState.kt | 31 - .../OnboardingOtherCardsFragment.kt | 128 -- .../redux/OnboardingOtherCardsAction.kt | 26 - .../redux/OnboardingOtherCardsMiddleware.kt | 129 -- .../redux/OnboardingOtherCardsReducer.kt | 35 - .../redux/OnboardingOtherCardsState.kt | 22 - .../products/twins/redux/TwinCardsAction.kt | 66 - .../twins/redux/TwinCardsMiddleware.kt | 399 ---- .../products/twins/redux/TwinCardsReducer.kt | 77 - .../products/twins/redux/TwinCardsState.kt | 82 - .../twins/ui/OnboardingTwinsFragment.kt | 460 ---- .../TwinningProcessNotCompletedDialog.kt | 21 - .../products/wallet/redux/BackupDialog.kt | 14 + .../products/wallet/redux/BackupMiddleware.kt | 59 + .../wallet/redux/OnboardingWalletAction.kt | 89 - .../redux/OnboardingWalletMiddleware.kt | 849 ------- .../wallet/redux/OnboardingWalletReducer.kt | 185 -- .../wallet/redux/OnboardingWalletState.kt | 128 -- .../products/wallet/ui/BackupAnimator.kt | 314 --- .../products/wallet/ui/BackupInfoAdapter.kt | 57 - .../ui/OnboardingSeedPhraseStateHandler.kt | 82 - .../wallet/ui/OnboardingWalletFragment.kt | 691 ------ .../products/wallet/ui/WalletCardsWidget.kt | 240 -- .../wallet/ui/dialogs/AccessCodeDialog.kt | 81 - .../ui/dialogs/AddMoreBackupCardsDialog.kt | 26 - .../ui/dialogs/AttestationFailedDialog.kt | 24 - .../ui/dialogs/BackupInProgressDialog.kt | 23 - .../ui/dialogs/InterruptOnboardingDialog.kt | 25 - .../ui/dialogs/ResetBackupCardDialog.kt | 30 - .../tangem/tap/routing/utils/ChildFactory.kt | 20 - .../com/tangem/common/routing/AppRoute.kt | 21 - .../configs/feature_toggles_config.json | 12 - .../com/tangem/domain/redux/LegacyAction.kt | 8 - .../v2/OnboardingV2FeatureToggles.kt | 3 - .../v2/DefaultOnboardingV2FeatureToggles.kt | 6 - features/onboarding/.gitignore | 1 - features/onboarding/build.gradle.kts | 64 - .../mnemonic/mnemonic_dictionary_en.txt | 2048 ----------------- .../onboarding/api/OnboardingSeedPhraseApi.kt | 14 - .../api/OnboardingSeedPhraseScreen.kt | 71 - .../data/DefaultMnemonicRepository.kt | 32 - .../onboarding/data/MnemonicRepository.kt | 22 - .../data/model/CreateWalletResponse.kt | 15 - .../onboarding/di/OnboardingRouterModule.kt | 20 - .../domain/DefaultSeedPhraseInteractor.kt | 113 - .../domain/OnboardingModuleError.kt | 41 - .../onboarding/domain/PartWordFinder.kt | 50 - .../onboarding/domain/SeedPhraseInteractor.kt | 27 - .../domain/di/OnboardingDomainModule.kt | 21 - .../onboarding/domain/models/MnemonicType.kt | 5 - .../navigation/DefaultOnboardingRouter.kt | 3 - .../onboarding/navigation/OnboardingRouter.kt | 7 - .../wallet2/analytics/SeedPhraseEvents.kt | 41 - .../presentation/wallet2/model/ButtonState.kt | 11 - .../wallet2/model/DescriptionResource.kt | 8 - .../model/OnboardingSeedPhraseState.kt | 102 - .../presentation/wallet2/model/UiActions.kt | 60 - .../wallet2/ui/AboutSeedPhraseScreen.kt | 151 -- .../wallet2/ui/CheckSeedPhraseScreen.kt | 110 - .../wallet2/ui/ImportSeedPhraseScreen.kt | 268 --- .../presentation/wallet2/ui/IntroScreen.kt | 102 - .../wallet2/ui/PassphraseInfoBottomSheet.kt | 90 - .../wallet2/ui/YourSeedPhraseScreen.kt | 196 -- .../ui/components/OnboardingActionBlock.kt | 39 - .../components/OnboardingDescriptionBlock.kt | 64 - .../CheckSeedPhraseStateBuilder.kt | 77 - .../ImportSeedPhraseStateBuilder.kt | 108 - .../wallet2/ui/stateBuiders/StateBuilder.kt | 197 -- .../InvalidWordsColorTransformation.kt | 79 - .../viewmodel/SeedPhraseErrorConverter.kt | 28 - .../wallet2/viewmodel/SeedPhraseMediator.kt | 22 - .../wallet2/viewmodel/SeedPhraseRouter.kt | 50 - .../wallet2/viewmodel/SeedPhraseViewModel.kt | 484 ---- .../model/WalletSettingsModel.kt | 28 +- features/wallet/impl/build.gradle.kts | 3 - .../intents/WalletWarningsClickIntents.kt | 11 - .../router/DefaultWalletRouter.kt | 28 +- settings.gradle.kts | 1 - 103 files changed, 142 insertions(+), 10555 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/di/domain/MnemonicModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/dialog/TwinningProcessNotCompletedDialog.kt create mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt create mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/InterruptOnboardingDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt delete mode 100644 features/onboarding/.gitignore delete mode 100644 features/onboarding/build.gradle.kts delete mode 100644 features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseApi.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/data/model/CreateWalletResponse.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/di/OnboardingRouterModule.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/models/MnemonicType.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/DefaultOnboardingRouter.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseMediator.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseRouter.kt delete mode 100644 features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d7888942f..154659cd53 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -158,7 +158,6 @@ dependencies { implementation(projects.data.blockaid) /** Features */ - implementation(projects.features.onboarding) implementation(projects.features.referral.impl) implementation(projects.features.referral.domain) implementation(projects.features.referral.data) diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 3eb6102ce8..081728b21c 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -62,13 +62,11 @@ import com.tangem.domain.tokens.GetPolkadotCheckHasImmortalUseCase import com.tangem.domain.tokens.GetPolkadotCheckHasResetUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent -import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService import com.tangem.sdk.api.BackupServiceHolder import com.tangem.sdk.api.TangemSdkManager -import com.tangem.sdk.extensions.init import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback @@ -83,7 +81,6 @@ import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandle import com.tangem.tap.features.intentHandler.handlers.OnPushClickedIntentHandler import com.tangem.tap.features.intentHandler.handlers.WalletConnectLinkIntentHandler import com.tangem.tap.features.main.MainViewModel -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.component.RoutingComponent @@ -181,9 +178,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var backupServiceHolder: BackupServiceHolder - @Inject - lateinit var onboardingV2FeatureToggles: OnboardingV2FeatureToggles - @Inject lateinit var setGoogleServicesAvailabilityUseCase: SetGoogleServicesAvailabilityUseCase @@ -364,12 +358,8 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac cardSdkOwner.register(activity = this) tangemSdkManager = injectedTangemSdkManager - if (onboardingV2FeatureToggles.isOnboardingV2Enabled) { - backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this) - backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually - } else { - backupService = BackupService.init(cardSdkConfigRepository.sdk, this) - } + backupServiceHolder.createAndSetService(cardSdkConfigRepository.sdk, this) + backupService = backupServiceHolder.backupService.get()!! // will be deleted eventually lockUserWalletsTimer = LockUserWalletsTimer( context = this, @@ -662,7 +652,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - store.dispatch(BackupAction.CheckForUnfinishedBackup) + viewModel.checkForUnfinishedBackup() } private fun observePolkadotAccountHealthCheck() { diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index 533cdac6a9..d60fbd6473 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -9,9 +9,11 @@ import com.tangem.tap.common.ui.* import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog import com.tangem.tap.features.details.ui.walletconnect.dialogs.* import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.features.onboarding.products.twins.ui.dialog.TwinningProcessNotCompletedDialog import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog -import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.* +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletActivationErrorDialog +import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.WalletAlreadyWasUsedDialog import com.tangem.tap.store import com.tangem.wallet.R import org.rekotlin.StoreSubscriber @@ -58,8 +60,6 @@ class DialogManager : StoreSubscriber { ) is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context) - is OnboardingDialog.TwinningProcessNotCompleted -> TwinningProcessNotCompletedDialog.create(context) - is OnboardingDialog.InterruptOnboarding -> InterruptOnboardingDialog.create(context, state.dialog) is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog) is WalletConnectDialog.UnsupportedCard -> SimpleAlertDialog.create( @@ -143,9 +143,6 @@ class DialogManager : StoreSubscriber { messageRes = R.string.unsupported_wc_version, context = context, ) - is BackupDialog.AttestationFailed -> AttestationFailedDialog.create(context) - is BackupDialog.AddMoreBackupCards -> AddMoreBackupCardsDialog.create(context) - is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context) is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create( context = context, scanResponse = state.dialog.scanResponse, @@ -154,10 +151,6 @@ class DialogManager : StoreSubscriber { context = context, unfinishedBackupScanResponse = state.dialog.scanResponse, ) - is BackupDialog.ResetBackupCard -> ResetBackupCardDialog.create( - context = context, - cardId = state.dialog.cardId, - ) is AppDialog.TokensAreLinkedDialog -> SimpleAlertDialog.create( title = context.getString(state.dialog.titleRes, state.dialog.currencySymbol), message = context.getString( diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 80b48b4193..7aa2d7a120 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -4,10 +4,6 @@ import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer import com.tangem.tap.features.home.redux.HomeReducer -import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteReducer -import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsReducer -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsReducer -import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletReducer import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer @@ -20,10 +16,6 @@ fun appReducer(action: Action, state: AppState?): AppState { return AppState( globalState = globalReducer(action, state), homeState = HomeReducer.reduce(action, state), - onboardingNoteState = OnboardingNoteReducer.reduce(action, state), - onboardingWalletState = OnboardingWalletReducer.reduce(action, state), - onboardingOtherCardsState = OnboardingOtherCardsReducer.reduce(action, state), - twinCardsState = TwinCardsReducer.reduce(action, state), detailsState = DetailsReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), welcomeState = WelcomeReducer.reduce(action, state), diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 04d6fa4896..99946cc324 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -9,15 +9,7 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectMiddlewa import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.features.home.redux.HomeMiddleware import com.tangem.tap.features.home.redux.HomeState -import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteMiddleware -import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState -import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsMiddleware -import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsMiddleware -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware -import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletMiddleware -import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware @@ -31,10 +23,6 @@ import org.rekotlin.StateType data class AppState( val globalState: GlobalState = GlobalState(), val homeState: HomeState = HomeState(), - val onboardingNoteState: OnboardingNoteState = OnboardingNoteState(), - val onboardingWalletState: OnboardingWalletState = OnboardingWalletState(), - val onboardingOtherCardsState: OnboardingOtherCardsState = OnboardingOtherCardsState(), - val twinCardsState: TwinCardsState = TwinCardsState(), val detailsState: DetailsState = DetailsState(), val walletConnectState: WalletConnectState = WalletConnectState(), val welcomeState: WelcomeState = WelcomeState(), @@ -49,10 +37,6 @@ data class AppState( notificationsMiddleware, GlobalMiddleware.handler, HomeMiddleware.handler, - OnboardingNoteMiddleware.handler, - OnboardingWalletMiddleware.handler, - OnboardingOtherCardsMiddleware.handler, - TwinCardsMiddleware.handler, DetailsMiddleware().detailsMiddleware, WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 1c984ff096..79947dc0ac 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -8,7 +8,6 @@ import com.tangem.domain.redux.StateDialog import com.tangem.tap.common.redux.DebugErrorAction import com.tangem.tap.common.redux.ErrorAction import com.tangem.tap.domain.TapError -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource import org.rekotlin.Action sealed class GlobalAction : Action { @@ -21,29 +20,6 @@ sealed class GlobalAction : Action { data class ShowDialog(val stateDialog: StateDialog) : GlobalAction() object HideDialog : GlobalAction() - sealed class Onboarding : GlobalAction() { - /** - * Initiate an onboarding process. - * For resuming unfinished backup of standard Wallet see - * BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup - */ - data class Start( - val scanResponse: ScanResponse, - val source: BackupStartedSource, - val canSkipBackup: Boolean = true, - ) : Onboarding() - - /** - * Initiate resuming of unfinished backup for standard Wallet. - * See more BackupAction.CheckForUnfinishedBackup - */ - data class StartForUnfinishedBackup(val addedBackupCardsCount: Int) : Onboarding() - - object Stop : Onboarding() - - data class ShouldResetCardOnCreate(val shouldReset: Boolean) : Onboarding() - } - object ScanFailsCounter { data class ChooseBehavior( val result: CompletionResult, diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index c8372a3d0d..51da092058 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -1,7 +1,6 @@ package com.tangem.tap.common.redux.global import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.onboarding.OnboardingManager import com.tangem.utils.extensions.replaceBy import org.rekotlin.Action @@ -12,21 +11,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState { val globalState = state.globalState return when (action) { - is GlobalAction.Onboarding.Start -> { - val onboardingManager = OnboardingManager(action.scanResponse) - globalState.copy(onboardingState = OnboardingState(true, onboardingManager)) - } - is GlobalAction.Onboarding.StartForUnfinishedBackup -> { - globalState.copy(onboardingState = OnboardingState(true, null)) - } - is GlobalAction.Onboarding.Stop -> { - globalState.copy(onboardingState = OnboardingState(false)) - } - is GlobalAction.Onboarding.ShouldResetCardOnCreate -> { - globalState.copy( - onboardingState = globalState.onboardingState.copy(shouldResetOnCreate = action.shouldReset), - ) - } is GlobalAction.ScanFailsCounter.Increment -> { globalState.copy(scanCardFailsCounter = globalState.scanCardFailsCounter + 1) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt index 22932d20e9..1eea5ec1a0 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt @@ -5,10 +5,8 @@ import com.tangem.domain.redux.LegacyAction import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.details.redux.AppSettingsState import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope import com.tangem.tap.store @@ -26,15 +24,6 @@ internal object LegacyMiddleware { { next -> { action -> when (action) { - is LegacyAction.StartOnboardingProcess -> { - store.dispatch( - GlobalAction.Onboarding.Start( - scanResponse = action.scanResponse, - source = BackupStartedSource.CreateBackup, - canSkipBackup = action.canSkipBackup, - ), - ) - } is LegacyAction.PrepareDetailsScreen -> { val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) val walletsRepository = store.inject(DaggerGraphState::walletsRepository) diff --git a/app/src/main/java/com/tangem/tap/di/domain/MnemonicModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MnemonicModule.kt deleted file mode 100644 index 8f62a7e255..0000000000 --- a/app/src/main/java/com/tangem/tap/di/domain/MnemonicModule.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.di.domain - -import android.content.Context -import com.tangem.crypto.bip39.Wordlist -import com.tangem.feature.onboarding.data.DefaultMnemonicRepository -import com.tangem.feature.onboarding.data.MnemonicRepository -import com.tangem.sdk.extensions.getWordlist -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class MnemonicModule { - - @Provides - @Singleton - fun provideMnemonicRepository(@ApplicationContext context: Context): MnemonicRepository { - return DefaultMnemonicRepository(Wordlist.getWordlist(context)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt index 8225f9014c..63058e7738 100644 --- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt @@ -7,7 +7,6 @@ import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.store import com.tangem.tap.tangemSdkManager import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider @@ -42,7 +41,6 @@ class TapWalletManager( withMainContext { // Order is important - store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) store.dispatch(GlobalAction.SaveScanResponse(scanResponse)) } } diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt index 6ea8247faf..29665865af 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/LegacyScanProcessor.kt @@ -21,7 +21,6 @@ import com.tangem.core.ui.message.dialog.Dialogs import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull -import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.feedback.models.FeedbackEmailType @@ -34,9 +33,6 @@ import com.tangem.tap.common.redux.AppDialog import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.disclaimer.createDisclaimer import com.tangem.tap.features.onboarding.OnboardingHelper -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope @@ -208,8 +204,6 @@ internal class LegacyScanProcessor @Inject constructor( crossinline onCancel: suspend () -> Unit, crossinline onSuccess: suspend (ScanResponse) -> Unit, ) { - store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(scanResponse)) - checkCardWasUsedInApp( scanResponse = scanResponse, onCancel = { @@ -222,38 +216,26 @@ internal class LegacyScanProcessor @Inject constructor( if (OnboardingHelper.isOnboardingCase(scanResponse)) { Analytics.addContext(scanResponse) onWalletNotCreated() - // must check skip backup using card canSkipBackup - store.dispatchOnMain( - GlobalAction.Onboarding.Start( + navigateTo( + AppRoute.Onboarding( scanResponse = scanResponse, - source = BackupStartedSource.Onboarding, - canSkipBackup = scanResponse.card.canSkipBackup, + mode = AppRoute.Onboarding.Mode.Onboarding, ), - ) - val appScreen = OnboardingHelper.whereToNavigate(scanResponse) - navigateTo(appScreen) { onProgressStateChange(it) } + ) { onProgressStateChange(it) } } else { Analytics.setContext(scanResponse) - val isTwinRefactoringEnabled = - store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled - val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase).invokeSync() if (scanResponse.twinsIsTwinned() && !wasTwinsOnboardingShown) { onWalletNotCreated() - if (isTwinRefactoringEnabled) { - navigateTo( - AppRoute.Onboarding( - scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, - ), - ) { onProgressStateChange(it) } - } else { - store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(scanResponse))) - navigateTo(AppRoute.OnboardingTwins) { onProgressStateChange(it) } - } + navigateTo( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, + ), + ) { onProgressStateChange(it) } } else { delay(DELAY_SDK_DIALOG_CLOSE) onSuccess(scanResponse) diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt index b9d6aff5fd..0912058b65 100644 --- a/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt +++ b/app/src/main/java/com/tangem/tap/domain/scanCard/chains/CheckForOnboardingChain.kt @@ -5,21 +5,15 @@ import arrow.core.right import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.Analytics import com.tangem.domain.card.ScanCardException -import com.tangem.domain.common.TapWorkarounds.canSkipBackup import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.core.chain.Chain import com.tangem.domain.core.chain.ResultChain import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.addContext -import com.tangem.tap.common.extensions.dispatchOnMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.extensions.setContext import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.onboarding.OnboardingHelper -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE import kotlinx.coroutines.delay @@ -40,46 +34,30 @@ class CheckForOnboardingChain( ) : ResultChain() { override suspend fun launch(previousChainResult: ScanResponse): ScanChainResult { - store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult)) - return when { OnboardingHelper.isOnboardingCase(previousChainResult) -> { Analytics.addContext(previousChainResult) - // must check skip backup using card canSkipBackup - store.dispatchOnMain( - GlobalAction.Onboarding.Start( + ScanChainException.OnboardingNeeded( + AppRoute.Onboarding( scanResponse = previousChainResult, - source = BackupStartedSource.Onboarding, - canSkipBackup = previousChainResult.card.canSkipBackup, + mode = AppRoute.Onboarding.Mode.Onboarding, ), - ) - val route = OnboardingHelper.whereToNavigate(previousChainResult) - ScanChainException.OnboardingNeeded(route).left() + ).left() } else -> { Analytics.setContext(previousChainResult) - val isTwinRefactoringEnabled = - store.inject(DaggerGraphState::onboardingV2FeatureToggles).isTwinRefactoringEnabled - val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) .invokeSync() // If twins was twinned previously but twins welcome not shown if (previousChainResult.twinsIsTwinned() && !wasTwinsOnboardingShown) { - if (isTwinRefactoringEnabled) { - ScanChainException.OnboardingNeeded( - AppRoute.Onboarding( - scanResponse = previousChainResult, - mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, - ), - ).left() - } else { - store.dispatchOnMain( - TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)), - ) - ScanChainException.OnboardingNeeded(AppRoute.OnboardingTwins).left() - } + ScanChainException.OnboardingNeeded( + AppRoute.Onboarding( + scanResponse = previousChainResult, + mode = AppRoute.Onboarding.Mode.WelcomeOnlyTwin, + ), + ).left() } else { delay(DELAY_SDK_DIALOG_CLOSE) previousChainResult.right() diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt index 4895cd65b9..577ac2f3a8 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt @@ -7,7 +7,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.store import com.tangem.wallet.R @@ -16,13 +15,8 @@ import org.rekotlin.Action object DemoHelper { val config = DemoConfig() - private val demoMiddlewares = listOf( - DemoOnboardingNoteMiddleware(), - ) - private val disabledActionFeatures = listOf( WalletConnectAction.StartWalletConnect::class.java, - BackupAction.StartBackup::class.java, ) fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId) @@ -35,10 +29,6 @@ object DemoHelper { val scanResponse = getScanResponse(appState) ?: return false if (!scanResponse.isDemoCard()) return false - demoMiddlewares.forEach { - if (it.tryHandle(config, scanResponse, action)) return true - } - disabledActionFeatures.firstOrNull { it == action::class.java }?.let { val uiMessageSender = store.inject(DaggerGraphState::uiMessageSender) uiMessageSender.send( diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt deleted file mode 100644 index 4b3ac9357d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.tangem.tap.features.demo - -import com.tangem.common.extensions.guard -import com.tangem.domain.common.extensions.makePrimaryWalletManager -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.domain.demo.DemoConfig -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.entities.ProgressState -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.domain.model.Currency -import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -internal class DemoOnboardingNoteMiddleware : DemoMiddleware { - - override fun tryHandle(config: DemoConfig, scanResponse: ScanResponse, action: Action): Boolean { - val noteState = store.state.onboardingNoteState - - return when (action) { - is OnboardingNoteAction.Balance.Update -> { - val walletManager = if (noteState.walletManager != null) { - noteState.walletManager - } else { - val wmFactory = runBlocking { - store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync() - } - val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard { - return false - } - store.dispatch(OnboardingNoteAction.SetWalletManager(walletManager)) - walletManager - } - - val balanceAmount = config.getBalance(walletManager.wallet.blockchain) - val loadedBalance = noteState.walletBalance.copy( - value = balanceAmount.value!!, - currency = Currency.Blockchain(walletManager.wallet.blockchain, null), - state = ProgressState.Done, - error = null, - criticalError = null, - ) - - walletManager.wallet.setAmount(balanceAmount) - - scope.launch { - withMainContext { - store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance)) - store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError)) - store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error)) - } - } - return true - } - else -> false - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 894ab1be67..1390b8d0bd 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -20,7 +20,6 @@ import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.builder.UserWalletIdBuilder import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings @@ -33,8 +32,6 @@ import com.tangem.tap.features.details.ui.cardsettings.CardSettingsScreenState import com.tangem.tap.features.details.ui.cardsettings.api.CardSettingsComponent import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor import com.tangem.tap.features.details.ui.common.utils.* -import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.R @@ -56,7 +53,6 @@ internal class CardSettingsModel @Inject constructor( private val getUserWalletUseCase: GetUserWalletUseCase, private val cardSdkConfigRepository: CardSdkConfigRepository, private val settingsRepository: SettingsRepository, - private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -200,23 +196,13 @@ internal class CardSettingsModel @Inject constructor( } if (scanResponse.cardTypesResolver.isTangemTwins()) { - if (onboardingV2FeatureToggles.isTwinRefactoringEnabled) { - store.dispatchNavigationAction { - push( - AppRoute.Onboarding( - scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.RecreateWalletTwin, - ), - ) - } - } else { - // needs to prepare twin state if it's twin cards (depends on onboarding refactoring) - store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse)) - store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet(scanResponse))) - - cardSettingsInteractor.clear() - - store.dispatchNavigationAction { push(AppRoute.OnboardingTwins) } + store.dispatchNavigationAction { + push( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.RecreateWalletTwin, + ), + ) } } else { val card = scanResponse.card diff --git a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt index e6a4d37f48..212e39e905 100644 --- a/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/main/MainViewModel.kt @@ -21,6 +21,7 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.balancehiding.ListenToFlipsUseCase import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase +import com.tangem.domain.onboarding.repository.OnboardingRepository import com.tangem.domain.onramp.FetchHotCryptoUseCase import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds @@ -31,6 +32,9 @@ import com.tangem.domain.staking.FetchStakingTokensUseCase import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.tap.common.extensions.setContext +import com.tangem.tap.common.redux.global.GlobalAction +import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog +import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.* @@ -58,6 +62,7 @@ internal class MainViewModel @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val imagePreloader: ImagePreloader, private val fetchHotCryptoUseCase: FetchHotCryptoUseCase, + private val onboardingRepository: OnboardingRepository, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, ) : ViewModel() { @@ -94,6 +99,13 @@ internal class MainViewModel @Inject constructor( preloadImages() } + fun checkForUnfinishedBackup() { + viewModelScope.launch(dispatchers.main) { + val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch + store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse))) + } + } + /** Loading the resources needed to run the application */ private fun loadApplicationResources() { viewModelScope.launch(dispatchers.main) { diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt index 97f17bd9b5..e2de5c257b 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingDialog.kt @@ -1,13 +1,10 @@ package com.tangem.tap.features.onboarding -import com.tangem.common.extensions.VoidCallback import com.tangem.domain.redux.StateDialog /** [REDACTED_AUTHOR] */ sealed class OnboardingDialog : StateDialog { - object TwinningProcessNotCompleted : OnboardingDialog() - data class InterruptOnboarding(val onOk: VoidCallback) : OnboardingDialog() data class WalletActivationError(val onConfirm: () -> Unit) : OnboardingDialog() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt index 7eda3eb6c5..0f3997d317 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt @@ -1,46 +1,23 @@ package com.tangem.tap.features.onboarding -import com.tangem.blockchain.common.WalletManager -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess -import com.tangem.common.extensions.guard -import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.Basic -import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.common.util.twinsIsTwinned import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.ProductType import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.visa.model.VisaCardActivationStatus -import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter -import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.extensions.inject import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStartedSource -import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction -import com.tangem.tap.features.saveWallet.redux.SaveWalletAction -import com.tangem.tap.mainScope import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import timber.log.Timber /** [REDACTED_AUTHOR] */ object OnboardingHelper { suspend fun isOnboardingCase(response: ScanResponse): Boolean { - val onboardingManager = - store.state.globalState.onboardingState.onboardingManager ?: OnboardingManager(response) + val cardRepository = store.inject(DaggerGraphState::cardRepository) val cardId = response.card.cardId + return when { response.cardTypesResolver.isVisaWallet() -> { if (response.visaCardActivationStatus == null) error("Visa card activation status is null") @@ -52,227 +29,20 @@ object OnboardingHelper { if (!response.twinsIsTwinned()) { true } else { - onboardingManager.isActivationInProgress(cardId) + cardRepository.isActivationInProgress(cardId) } } response.cardTypesResolver.isWallet2() || response.cardTypesResolver.isShibaWallet() -> { val emptyWallets = response.card.wallets.isEmpty() - val activationInProgress = onboardingManager.isActivationInProgress(cardId) + val activationInProgress = cardRepository.isActivationInProgress(cardId) val isNoBackup = response.card.backupStatus == CardDTO.BackupStatus.NoBackup && !DemoHelper.isDemoCard(response) emptyWallets || activationInProgress || isNoBackup } - response.card.wallets.isNotEmpty() -> onboardingManager.isActivationInProgress(cardId) + response.card.wallets.isNotEmpty() -> cardRepository.isActivationInProgress(cardId) else -> true } } - - @Suppress("CyclomaticComplexMethod") - fun whereToNavigate(scanResponse: ScanResponse): AppRoute { - val newOnboardingSupportTypes = scanResponse.productType == ProductType.Wallet2 || - scanResponse.productType == ProductType.Ring || - scanResponse.productType == ProductType.Wallet || - scanResponse.productType == ProductType.Visa // AppRoute.OnboardingOther is also supported - val featureToggles = store.inject(DaggerGraphState::onboardingV2FeatureToggles) - if (featureToggles.isOnboardingV2Enabled && - newOnboardingSupportTypes - ) { - val backupState = store.state.onboardingWalletState.backupState - - return AppRoute.Onboarding( - scanResponse = scanResponse, - mode = when (backupState.startedSource) { - BackupStartedSource.Onboarding -> AppRoute.Onboarding.Mode.Onboarding - BackupStartedSource.CreateBackup -> AppRoute.Onboarding.Mode.AddBackupWallet1 - }, - ) - } - - return when (val type = scanResponse.productType) { - ProductType.Note -> if (featureToggles.isNoteRefactoringEnabled) { - AppRoute.Onboarding(scanResponse = scanResponse) - } else { - AppRoute.OnboardingNote - } - ProductType.Wallet, - ProductType.Wallet2, - ProductType.Ring, - -> if (scanResponse.card.settings.isBackupAllowed) { - AppRoute.OnboardingWallet() - } else { - AppRoute.OnboardingOther - } - ProductType.Twins -> if (featureToggles.isTwinRefactoringEnabled) { - AppRoute.Onboarding(scanResponse = scanResponse) - } else { - AppRoute.OnboardingTwins - } - ProductType.Start2Coin, - ProductType.Visa, - -> throw UnsupportedOperationException("Onboarding for ${type.name} cards is not supported") - } - } - - suspend fun saveWallet( - alreadyCreatedWallet: UserWallet?, - scanResponse: ScanResponse, - accessCode: String? = null, - backupCardsIds: List? = null, - hasBackupError: Boolean = false, - ) { - Analytics.setContext(scanResponse) - val settingsRepository = store.inject(DaggerGraphState::settingsRepository) - when { - // When should save user wallets, then save card without navigate to save wallet screen - store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { - store.dispatchWithMain( - SaveWalletAction.ProvideBackupInfo( - scanResponse = scanResponse, - accessCode = accessCode, - backupCardsIds = backupCardsIds?.toSet(), - ), - ) - - store.dispatchWithMain( - SaveWalletAction.SaveWalletAfterBackup( - hasBackupError = hasBackupError, - shouldNavigateToWallet = false, - ), - ) - } - // When should not save user wallets but device has biometry and save wallet screen has not been shown, - // then open save wallet screen - tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> { - proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet) - - delay(timeMillis = 1_200) - - store.dispatchWithMain( - SaveWalletAction.ProvideBackupInfo( - scanResponse = scanResponse, - accessCode = accessCode, - backupCardsIds = backupCardsIds?.toSet(), - ), - ) - } - // If device has no biometry and save wallet screen has been shown, then go through old scenario - else -> proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError, alreadyCreatedWallet) - } - } - - fun trySaveWalletAndNavigateToWalletScreen( - scanResponse: ScanResponse, - accessCode: String? = null, - backupCardsIds: List? = null, - hasBackupError: Boolean = false, - ) { - Analytics.setContext(scanResponse) - scope.launch { - val settingsRepository = store.inject(DaggerGraphState::settingsRepository) - - when { - // When should save user wallets, then save card without navigate to save wallet screen - store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> { - store.dispatchWithMain( - SaveWalletAction.ProvideBackupInfo( - scanResponse = scanResponse, - accessCode = accessCode, - backupCardsIds = backupCardsIds?.toSet(), - ), - ) - - store.dispatchWithMain( - SaveWalletAction.SaveWalletAfterBackup( - hasBackupError = hasBackupError, - shouldNavigateToWallet = true, - ), - ) - } - // When should not save user wallets but device has biometry and save wallet screen has not been shown, - // then open save wallet screen - tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> { - proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) - - delay(timeMillis = 1_200) - - store.dispatchOnMain( - SaveWalletAction.ProvideBackupInfo( - scanResponse = scanResponse, - accessCode = accessCode, - backupCardsIds = backupCardsIds?.toSet(), - ), - ) - store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } - delay(timeMillis = 1_800) - store.dispatchNavigationAction { push(AppRoute.SaveWallet) } - } - // If device has no biometry and save wallet screen has been shown, then go through old scenario - else -> { - proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError) - store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } - } - } - } - } - - fun onInterrupted() { - Analytics.removeContext() - } - - fun sendToppedUpEvent(scanResponse: ScanResponse) { - val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build() - val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver) - - if (userWalletId != null && currency != null) { - Analytics.send(Basic.ToppedUp(userWalletId.stringValue, currency)) - } - } - - fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse) { - val blockchain = walletManager.wallet.blockchain - val excludedBlockchains = store.inject(DaggerGraphState::excludedBlockchains) - - val cryptoCurrency = CryptoCurrencyFactory(excludedBlockchains).createCoin( - blockchain = blockchain, - extraDerivationPath = null, - scanResponse = scanResponse, - ) ?: return - - val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return - - val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain) - Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType)) - - store.dispatchOpenUrl(topUpUrl) - } - - private suspend fun proceedWithScanResponse( - scanResponse: ScanResponse, - backupCardsIds: List?, - hasBackupError: Boolean, - alreadyCreatedWallet: UserWallet? = null, - ) { - val userWalletBuilderFactory = store.inject(DaggerGraphState::userWalletBuilderFactory) - - val userWallet = alreadyCreatedWallet ?: userWalletBuilderFactory.create(scanResponse) - .hasBackupError(hasBackupError) - .backupCardsIds(backupCardsIds?.toSet()) - .build() - .guard { - Timber.e("User wallet not created") - return - } - - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - userWalletsListManager.save(userWallet, canOverride = true) - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - } - .doOnSuccess { - mainScope.launch { store.onUserWalletSelected(userWallet) } - store.dispatchWithMain(OnboardingWalletAction.WalletSaved(userWallet.walletId)) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt index e982cd5f2e..8ea87b04a4 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingManager.kt @@ -1,141 +1,6 @@ package com.tangem.tap.features.onboarding -import com.tangem.blockchain.common.AmountType -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.WalletManager -import com.tangem.common.extensions.isZero -import com.tangem.common.services.Result -import com.tangem.domain.card.repository.CardRepository import com.tangem.domain.models.scan.ScanResponse -import com.tangem.operations.attestation.OnlineCardVerifier -import com.tangem.operations.attestation.api.models.CardVerifyAndGetInfo -import com.tangem.tap.common.entities.ProgressState -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.extensions.safeUpdate -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl -import com.tangem.tap.domain.model.Currency -import com.tangem.tap.domain.model.hasPendingTransactions -import com.tangem.tap.features.demo.isDemoCard -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import com.tangem.utils.extensions.isPositive -import timber.log.Timber -import java.math.BigDecimal -/** -[REDACTED_AUTHOR] - */ @Deprecated("Remove when navigation refactoring will be implemented") -class OnboardingManager(var scanResponse: ScanResponse) { - - private val cardRepository: CardRepository by lazy { store.inject(DaggerGraphState::cardRepository) } - - private val onlineCardVerifier: OnlineCardVerifier by lazy { store.inject(DaggerGraphState::onlineCardVerifier) } - - private var cardInfo: Result? = null - - suspend fun loadArtworkUrl(): String { - val cardInfo = cardInfo - ?: onlineCardVerifier.getCardInfo(scanResponse.card.cardId, scanResponse.card.cardPublicKey) - - this.cardInfo = cardInfo - - return scanResponse.card.getOrLoadCardArtworkUrl(cardInfo, onlineCardVerifier) - } - - suspend fun updateBalance(walletManager: WalletManager): OnboardingWalletBalance { - val isDemoCard = scanResponse.isDemoCard() - val balance = when (val result = walletManager.safeUpdate(isDemoCard)) { - is Result.Success -> { - val wallet = walletManager.wallet - val valueOfAmount = wallet.amounts[AmountType.Coin]?.value - if (valueOfAmount == null) { - val customError = TapError.CustomError("Amount is NULL") - OnboardingWalletBalance.criticalError(customError) - } else { - val balance = if (valueOfAmount.isZero()) { - OnboardingWalletBalance.done(valueOfAmount, wallet.hasPendingTransactions()) - } else { - OnboardingWalletBalance.done(valueOfAmount, false) - } - balance - } - } - is Result.Failure -> { - val error = result.error as? TapError ?: TapError.UnknownError - when (error) { - is TapError.WalletManager.NoAccountError -> OnboardingWalletBalance.error(error) - // NoInternetConnection, WalletManager.InternalError - else -> { - Timber.e(error.localizedMessage) - OnboardingWalletBalance.criticalError(TapError.WalletManager.BlockchainIsUnreachableTryLater) - } - } - } - } - - return balance.copy( - currency = Currency.Blockchain( - blockchain = walletManager.wallet.blockchain, - derivationPath = walletManager.wallet.publicKey.derivationPath?.rawPath, - ), - ) - } - - suspend fun startActivation(cardId: String) { - cardRepository.startCardActivation(cardId) - } - - suspend fun finishActivation(cardId: String) { - cardRepository.finishCardActivation(cardId) - } - - suspend fun finishActivation(cardIds: List) { - cardRepository.finishCardsActivation(cardIds) - } - - suspend fun isActivationStarted(cardId: String): Boolean = cardRepository.isActivationStarted(cardId) - - suspend fun isActivationFinished(cardId: String): Boolean = cardRepository.isActivationFinished(cardId) - - suspend fun isActivationInProgress(cardId: String): Boolean = cardRepository.isActivationInProgress(cardId) -} - -data class OnboardingWalletBalance( - val value: BigDecimal = BigDecimal.ZERO, - val currency: Currency = Currency.Blockchain(Blockchain.Unknown, null), - val hasIncomingTransaction: Boolean = false, - val state: ProgressState, - val error: TapError? = null, - val criticalError: TapError? = null, -) { - - val amountToCreateAccount: String? - get() = if (error is TapError.WalletManager.NoAccountError) error.customMessage else null - - fun balanceIsToppedUp(): Boolean = value.isPositive() || hasIncomingTransaction - - companion object { - fun error(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance( - state = ProgressState.Error, - error = error, - ) - - fun criticalError(error: TapError): OnboardingWalletBalance = OnboardingWalletBalance( - state = ProgressState.Error, - criticalError = error, - ) - - fun loading(value: BigDecimal = BigDecimal.ZERO): OnboardingWalletBalance = OnboardingWalletBalance( - value, - state = ProgressState.Loading, - ) - - fun done(value: BigDecimal, hasTransactions: Boolean): OnboardingWalletBalance = OnboardingWalletBalance( - value, - hasIncomingTransaction = hasTransactions, - state = ProgressState.Done, - ) - } -} \ No newline at end of file +class OnboardingManager(var scanResponse: ScanResponse) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt deleted file mode 100644 index c619d4bd17..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingMenuProvider.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.tap.features.onboarding - -import android.view.Menu -import android.view.MenuInflater -import android.view.MenuItem -import androidx.core.view.MenuProvider -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.utils.Provider -import com.tangem.wallet.R -import kotlinx.coroutines.launch - -/** -[REDACTED_AUTHOR] - */ -class OnboardingMenuProvider( - private val scanResponseProvider: Provider, -) : MenuProvider { - - override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { - menuInflater.inflate(R.menu.menu_onboarding, menu) - } - - override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) { - R.id.menu_item_chat_support -> { - Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) - - val cardInfo = store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponseProvider()).getOrNull() - ?: error("CardInfo must be not null") - - scope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo)) - } - - true - } - else -> false - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt deleted file mode 100644 index 0922d3f023..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/BaseOnboardingFragment.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.tangem.tap.features.onboarding.products - -import android.os.Bundle -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.MenuProvider -import by.kirich1409.viewbindingdelegate.viewBinding -import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.transitions.HomeToOnboardingTransition -import com.tangem.tap.features.BaseStoreFragment -import com.tangem.tap.features.onboarding.OnboardingMenuProvider -import com.tangem.tap.store -import com.tangem.utils.Provider -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentOnboardingMainBinding -import com.tangem.wallet.databinding.ViewOnboardingProgressBinding -import org.rekotlin.StoreSubscriber - -/** -[REDACTED_AUTHOR] - */ -abstract class BaseOnboardingFragment : BaseStoreFragment(R.layout.fragment_onboarding_main), StoreSubscriber { - - protected val binding: FragmentOnboardingMainBinding by viewBinding(FragmentOnboardingMainBinding::bind) - protected val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind) - - override fun configureTransitions() { - enterTransition = HomeToOnboardingTransition() - exitTransition = HomeToOnboardingTransition() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) - } - - override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( - scanResponseProvider = Provider { - store.state.globalState.onboardingState.onboardingManager?.scanResponse - ?: store.state.detailsState.scanResponse - ?: error("ScanResponse must be not null") - }, - ) - - protected fun showConfetti(show: Boolean) = with(binding.vConfetti) { - lavConfetti.show(show) - - if (show) { - lavConfetti.playAnimation() - } else { - lavConfetti.cancelAnimation() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt deleted file mode 100644 index 8346f444cd..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/OnboardingNoteFragment.kt +++ /dev/null @@ -1,228 +0,0 @@ -package com.tangem.tap.features.onboarding.products.note - -import android.os.Bundle -import android.view.View -import android.view.animation.OvershootInterpolator -import androidx.annotation.LayoutRes -import androidx.constraintlayout.widget.ConstraintSet -import androidx.core.view.isVisible -import androidx.transition.TransitionManager -import coil.load -import com.tangem.blockchain.common.Blockchain -import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.ShareElement -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.getDrawableCompat -import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget -import com.tangem.tap.common.transitions.InternalNoteLayoutTransition -import com.tangem.tap.features.addBackPressHandler -import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment -import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteAction -import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState -import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStep -import com.tangem.tap.store -import com.tangem.utils.extensions.stripZeroPlainString -import com.tangem.wallet.R - -@Deprecated("Use OnboardingNoteComponent") -class OnboardingNoteFragment : BaseOnboardingFragment() { - - private var previousStep: OnboardingNoteStep = OnboardingNoteStep.None - private val mainBinding by lazy { binding.vMain } - - private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - addBackPressHandler(this) - - mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard - - binding.toolbar.setTitle(R.string.onboarding_title) - btnRefreshBalanceWidget = RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer) - - store.dispatch(OnboardingNoteAction.Init) - store.dispatch(OnboardingNoteAction.LoadCardArtwork) - store.dispatch(OnboardingNoteAction.DetermineStepOfScreen) - } - - override fun subscribeToStore() { - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.onboardingNoteState == newState.onboardingNoteState - }.select { it.onboardingNoteState } - } - storeSubscribersList.add(this) - } - - override fun newState(state: OnboardingNoteState) { - if (activity == null || view == null) return - - mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) { - placeholder(R.drawable.card_placeholder_black) - error(R.drawable.card_placeholder_black) - fallback(R.drawable.card_placeholder_black) - } - - pbBinding.pbState.max = state.steps.size - 1 - pbBinding.pbState.progress = state.progress - - when (state.currentStep) { - OnboardingNoteStep.None -> setupNoneState() - OnboardingNoteStep.CreateWallet -> setupCreateWalletState(state) - OnboardingNoteStep.TopUpWallet -> setupTopUpWalletState(state) - OnboardingNoteStep.Done -> setupDoneState(state) - } - setBalance(state) - showConfetti(state.showConfetti) - previousStep = state.currentStep - } - - private fun setBalance(state: OnboardingNoteState) { - if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return - - with(mainBinding.onboardingTopContainer.onboardingTvBalance) { - if (state.balanceCriticalError == null) { - val balanceValue = state.walletBalance.value.stripZeroPlainString() - tvBalanceValue.text = balanceValue - tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol - } else { - tvBalanceValue.text = "–" - tvBalanceCurrency.text = "" - } - } - } - - private fun setupNoneState() = with(mainBinding.onboardingActionContainer) { - btnMainAction.isVisible = false - btnAlternativeAction.isVisible = false - btnRefreshBalanceWidget.show(false) - tvHeader.isVisible = false - tvBody.isVisible = false - - btnMainAction.text = "" - btnMainAction.icon = null - btnAlternativeAction.text = "" - btnAlternativeAction.icon = null - tvHeader.text = "" - tvBody.text = "" - } - - private fun setupCreateWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { - btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) - btnMainAction.setIconResource(R.drawable.ic_tangem_24) - btnMainAction.setOnClickListener { - Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) - store.dispatch(OnboardingNoteAction.CreateWallet) - } - btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean) - btnAlternativeAction.setOnClickListener { } - - btnRefreshBalanceWidget.mainView.setOnClickListener(null) - - tvHeader.setText(R.string.onboarding_create_wallet_header) - tvBody.setText(R.string.onboarding_create_wallet_body) - - mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_circle), - ) - updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet) - - btnAlternativeAction.isVisible = false // temporary - } - - private fun setupTopUpWalletState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { - if (availableForBuy(state.scanResponse, state.walletBalance)) { - btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) - btnMainAction.icon = null - btnMainAction.setOnClickListener { - store.dispatch(OnboardingNoteAction.TopUp) - } - - btnAlternativeAction.isVisible = true - btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) - btnAlternativeAction.setOnClickListener { - store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog) - } - } else { - btnMainAction.setText(R.string.onboarding_button_receive_crypto) - btnMainAction.icon = null - btnMainAction.setOnClickListener { - store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog) - } - - btnAlternativeAction.isVisible = false - } - - tvHeader.setText(R.string.onboarding_topup_title) - if (state.balanceNonCriticalError == null) { - tvBody.setText(R.string.onboarding_top_up_body) - } else { - state.walletBalance.amountToCreateAccount?.let { amount -> - val tvBodyMessage = getString( - R.string.onboarding_top_up_body_no_account_error, - amount, - state.walletBalance.currency.currencySymbol, - ) - tvBody.text = tvBodyMessage - } - } - - btnRefreshBalanceWidget.changeState(state.walletBalance.state) - if (btnRefreshBalanceWidget.isShowing != true) { - btnRefreshBalanceWidget.mainView.setOnClickListener { - store.dispatch(OnboardingNoteAction.Balance.Update) - } - } - - val drawable = requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8) - mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable(drawable) - updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet) - } - - private fun setupDoneState(state: OnboardingNoteState) = with(mainBinding.onboardingActionContainer) { - btnMainAction.setText(R.string.common_continue) - btnMainAction.icon = null - btnMainAction.setOnClickListener { - showConfetti(false) - store.dispatch(OnboardingNoteAction.Done) - } - - btnAlternativeAction.isVisible = false - btnAlternativeAction.text = "" - btnAlternativeAction.setOnClickListener { } - btnRefreshBalanceWidget.mainView.setOnClickListener(null) - - tvHeader.setText(R.string.onboarding_done_header) - tvBody.setText(R.string.onboarding_done_body) - - mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), - ) - updateConstraints(state.currentStep, R.layout.lp_onboarding_done_activation) - } - - private fun updateConstraints(currentStep: OnboardingNoteStep, @LayoutRes layoutId: Int) { - if (this.previousStep == currentStep) return - - with(mainBinding.onboardingTopContainer) { - val constraintSet = ConstraintSet() - constraintSet.clone(requireContext(), layoutId) - constraintSet.applyTo(onboardingWalletContainer) - val transition = InternalNoteLayoutTransition() - transition.interpolator = OvershootInterpolator() - TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition) - } - } - - private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean { - scanResponse ?: return false - return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency) - } - - override fun handleOnBackPressed() { - store.dispatch(OnboardingNoteAction.OnBackPressed) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteAction.kt deleted file mode 100644 index ebdf31becb..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteAction.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.tap.features.onboarding.products.note.redux - -import com.tangem.blockchain.common.WalletManager -import com.tangem.tap.domain.TapError -import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -sealed class OnboardingNoteAction : Action { - // from user, ui - object Init : OnboardingNoteAction() - object LoadCardArtwork : OnboardingNoteAction() - object CreateWallet : OnboardingNoteAction() - object TopUp : OnboardingNoteAction() - object ShowAddressInfoDialog : OnboardingNoteAction() - object OnBackPressed : OnboardingNoteAction() - - // from redux - class SetArtworkUrl(val artworkUrl: String) : OnboardingNoteAction() - data class SetWalletManager(val walletManager: WalletManager) : OnboardingNoteAction() - object DetermineStepOfScreen : OnboardingNoteAction() - object Done : OnboardingNoteAction() - - data class SetStepOfScreen(val step: OnboardingNoteStep) : OnboardingNoteAction() - - sealed class Balance { - object Update : OnboardingNoteAction() - data class Set(val balance: OnboardingWalletBalance) : OnboardingNoteAction() - data class SetCriticalError(val error: TapError?) : OnboardingNoteAction() - data class SetNonCriticalError(val error: TapError?) : OnboardingNoteAction() - } - - sealed class Confetti { - object Show : OnboardingNoteAction() - object Hide : OnboardingNoteAction() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt deleted file mode 100644 index 99c704189c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteMiddleware.kt +++ /dev/null @@ -1,197 +0,0 @@ -package com.tangem.tap.features.onboarding.products.note.redux - -import com.tangem.common.CompletionResult -import com.tangem.common.extensions.guard -import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics -import com.tangem.domain.common.extensions.makePrimaryWalletManager -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.entities.ProgressState -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.postUi -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.model.Currency -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.features.onboarding.OnboardingHelper -import com.tangem.tap.mainScope -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.rekotlin.Action -import org.rekotlin.DispatchFunction -import org.rekotlin.Middleware - -object OnboardingNoteMiddleware { - val handler = onboardingNoteMiddleware -} - -private val onboardingNoteMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - handleNoteAction(state, action, dispatch) - next(action) - } - } -} - -@Suppress("LongMethod", "ComplexMethod", "MagicNumber") -private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch: DispatchFunction) { - if (action !is OnboardingNoteAction) return - if (DemoHelper.tryHandle(appState, action)) return - - val globalState = store.state.globalState - val onboardingManager = globalState.onboardingState.onboardingManager ?: return - - val scanResponse = onboardingManager.scanResponse - val card = onboardingManager.scanResponse.card - val noteState = store.state.onboardingNoteState - - when (action) { - is OnboardingNoteAction.Init -> { - scope.launch { - if (!onboardingManager.isActivationStarted(card.cardId)) { - Analytics.send(Onboarding.Started()) - } - } - } - is OnboardingNoteAction.LoadCardArtwork -> { - scope.launch { - val artworkUrl = onboardingManager.loadArtworkUrl() - withMainContext { store.dispatch(OnboardingNoteAction.SetArtworkUrl(artworkUrl)) } - } - } - is OnboardingNoteAction.DetermineStepOfScreen -> { - val step = when { - card.wallets.isEmpty() -> OnboardingNoteStep.CreateWallet - noteState.walletBalance.balanceIsToppedUp() -> OnboardingNoteStep.Done - else -> OnboardingNoteStep.TopUpWallet - } - store.dispatch(OnboardingNoteAction.SetStepOfScreen(step)) - } - is OnboardingNoteAction.SetStepOfScreen -> { - when (action.step) { - OnboardingNoteStep.CreateWallet -> { - Analytics.send(Onboarding.CreateWallet.ScreenOpened()) - } - OnboardingNoteStep.TopUpWallet -> { - Analytics.send(Onboarding.Topup.ScreenOpened()) - store.dispatch(OnboardingNoteAction.Balance.Update) - } - OnboardingNoteStep.Done -> { - Analytics.send(Onboarding.Finished()) - mainScope.launch { - onboardingManager.finishActivation(card.cardId) - postUi(DELAY_SDK_DIALOG_CLOSE) { store.dispatch(OnboardingNoteAction.Confetti.Show) } - } - } - else -> Unit - } - } - is OnboardingNoteAction.CreateWallet -> { - scope.launch { - val result = tangemSdkManager.createProductWallet(scanResponse) - withMainContext { - when (result) { - is CompletionResult.Success -> { - Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) - val updatedResponse = scanResponse.copy(card = result.data.card) - onboardingManager.scanResponse = updatedResponse - onboardingManager.startActivation(updatedResponse.card.cardId) - store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.TopUpWallet)) - } - is CompletionResult.Failure -> Unit - } - } - } - } - is OnboardingNoteAction.Balance.Update -> { - val walletManager = if (noteState.walletManager != null) { - noteState.walletManager - } else { - val wmFactory = runBlocking { - store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync() - } - val walletManager = wmFactory?.makePrimaryWalletManager(scanResponse).guard { - val message = "Loading cancelled. Cause: wallet manager didn't created" - val customError = TapError.CustomError(message) - store.dispatchErrorNotification(customError) - return - } - dispatch(OnboardingNoteAction.SetWalletManager(walletManager)) - walletManager - } - - val isLoadedBefore = noteState.walletBalance.state != ProgressState.Loading - val balanceIsLoading = noteState.walletBalance.copy( - currency = Currency.Blockchain( - walletManager.wallet.blockchain, - walletManager.wallet.publicKey.derivationPath?.rawPath, - ), - state = ProgressState.Loading, - error = null, - criticalError = null, - ) - store.dispatch(OnboardingNoteAction.Balance.Set(balanceIsLoading)) - - scope.launch { - val loadedBalance = onboardingManager.updateBalance(walletManager) - delay(if (isLoadedBefore) 0 else 300) - loadedBalance.criticalError?.let { store.dispatchErrorNotification(it) } - withMainContext { - store.dispatch(OnboardingNoteAction.Balance.Set(loadedBalance)) - store.dispatch(OnboardingNoteAction.Balance.SetCriticalError(loadedBalance.criticalError)) - store.dispatch(OnboardingNoteAction.Balance.SetNonCriticalError(loadedBalance.error)) - } - } - } - is OnboardingNoteAction.Balance.Set -> { - if (action.balance.balanceIsToppedUp()) { - OnboardingHelper.sendToppedUpEvent(scanResponse) - store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done)) - } - } - is OnboardingNoteAction.ShowAddressInfoDialog -> { - val addressData = noteState.walletManager?.getAddressData() ?: return - - Analytics.send(Onboarding.Topup.ButtonShowWalletAddress()) - val appDialog = AppDialog.AddressInfoDialog(noteState.walletBalance.currency, addressData) - store.dispatchDialogShow(appDialog) - } - is OnboardingNoteAction.TopUp -> { - val walletManager = noteState.walletManager.guard { - store.dispatchDebugErrorNotification("NPE: WalletManager") - return - } - - OnboardingHelper.handleTopUpAction( - walletManager = walletManager, - scanResponse = scanResponse, - ) - } - is OnboardingNoteAction.Done -> { - store.dispatch(GlobalAction.Onboarding.Stop) - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) - } - is OnboardingNoteAction.OnBackPressed -> { - store.dispatchDialogShow( - OnboardingDialog.InterruptOnboarding( - onOk = { - OnboardingHelper.onInterrupted() - store.dispatchNavigationAction(AppRouter::pop) - }, - ), - ) - } - else -> Unit - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt deleted file mode 100644 index 14f7989391..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteReducer.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.tap.features.onboarding.products.note.redux - -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import org.rekotlin.Action - -object OnboardingNoteReducer { - fun reduce(action: Action, state: AppState): OnboardingNoteState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, appState: AppState): OnboardingNoteState { - var state = appState.onboardingNoteState - - when (action) { - is GlobalAction.Onboarding.Start -> { - state = OnboardingNoteState(scanResponse = action.scanResponse) - } - is OnboardingNoteAction.SetArtworkUrl -> { - state = state.copy(cardArtworkUrl = action.artworkUrl) - } - is OnboardingNoteAction.SetWalletManager -> { - state = state.copy(walletManager = action.walletManager) - } - is OnboardingNoteAction.Balance.Set -> { - state = state.copy(walletBalance = action.balance) - } - is OnboardingNoteAction.Balance.SetCriticalError -> { - state = state.copy(balanceCriticalError = action.error) - } - is OnboardingNoteAction.Balance.SetNonCriticalError -> { - state = state.copy(balanceNonCriticalError = action.error) - } - is OnboardingNoteAction.SetStepOfScreen -> { - if (action.step != state.currentStep && state.steps.contains(action.step)) { - state = state.copy(currentStep = action.step) - } - } - is OnboardingNoteAction.Confetti.Show -> { - state = state.copy(showConfetti = true) - } - is OnboardingNoteAction.Confetti.Hide -> { - state = state.copy(showConfetti = false) - } - } - - return state -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt deleted file mode 100644 index 3d659e39da..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/note/redux/OnboardingNoteState.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.tap.features.onboarding.products.note.redux - -import com.tangem.blockchain.common.WalletManager -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.domain.TapError -import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import org.rekotlin.StateType - -/** -[REDACTED_AUTHOR] - */ -data class OnboardingNoteState( - val walletManager: WalletManager? = null, - // UI - val cardArtworkUrl: String? = null, - val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(), - val balanceNonCriticalError: TapError? = null, - val balanceCriticalError: TapError? = null, - val currentStep: OnboardingNoteStep = OnboardingNoteStep.None, - val steps: List = OnboardingNoteStep.values().toList(), - val showConfetti: Boolean = false, - val scanResponse: ScanResponse? = null, -) : StateType { - - val progress: Int - get() = steps.indexOf(currentStep) -} - -enum class OnboardingNoteStep { - None, CreateWallet, TopUpWallet, Done -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt deleted file mode 100644 index 2e6ac715e0..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/OnboardingOtherCardsFragment.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.tap.features.onboarding.products.otherCards - -import android.os.Bundle -import android.view.View -import android.view.animation.OvershootInterpolator -import androidx.annotation.LayoutRes -import androidx.constraintlayout.widget.ConstraintSet -import androidx.core.view.isVisible -import androidx.transition.TransitionManager -import coil.load -import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.ShareElement -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.getDrawableCompat -import com.tangem.tap.common.transitions.InternalNoteLayoutTransition -import com.tangem.tap.features.addBackPressHandler -import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment -import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsAction -import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsState -import com.tangem.tap.features.onboarding.products.otherCards.redux.OnboardingOtherCardsStep -import com.tangem.tap.store -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -class OnboardingOtherCardsFragment : BaseOnboardingFragment() { - - private val mainBinding by lazy { binding.vMain } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - addBackPressHandler(this) - - mainBinding.onboardingTopContainer.imvFrontCard.transitionName = ShareElement.imvFrontCard - - binding.toolbar.setTitle(R.string.onboarding_title) - store.dispatch(OnboardingOtherCardsAction.Init) - store.dispatch(OnboardingOtherCardsAction.LoadCardArtwork) - store.dispatch(OnboardingOtherCardsAction.DetermineStepOfScreen) - } - - override fun subscribeToStore() { - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.onboardingOtherCardsState == newState.onboardingOtherCardsState - }.select { it.onboardingOtherCardsState } - } - storeSubscribersList.add(this) - } - - override fun newState(state: OnboardingOtherCardsState) { - if (activity == null || view == null) return - if (state.currentStep == OnboardingOtherCardsStep.None) return - - mainBinding.onboardingTopContainer.imvFrontCard.load(state.cardArtworkUrl) { - placeholder(R.drawable.card_placeholder_black) - error(R.drawable.card_placeholder_black) - fallback(R.drawable.card_placeholder_black) - } - - pbBinding.pbState.max = state.steps.size - 1 - pbBinding.pbState.progress = state.progress - - when (state.currentStep) { - OnboardingOtherCardsStep.CreateWallet -> setupCreateWalletState() - OnboardingOtherCardsStep.Done -> setupDoneState() - else -> {} - } - showConfetti(state.showConfetti) - } - - private fun setupCreateWalletState() = with(mainBinding.onboardingActionContainer) { - btnMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) - btnMainAction.setIconResource(R.drawable.ic_tangem_24) - btnMainAction.setOnClickListener { - Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) - store.dispatch(OnboardingOtherCardsAction.CreateWallet) - } - btnAlternativeAction.setText(R.string.onboarding_button_what_does_it_mean) - btnAlternativeAction.setOnClickListener { } - - tvHeader.setText(R.string.onboarding_create_wallet_header) - tvBody.setText(R.string.onboarding_create_wallet_body) - - btnAlternativeAction.isVisible = false // temporary - - mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_circle), - ) - updateConstraints(R.layout.lp_onboarding_create_wallet) - } - - private fun setupDoneState() = with(mainBinding.onboardingActionContainer) { - btnMainAction.setText(R.string.common_continue) - btnMainAction.icon = null - btnMainAction.setOnClickListener { - showConfetti(false) - store.dispatch(OnboardingOtherCardsAction.Done) - } - - btnAlternativeAction.isVisible = false - btnAlternativeAction.text = "" - btnAlternativeAction.setOnClickListener { } - - tvHeader.setText(R.string.onboarding_done_header) - tvBody.setText(R.string.onboarding_done_body) - - mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), - ) - updateConstraints(R.layout.lp_onboarding_done) - } - - private fun updateConstraints(@LayoutRes layoutId: Int) = with(mainBinding.onboardingTopContainer) { - val constraintSet = ConstraintSet() - constraintSet.clone(requireContext(), layoutId) - constraintSet.applyTo(onboardingWalletContainer) - val transition = InternalNoteLayoutTransition() - transition.interpolator = OvershootInterpolator() - TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition) - } - - override fun handleOnBackPressed() { - store.dispatch(OnboardingOtherCardsAction.OnBackPressed) - super.handleOnBackPressed() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsAction.kt deleted file mode 100644 index 60c3c3927d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsAction.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.tap.features.onboarding.products.otherCards.redux - -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -sealed class OnboardingOtherCardsAction : Action { - // from user, ui - object Init : OnboardingOtherCardsAction() - object LoadCardArtwork : OnboardingOtherCardsAction() - object CreateWallet : OnboardingOtherCardsAction() - - // from redux - class SetArtworkUrl(val artworkUrl: String) : OnboardingOtherCardsAction() - object DetermineStepOfScreen : OnboardingOtherCardsAction() - object Done : OnboardingOtherCardsAction() - object OnBackPressed : OnboardingOtherCardsAction() - - data class SetStepOfScreen(val step: OnboardingOtherCardsStep) : OnboardingOtherCardsAction() - - sealed class Confetti { - object Show : OnboardingOtherCardsAction() - object Hide : OnboardingOtherCardsAction() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt deleted file mode 100644 index d5e8144485..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.tangem.tap.features.onboarding.products.otherCards.redux - -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemSdkError -import com.tangem.core.analytics.Analytics -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.dispatchDialogShow -import com.tangem.tap.common.postUi -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.features.onboarding.OnboardingHelper -import com.tangem.tap.mainScope -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware - -object OnboardingOtherCardsMiddleware { - val handler = onboardingOtherCardsMiddleware -} - -private val onboardingOtherCardsMiddleware: Middleware = { dispatch, _ -> - { next -> - { action -> - handleOtherCardsAction(action) - next(action) - } - } -} - -@Suppress("LongMethod", "ComplexMethod", "MagicNumber") -private fun handleOtherCardsAction(action: Action) { - if (action !is OnboardingOtherCardsAction) return - val globalState = store.state.globalState - val onboardingManager = globalState.onboardingState.onboardingManager ?: return - - val card = onboardingManager.scanResponse.card - - when (action) { - is OnboardingOtherCardsAction.Init -> { - scope.launch { - if (!onboardingManager.isActivationStarted(card.cardId)) { - Analytics.send(Onboarding.Started()) - } - } - } - is OnboardingOtherCardsAction.LoadCardArtwork -> { - scope.launch { - val artworkUrl = onboardingManager.loadArtworkUrl() - withMainContext { store.dispatch(OnboardingOtherCardsAction.SetArtworkUrl(artworkUrl)) } - } - } - is OnboardingOtherCardsAction.DetermineStepOfScreen -> { - val step = when { - card.wallets.isEmpty() -> OnboardingOtherCardsStep.CreateWallet - else -> OnboardingOtherCardsStep.Done - } - store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(step)) - } - is OnboardingOtherCardsAction.SetStepOfScreen -> { - when (action.step) { - OnboardingOtherCardsStep.CreateWallet -> { - Analytics.send(Onboarding.CreateWallet.ScreenOpened()) - } - OnboardingOtherCardsStep.Done -> { - Analytics.send(Onboarding.Finished()) - mainScope.launch { - onboardingManager.finishActivation(card.cardId) - postUi(200) { store.dispatch(OnboardingOtherCardsAction.Confetti.Show) } - } - } - else -> Unit - } - } - is OnboardingOtherCardsAction.CreateWallet -> { - scope.launch { - val result = tangemSdkManager.createProductWallet( - scanResponse = onboardingManager.scanResponse, - shouldReset = globalState.onboardingState.shouldResetOnCreate, - ) - withMainContext { - when (result) { - is CompletionResult.Success -> { - Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) - val updatedResponse = onboardingManager.scanResponse.copy( - card = result.data.card, - ) - val updatedCard = updatedResponse.card - onboardingManager.scanResponse = updatedResponse - onboardingManager.startActivation(updatedCard.cardId) - - delay(DELAY_SDK_DIALOG_CLOSE) - store.dispatch(OnboardingOtherCardsAction.SetStepOfScreen(OnboardingOtherCardsStep.Done)) - } - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.WalletAlreadyCreated) { - handleActivationError() - } - } - } - } - } - } - OnboardingOtherCardsAction.Done -> { - store.dispatch(GlobalAction.Onboarding.Stop) - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(onboardingManager.scanResponse) - } - OnboardingOtherCardsAction.OnBackPressed -> { - OnboardingHelper.onInterrupted() - } - else -> Unit - } -} - -private fun handleActivationError() { - store.dispatchDialogShow( - OnboardingDialog.WalletActivationError( - onConfirm = { - store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true)) - }, - ), - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsReducer.kt deleted file mode 100644 index 3dae886ef6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsReducer.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.tap.features.onboarding.products.otherCards.redux - -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import org.rekotlin.Action - -object OnboardingOtherCardsReducer { - fun reduce(action: Action, state: AppState): OnboardingOtherCardsState = internalReduce(action, state) -} - -private fun internalReduce(action: Action, appState: AppState): OnboardingOtherCardsState { - var state = appState.onboardingOtherCardsState - - when (action) { - is GlobalAction.Onboarding.Start -> { - state = OnboardingOtherCardsState() - } - is OnboardingOtherCardsAction.SetArtworkUrl -> { - state = state.copy(cardArtworkUrl = action.artworkUrl) - } - is OnboardingOtherCardsAction.SetStepOfScreen -> { - if (action.step != state.currentStep && state.steps.contains(action.step)) { - state = state.copy(currentStep = action.step) - } - } - is OnboardingOtherCardsAction.Confetti.Show -> { - state = state.copy(showConfetti = true) - } - is OnboardingOtherCardsAction.Confetti.Hide -> { - state = state.copy(showConfetti = false) - } - } - - return state -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsState.kt deleted file mode 100644 index f502350050..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsState.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.tap.features.onboarding.products.otherCards.redux - -import org.rekotlin.StateType - -/** -[REDACTED_AUTHOR] - */ -data class OnboardingOtherCardsState( - // UI - val cardArtworkUrl: String? = null, - val showConfetti: Boolean = false, - val currentStep: OnboardingOtherCardsStep = OnboardingOtherCardsStep.None, - val steps: List = OnboardingOtherCardsStep.values().toList(), -) : StateType { - - val progress: Int - get() = steps.indexOf(currentStep) -} - -enum class OnboardingOtherCardsStep { - None, CreateWallet, Done -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt deleted file mode 100644 index ca00a0c255..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.tap.features.onboarding.products.twins.redux - -import com.tangem.Message -import com.tangem.blockchain.common.WalletManager -import com.tangem.common.extensions.VoidCallback -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.twins.TwinCardsManager -import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import org.rekotlin.Action - -/** -[REDACTED_AUTHOR] - */ -sealed class TwinCardsAction : Action { - data class IfTwinsPrepareState(val scanResponse: ScanResponse) : TwinCardsAction() - - object Init : TwinCardsAction() - data class SetMode(val mode: CreateTwinWalletMode) : TwinCardsAction() - data class SetStepOfScreen(val step: TwinCardsStep) : TwinCardsAction() - data class SetUserUnderstand(val isUnderstand: Boolean) : TwinCardsAction() - - sealed class CardsManager { - data class Set(val manager: TwinCardsManager) : TwinCardsAction() - object Release : TwinCardsAction() - } - - sealed class Wallet : TwinCardsAction() { - data class LaunchFirstStep(val initialMessage: Message) : TwinCardsAction() - data class LaunchSecondStep( - val initialMessage: Message, - val preparingMessage: Message, - val creatingWalletMessage: Message, - ) : TwinCardsAction() - - data class LaunchThirdStep(val message: Message) : TwinCardsAction() - } - - // for the onboarding - data class SetPairCardId(val cardId: String) : TwinCardsAction() - object TopUp : TwinCardsAction() - object ShowAddressInfoDialog : TwinCardsAction() - data class SetWalletManager(val walletManager: WalletManager) : TwinCardsAction() - object Done : TwinCardsAction() - - data class OnBackPressed( - // this is necessary for the correct animation of the return of cards to the Home screen - val shouldResetTwinCardsWidget: (should: Boolean, popAction: VoidCallback) -> Unit, - ) : TwinCardsAction() - - data class SaveScannedTwinCardAndNavigateToWallet( - val scanResponse: ScanResponse, - ) : TwinCardsAction() - - sealed class Balance { - object Update : TwinCardsAction() - data class Set(val balance: OnboardingWalletBalance) : TwinCardsAction() - data class SetCriticalError(val error: TapError?) : TwinCardsAction() - data class SetNonCriticalError(val error: TapError?) : TwinCardsAction() - } - - sealed class Confetti { - object Show : TwinCardsAction() - object Hide : TwinCardsAction() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt deleted file mode 100644 index 9b0daea01d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt +++ /dev/null @@ -1,399 +0,0 @@ -package com.tangem.tap.features.onboarding.products.twins.redux - -import com.tangem.common.CompletionResult -import com.tangem.common.core.TangemError -import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.guard -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.utils.popTo -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.toWrappedList -import com.tangem.core.ui.message.dialog.Dialogs -import com.tangem.domain.common.extensions.makePrimaryWalletManager -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.domain.common.util.twinsIsTwinned -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.legacy.asLockable -import com.tangem.sdk.extensions.localizedDescriptionRes -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.entities.ProgressState -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.postUi -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.model.Currency -import com.tangem.tap.domain.twins.TwinCardsManager -import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.features.onboarding.OnboardingHelper -import com.tangem.tap.mainScope -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.utils.extensions.DELAY_SDK_DIALOG_CLOSE -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.rekotlin.Action -import org.rekotlin.DispatchFunction -import org.rekotlin.Middleware -import kotlin.reflect.KClass - -object TwinCardsMiddleware { - val handler = twinsWalletMiddleware -} - -private val twinsWalletMiddleware: Middleware = { dispatch, _ -> - { next -> - { action -> - handle(action, dispatch) - next(action) - } - } -} - -@Suppress("LongMethod", "ComplexMethod", "MagicNumber") -private fun handle(action: Action, dispatch: DispatchFunction) { - if (action !is TwinCardsAction) return - - val globalState = store.state.globalState - val onboardingManager = globalState.onboardingState.onboardingManager - val twinCardsState = store.state.twinCardsState - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - - fun getScanResponse(): ScanResponse { - return when (twinCardsState.mode) { - is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse - is CreateTwinWalletMode.RecreateWallet -> globalState.scanResponse - } ?: throw NullPointerException("ScanResponse can't be NULL") - } - - fun updateScanResponse(response: ScanResponse) { - when (twinCardsState.mode) { - is CreateTwinWalletMode.CreateWallet -> onboardingManager?.scanResponse = response - is CreateTwinWalletMode.RecreateWallet -> store.dispatchOnMain(GlobalAction.SaveScanResponse(response)) - } - } - - fun startCardActivation(cardId: String) { - if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) { - scope.launch { - onboardingManager?.startActivation(cardId) - } - } - } - - fun finishCardActivation() { - if (twinCardsState.mode == CreateTwinWalletMode.CreateWallet) { - Analytics.send(Onboarding.Finished()) - - val cardIds = listOfNotNull(getScanResponse().card.cardId, twinCardsState.pairCardId) - - if (cardIds.isNotEmpty()) { - mainScope.launch { - onboardingManager?.finishActivation(cardIds) - } - } - } - } - - when (action) { - is TwinCardsAction.Init -> { - mainScope.launch { - if (twinCardsState.currentStep is TwinCardsStep.WelcomeOnly) return@launch - - if (twinCardsState.mode is CreateTwinWalletMode.RecreateWallet) { - store.dispatch(GlobalAction.SaveScanResponse(twinCardsState.mode.scanResponse)) - } - - val scanResponse = getScanResponse() - onboardingManager?.apply { - if (!isActivationStarted(scanResponse.card.cardId)) { - Analytics.send(Onboarding.Started()) - } - } - - when (twinCardsState.mode) { - is CreateTwinWalletMode.CreateWallet -> { - mainScope.launch { - val wasTwinsOnboardingShown = store.inject(DaggerGraphState::wasTwinsOnboardingShownUseCase) - .invokeSync() - - val dispatchAction = if (wasTwinsOnboardingShown) { - val step = when { - !scanResponse.twinsIsTwinned() -> TwinCardsStep.CreateFirstWallet - twinCardsState.walletBalance.balanceIsToppedUp() -> TwinCardsStep.Done - else -> TwinCardsStep.TopUpWallet - } - TwinCardsAction.SetStepOfScreen(step) - } else { - TwinCardsAction.SetStepOfScreen(TwinCardsStep.Welcome) - } - - store.dispatch(dispatchAction) - } - } - is CreateTwinWalletMode.RecreateWallet -> { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Warning)) - } - } - } - } - is TwinCardsAction.SetStepOfScreen -> { - when (action.step) { - is TwinCardsStep.WelcomeOnly, TwinCardsStep.Welcome -> { - Analytics.send(Onboarding.Twins.ScreenOpened()) - - scope.launch { - store.inject(DaggerGraphState::saveTwinsOnboardingShownUseCase).invoke() - } - } - is TwinCardsStep.CreateFirstWallet -> { - Analytics.send(Onboarding.CreateWallet.ScreenOpened()) - Analytics.send(Onboarding.Twins.SetupStarted()) - } - TwinCardsStep.TopUpWallet -> { - Analytics.send(Onboarding.Topup.ScreenOpened()) - store.dispatch(TwinCardsAction.Balance.Update) - } - TwinCardsStep.Done -> { - finishCardActivation() - postUi(500) { store.dispatch(TwinCardsAction.Confetti.Show) } - } - TwinCardsStep.None, - TwinCardsStep.Warning, - TwinCardsStep.CreateSecondWallet, - TwinCardsStep.CreateThirdWallet, - -> Unit - } - } - is TwinCardsAction.Wallet.LaunchFirstStep -> { - val manager = TwinCardsManager(card = getScanResponse().card) - store.dispatch(TwinCardsAction.CardsManager.Set(manager)) - - scope.launch { - when (val result = manager.createFirstWallet(action.initialMessage)) { - is CompletionResult.Success -> { - // remove wallet only after first step of retwin - scope.launch { - userWalletsListManager.delete( - listOfNotNull(UserWalletIdBuilder.scanResponse(getScanResponse()).build()), - ) - } - Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) - startCardActivation(result.data.cardId) - delay(DELAY_SDK_DIALOG_CLOSE) - withMainContext { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateSecondWallet)) - } - } - is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) - } - } - } - is TwinCardsAction.Wallet.LaunchSecondStep -> { - val manager = twinCardsState.twinCardsManager ?: return - - scope.launch { - val result = manager.createSecondWallet( - action.initialMessage, - action.preparingMessage, - action.creatingWalletMessage, - ) - when (result) { - is CompletionResult.Success -> { - startCardActivation(result.data.cardId) - delay(DELAY_SDK_DIALOG_CLOSE) - withMainContext { - store.dispatch(TwinCardsAction.SetPairCardId(result.data.cardId)) - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateThirdWallet)) - } - } - is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) - } - } - } - is TwinCardsAction.Wallet.LaunchThirdStep -> { - val manager = twinCardsState.twinCardsManager ?: return - - scope.launch { - when (val result = manager.complete(action.message)) { - is CompletionResult.Success -> { - Analytics.send(Onboarding.Twins.SetupFinished()) - updateScanResponse(result.data) - - delay(DELAY_SDK_DIALOG_CLOSE) - withMainContext { - when (twinCardsState.mode) { - is CreateTwinWalletMode.CreateWallet -> { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet)) - } - is CreateTwinWalletMode.RecreateWallet -> { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done)) - } - } - } - } - is CompletionResult.Failure -> showCardVerificationFailedDialog(result.error) - } - } - } - is TwinCardsAction.Balance.Update -> { - val walletManager = if (twinCardsState.walletManager != null) { - twinCardsState.walletManager - } else { - val wmFactory = runBlocking { - store.inject(DaggerGraphState::blockchainSDKFactory).getWalletManagerFactorySync() - } - val walletManager = wmFactory?.makePrimaryWalletManager(getScanResponse()).guard { - val message = "Loading cancelled. Cause: wallet manager didn't created" - val customError = TapError.CustomError(message) - store.dispatchErrorNotification(customError) - return - } - dispatch(TwinCardsAction.SetWalletManager(walletManager)) - walletManager - } - val isLoadedBefore = twinCardsState.walletBalance.state != ProgressState.Loading - val balanceIsLoading = twinCardsState.walletBalance.copy( - currency = Currency.Blockchain( - walletManager.wallet.blockchain, - walletManager.wallet.publicKey.derivationPath?.rawPath, - ), - state = ProgressState.Loading, - error = null, - criticalError = null, - ) - store.dispatch(TwinCardsAction.Balance.Set(balanceIsLoading)) - - scope.launch { - val loadedBalance = onboardingManager?.updateBalance(walletManager) ?: return@launch - loadedBalance.criticalError?.let(store::dispatchErrorNotification) - delay(if (isLoadedBefore) 0 else 300) - withMainContext { - store.dispatch(TwinCardsAction.Balance.Set(loadedBalance)) - store.dispatch(TwinCardsAction.Balance.SetCriticalError(loadedBalance.criticalError)) - store.dispatch(TwinCardsAction.Balance.SetNonCriticalError(loadedBalance.error)) - } - } - } - is TwinCardsAction.Balance.Set -> { - if (action.balance.balanceIsToppedUp()) { - OnboardingHelper.sendToppedUpEvent(getScanResponse()) - store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done)) - } - } - is TwinCardsAction.ShowAddressInfoDialog -> { - val addressData = twinCardsState.walletManager?.getAddressData() ?: return - - val appDialog = AppDialog.AddressInfoDialog(twinCardsState.walletBalance.currency, addressData) - store.dispatchDialogShow(appDialog) - } - is TwinCardsAction.TopUp -> { - val walletManager = twinCardsState.walletManager.guard { - store.dispatchDebugErrorNotification("NPE: WalletManager") - return - } - - OnboardingHelper.handleTopUpAction( - walletManager = walletManager, - scanResponse = onboardingManager?.scanResponse ?: return, - ) - } - TwinCardsAction.Done -> { - val scanResponse = getScanResponse() - when (twinCardsState.mode) { - is CreateTwinWalletMode.CreateWallet -> { - store.dispatchOnMain(GlobalAction.Onboarding.Stop) - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) - } - is CreateTwinWalletMode.RecreateWallet -> { - scope.launch { - val walletsRepository = store.inject(DaggerGraphState::walletsRepository) - - if (walletsRepository.shouldSaveUserWalletsSync()) { - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(scanResponse) - } else { - store.dispatchNavigationAction { popTo() } - } - } - } - } - } - is TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet -> { - OnboardingHelper.trySaveWalletAndNavigateToWalletScreen( - scanResponse = action.scanResponse, - ) - } - is TwinCardsAction.OnBackPressed -> { - if (twinCardsState.twinningInProgress) { - store.dispatchDialogShow(OnboardingDialog.TwinningProcessNotCompleted) - } else { - val onOkCallback = { - val shouldReturnCardBack = twinCardsState.mode == CreateTwinWalletMode.CreateWallet && - twinCardsState.currentStep != TwinCardsStep.TopUpWallet && - twinCardsState.currentStep != TwinCardsStep.Done - - OnboardingHelper.onInterrupted() - store.dispatch(TwinCardsAction.CardsManager.Release) - - action.shouldResetTwinCardsWidget(shouldReturnCardBack) { - store.dispatchNavigationAction { popTo(routeClass = getPopBackScreen()) } - } - } - store.dispatchDialogShow(OnboardingDialog.InterruptOnboarding(onOkCallback)) - } - } - else -> Unit - } -} - -private fun showCardVerificationFailedDialog(error: TangemError) { - if (error is TangemSdkError.CardVerificationFailed) { - Analytics.send( - event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed( - AnalyticsParam.ScreensSources.Onboarding, - ), - ) - - val resource = error.localizedDescriptionRes() - val resId = resource.resId ?: R.string.common_unknown_error - val resArgs = resource.args.map { it.value } - - store.inject(DaggerGraphState::uiMessageSender).send( - message = Dialogs.cardVerificationFailed( - errorDescription = resourceReference(id = resId, resArgs.toWrappedList()), - onRequestSupport = { - mainScope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase).invoke( - type = FeedbackEmailType.CardAttestationFailed, - ) - } - }, - ), - ) - } -} - -private fun getPopBackScreen(): KClass { - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - - return if (userWalletsListManager.hasUserWallets) { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } - - if (isLocked) { - AppRoute.Welcome::class - } else { - AppRoute.Wallet::class - } - } else { - AppRoute.Home::class - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt deleted file mode 100644 index 594dd4e77c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.tap.features.onboarding.products.twins.redux - -import com.tangem.domain.common.getTwinCardNumber -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.tap.common.redux.AppState -import org.rekotlin.Action - -object TwinCardsReducer { - fun reduce(action: Action, state: AppState): TwinCardsState = internalReduce(action, state) -} - -@Suppress("ComplexMethod") -private fun internalReduce(action: Action, state: AppState): TwinCardsState { - if (action !is TwinCardsAction) return state.twinCardsState - - var state = state.twinCardsState - - when (action) { - is TwinCardsAction.IfTwinsPrepareState -> { - state = if (action.scanResponse.cardTypesResolver.isTangemTwins()) { - val cardNumber = action.scanResponse.card.getTwinCardNumber() ?: return state - - TwinCardsState( - cardNumber = cardNumber, - mode = CreateTwinWalletMode.CreateWallet, - ) - } else { - TwinCardsState() - } - } - is TwinCardsAction.SetMode -> { - state = state.copy( - mode = action.mode, - userWasUnderstandIfWalletRecreate = false, - ) - } - is TwinCardsAction.SetStepOfScreen -> { - state = state.copy( - currentStep = action.step, - welcomeOnlyScanResponse = (action.step as? TwinCardsStep.WelcomeOnly)?.scanResponse, - ) - } - is TwinCardsAction.SetUserUnderstand -> { - state = state.copy(userWasUnderstandIfWalletRecreate = action.isUnderstand) - } - is TwinCardsAction.CardsManager.Set -> { - state = state.copy(twinCardsManager = action.manager) - } - TwinCardsAction.CardsManager.Release -> { - state = state.copy(twinCardsManager = null) - } - is TwinCardsAction.SetPairCardId -> { - state = state.copy(pairCardId = action.cardId) - } - is TwinCardsAction.SetWalletManager -> { - state = state.copy(walletManager = action.walletManager) - } - is TwinCardsAction.Balance.Set -> { - state = state.copy(walletBalance = action.balance) - } - is TwinCardsAction.Balance.SetCriticalError -> { - state = state.copy(balanceCriticalError = action.error) - } - is TwinCardsAction.Balance.SetNonCriticalError -> { - state = state.copy(balanceNonCriticalError = action.error) - } - is TwinCardsAction.Confetti.Show -> { - state = state.copy(showConfetti = true) - } - is TwinCardsAction.Confetti.Hide -> { - state = state.copy(showConfetti = false) - } - else -> {} - } - - return state -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt deleted file mode 100644 index 83e641bc8a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.tap.features.onboarding.products.twins.redux - -import com.tangem.blockchain.common.WalletManager -import com.tangem.domain.common.TwinCardNumber -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.twins.TwinCardsManager -import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import org.rekotlin.StateType - -/** -[REDACTED_AUTHOR] - */ -data class TwinCardsState( - // if cardNumber = null -> the scanned card is not Twin - val cardNumber: TwinCardNumber? = null, - val mode: CreateTwinWalletMode = CreateTwinWalletMode.CreateWallet, - val currentStep: TwinCardsStep = TwinCardsStep.Welcome, - val userWasUnderstandIfWalletRecreate: Boolean = false, - // available only when creating or recreating wallets - val twinCardsManager: TwinCardsManager? = null, - - // for the onboarding - val pairCardId: String? = null, // available after create\recreate wallets - val walletManager: WalletManager? = null, - val walletBalance: OnboardingWalletBalance = OnboardingWalletBalance.loading(), - val balanceNonCriticalError: TapError? = null, - val balanceCriticalError: TapError? = null, - val showConfetti: Boolean = false, - val welcomeOnlyScanResponse: ScanResponse? = null, -) : StateType { - - val steps: List - get() = when (mode) { - is CreateTwinWalletMode.CreateWallet -> listOf( - TwinCardsStep.None, - TwinCardsStep.CreateFirstWallet, - TwinCardsStep.CreateSecondWallet, - TwinCardsStep.CreateThirdWallet, - TwinCardsStep.TopUpWallet, - TwinCardsStep.Done, - ) - is CreateTwinWalletMode.RecreateWallet -> listOf( - TwinCardsStep.None, - TwinCardsStep.CreateFirstWallet, - TwinCardsStep.CreateSecondWallet, - TwinCardsStep.CreateThirdWallet, - TwinCardsStep.Done, - ) - } - - val progress: Int - get() = steps.indexOf(currentStep) - - val twinningInProgress: Boolean - get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet -} - -sealed class CreateTwinWalletMode { - data object CreateWallet : CreateTwinWalletMode() - - data class RecreateWallet( - val scanResponse: ScanResponse, - ) : CreateTwinWalletMode() -} - -sealed class TwinCardsStep { - object None : TwinCardsStep() - data class WelcomeOnly( - val scanResponse: ScanResponse, - ) : TwinCardsStep() - - object Welcome : TwinCardsStep() - object Warning : TwinCardsStep() - object CreateFirstWallet : TwinCardsStep() - object CreateSecondWallet : TwinCardsStep() - object CreateThirdWallet : TwinCardsStep() - - // for the onboarding - object TopUpWallet : TwinCardsStep() - object Done : TwinCardsStep() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt deleted file mode 100644 index f058c94f3b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/OnboardingTwinsFragment.kt +++ /dev/null @@ -1,460 +0,0 @@ -package com.tangem.tap.features.onboarding.products.twins.ui - -import android.os.Bundle -import android.util.TypedValue -import android.view.View -import android.view.animation.OvershootInterpolator -import androidx.annotation.LayoutRes -import androidx.constraintlayout.widget.ConstraintSet -import androidx.core.view.MenuProvider -import androidx.core.view.isVisible -import androidx.transition.TransitionManager -import coil.load -import com.tangem.Message -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.extensions.VoidCallback -import com.tangem.core.analytics.Analytics -import com.tangem.core.navigation.ShareElement -import com.tangem.core.ui.extensions.setStatusBarColor -import com.tangem.domain.common.TwinCardNumber -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.models.Artwork -import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.beginDelayedTransition -import com.tangem.tap.common.extensions.getDrawableCompat -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget -import com.tangem.tap.common.transitions.InternalNoteLayoutTransition -import com.tangem.tap.domain.twins.TwinsCardWidget -import com.tangem.tap.features.addBackPressHandler -import com.tangem.tap.features.onboarding.OnboardingMenuProvider -import com.tangem.tap.features.onboarding.OnboardingWalletBalance -import com.tangem.tap.features.onboarding.products.BaseOnboardingFragment -import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState -import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep -import com.tangem.tap.store -import com.tangem.utils.Provider -import com.tangem.utils.extensions.stripZeroPlainString -import com.tangem.wallet.R -import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding -import dagger.hilt.android.AndroidEntryPoint - -@Suppress("LargeClass") -@AndroidEntryPoint -internal class OnboardingTwinsFragment : BaseOnboardingFragment() { - - private val mainBinding by lazy { binding.vMain } - private var previousStep: TwinCardsStep = TwinCardsStep.None - - private lateinit var twinsWidget: TwinsCardWidget - private lateinit var btnRefreshBalanceWidget: RefreshBalanceWidget - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - store.dispatch(TwinCardsAction.Init) - } - - override fun configureTransitions() { - when (store.state.twinCardsState.mode) { - is CreateTwinWalletMode.CreateWallet -> { - super.configureTransitions() - } - is CreateTwinWalletMode.RecreateWallet -> { - configureDefaultTransactions() - } - } - } - - override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( - scanResponseProvider = Provider { getActualScanResponse() }, - ) - - @Suppress("MagicNumber") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - addBackPressHandler(this) - - reconfigureLayoutForTwins(mainBinding.onboardingTopContainer) - - val typedValue = TypedValue() - resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) - val deviceScaleFactorForWelcomeState = typedValue.float - - twinsWidget = TwinsCardWidget( - LeapfrogWidget(mainBinding.onboardingTopContainer.cardsContainer), - deviceScaleFactorForWelcomeState, - ) { - 285f * deviceScaleFactorForWelcomeState - } - btnRefreshBalanceWidget = - RefreshBalanceWidget(mainBinding.onboardingTopContainer.onboardingWalletContainer) - - binding.toolbar.title = getText(R.string.twins_recreate_toolbar) - - mainBinding.onboardingTopContainer.imvTwinFrontCard.load(Artwork.TWIN_CARD_1_URL) { - placeholder(R.drawable.card_placeholder_black) - error(R.drawable.card_placeholder_black) - fallback(R.drawable.card_placeholder_black) - } - - mainBinding.onboardingTopContainer.imvTwinBackCard.load(Artwork.TWIN_CARD_2_URL) { - placeholder(R.drawable.card_placeholder_white) - error(R.drawable.card_placeholder_white) - fallback(R.drawable.card_placeholder_white) - } - } - - override fun onStart() { - super.onStart() - setStatusBarColor(R.color.background_primary) - } - - private fun reconfigureLayoutForTwins(containerBinding: LayoutOnboardingContainerTopBinding) = - with(containerBinding) { - imvFrontCard.hide() - imvCardBackground.hide() - cardsContainer.show() - - imvTwinFrontCard.transitionName = ShareElement.imvFrontCard - imvTwinBackCard.transitionName = ShareElement.imvBackCard - - // if don't this, the bg_circle_... is overflow the app_bar - binding.appBar.bringToFront() - } - - override fun subscribeToStore() { - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.twinCardsState == newState.twinCardsState - }.select { it.twinCardsState } - } - storeSubscribersList.add(this) - } - - override fun newState(state: TwinCardsState) { - if (activity == null || view == null) return - - pbBinding.pbState.max = state.steps.size - 1 - pbBinding.pbState.progress = state.progress - - when (state.currentStep) { - is TwinCardsStep.WelcomeOnly -> setupWelcomeOnlyState(state, state.currentStep.scanResponse) - is TwinCardsStep.Welcome -> setupWelcomeState(state) - is TwinCardsStep.Warning -> setupWarningState(state) - is TwinCardsStep.CreateFirstWallet -> setupCreateFirstWalletState(state) - is TwinCardsStep.CreateSecondWallet -> setupCreateSecondWalletState(state) - is TwinCardsStep.CreateThirdWallet -> setupCreateThirdWalletState(state) - is TwinCardsStep.TopUpWallet -> setupTopUpWalletState(state) - is TwinCardsStep.Done -> setupDoneState(state) - else -> {} - } - - setBalance(state) - showConfetti(state.showConfetti) - previousStep = state.currentStep - } - - private fun setBalance(state: TwinCardsState) { - if (state.walletBalance.currency.blockchain == Blockchain.Unknown) return - - with(mainBinding.onboardingTopContainer.onboardingTvBalance) { - if (state.balanceCriticalError == null) { - val balanceValue = state.walletBalance.value.stripZeroPlainString() - tvBalanceValue.text = balanceValue - tvBalanceCurrency.text = state.walletBalance.currency.currencySymbol - } else { - tvBalanceValue.text = "–" - tvBalanceCurrency.text = "" - } - } - } - - private fun setupWelcomeOnlyState(state: TwinCardsState, scanResponse: ScanResponse) { - setupWelcomeState(state) { - store.dispatch(TwinCardsAction.SaveScannedTwinCardAndNavigateToWallet(scanResponse)) - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.None)) - } - } - - private fun setupWelcomeState(state: TwinCardsState) { - setupWelcomeState(state) { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) - } - } - - private fun setupWelcomeState(state: TwinCardsState, onMainButtonClick: VoidCallback) = - with(mainBinding.onboardingActionContainer) { - twinsWidget.toWelcome(false) - - mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.show() - pbBinding.pbState.hide() - - tvHeader.setText(R.string.twins_onboarding_subtitle) - tvBody.text = getString( - R.string.twins_onboarding_description_format, - state.cardNumber?.pairIndexNumber(), - ) - - btnMainAction.setText(R.string.common_continue) - btnMainAction.icon = null - btnMainAction.setOnClickListener { onMainButtonClick() } - } - - private fun setupWarningState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { - twinsWidget.toWelcome(false) - - mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() - pbBinding.pbState.hide() - chbUnderstand.show() - - tvHeader.setText(R.string.common_warning) - tvBody.setText(R.string.twins_recreate_warning) - - chbUnderstand.setOnCheckedChangeListener { _, isChecked -> - store.dispatch(TwinCardsAction.SetUserUnderstand(isChecked)) - } - btnMainAction.isEnabled = state.userWasUnderstandIfWalletRecreate - btnMainAction.setText(R.string.common_continue) - btnMainAction.icon = null - btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) - } - } - - private fun setupCreateFirstWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { - mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.root.hide() - mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleLarge.hide() - mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMedium.hide() - mainBinding.onboardingTopContainer.onboardingTwinsWelcomeBg.bgCircleMin.hide() - chbUnderstand.hide() - pbBinding.pbState.show() - - mainBinding.onboardingTopContainer.onboardingWalletContainer.beginDelayedTransition() - - when (previousStep) { - TwinCardsStep.None -> { - twinsWidget.toLeapfrog(false) { - when (state.cardNumber) { - TwinCardNumber.First -> {} - TwinCardNumber.Second -> { - switchToCard( - state.cardNumber, - false, - ) - } - else -> {} - } - } - } - TwinCardsStep.Welcome, TwinCardsStep.Warning -> { - twinsWidget.toLeapfrog(onEnd = { switchToCard(state.cardNumber) }) - } - else -> {} - } - - val twinIndexNumber = state.cardNumber?.indexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) - btnMainAction.setIconResource(R.drawable.ic_tangem_24) - btnMainAction.setOnClickListener { - Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) - store.dispatch( - TwinCardsAction.Wallet.LaunchFirstStep( - initialMessage = Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), - ), - ) - } - - btnAlternativeAction.isVisible = false - btnAlternativeAction.isClickable = false - } - - private fun setupCreateSecondWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { - switchToCard(state.cardNumber?.pairNumber()) - - val twinPairIndexNumber = state.cardNumber?.pairIndexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinPairIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = - getString(R.string.twins_recreate_button_format, twinPairIndexNumber) - btnMainAction.setIconResource(R.drawable.ic_tangem_24) - btnMainAction.setOnClickListener { - store.dispatch( - TwinCardsAction.Wallet.LaunchSecondStep( - Message( - getString( - R.string.twins_recreate_title_format, - twinPairIndexNumber, - ), - ), - Message(getString(R.string.twins_recreate_title_preparing)), - Message(getString(R.string.twins_recreate_title_creating_wallet)), - ), - ) - } - } - - private fun setupCreateThirdWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { - switchToCard(state.cardNumber) - - val twinIndexNumber = state.cardNumber?.indexNumber() - tvHeader.text = getString(R.string.twins_recreate_title_format, twinIndexNumber) - tvBody.setText(R.string.onboarding_twins_interrupt_warning) - - btnMainAction.text = getString(R.string.twins_recreate_button_format, twinIndexNumber) - btnMainAction.setIconResource(R.drawable.ic_tangem_24) - btnMainAction.setOnClickListener { - store.dispatch( - TwinCardsAction.Wallet.LaunchThirdStep( - Message(getString(R.string.twins_recreate_title_format, twinIndexNumber)), - ), - ) - } - } - - private fun setupTopUpWalletState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { - when (previousStep) { - TwinCardsStep.None, TwinCardsStep.Welcome -> { - when (state.cardNumber) { - TwinCardNumber.First -> { - twinsWidget.leapfrogWidget.unfold(true) { - twinsWidget.toActivate(true) - } - } - TwinCardNumber.Second -> { - switchToCard(state.cardNumber, true) { - twinsWidget.toActivate(true) - } - } - else -> {} - } - } - TwinCardsStep.CreateThirdWallet -> { - twinsWidget.toActivate() - } - else -> {} - } - - if (availableForBuy(getActualScanResponse(), state.walletBalance)) { - btnMainAction.setText(R.string.onboarding_top_up_button_but_crypto) - btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.TopUp) - } - btnAlternativeAction.isVisible = true - btnAlternativeAction.setText(R.string.onboarding_top_up_button_show_wallet_address) - btnAlternativeAction.setOnClickListener { - store.dispatch(TwinCardsAction.ShowAddressInfoDialog) - } - } else { - btnMainAction.setText(R.string.onboarding_button_receive_crypto) - btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.ShowAddressInfoDialog) - } - - btnAlternativeAction.isVisible = false - } - - tvHeader.setText(R.string.onboarding_topup_title) - tvBody.setText(R.string.onboarding_top_up_body) - btnMainAction.icon = null - - btnRefreshBalanceWidget.changeState(state.walletBalance.state) - if (btnRefreshBalanceWidget.isShowing != true) { - btnRefreshBalanceWidget.mainView.setOnClickListener { - store.dispatch(TwinCardsAction.Balance.Update) - } - } - mainBinding.onboardingTopContainer.imvCardBackground.setBackgroundDrawable( - requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8), - ) - mainBinding.onboardingTopContainer.imvCardBackground.backgroundTintList = - requireContext().resources.getColorStateList(R.color.onboarding_card_background, null) - - updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins) - } - - private fun setupDoneState(state: TwinCardsState) = with(mainBinding.onboardingActionContainer) { - btnMainAction.setText(R.string.common_continue) - btnMainAction.icon = null - btnMainAction.setOnClickListener { - store.dispatch(TwinCardsAction.Confetti.Hide) - store.dispatch(TwinCardsAction.Done) - } - - btnAlternativeAction.isVisible = false - btnAlternativeAction.isClickable = false - - tvHeader.setText(R.string.onboarding_done_header) - tvBody.setText(R.string.onboarding_done_body) - - val layout = when (state.mode) { - is CreateTwinWalletMode.CreateWallet -> R.layout.lp_onboarding_done_activation_twins - is CreateTwinWalletMode.RecreateWallet -> R.layout.lp_onboarding_done - } - updateConstraints(state.currentStep, layout) - } - - private fun updateConstraints(currentStep: TwinCardsStep, @LayoutRes layoutId: Int) { - if (this.previousStep == currentStep) return - - with(mainBinding.onboardingTopContainer) { - val constraintSet = ConstraintSet() - constraintSet.clone(requireContext(), layoutId) - constraintSet.applyTo(onboardingWalletContainer) - val transition = InternalNoteLayoutTransition() - transition.interpolator = OvershootInterpolator() - TransitionManager.beginDelayedTransition(onboardingWalletContainer, transition) - } - } - - private fun switchToCard(cardNumber: TwinCardNumber?, animate: Boolean = true, onEnd: VoidCallback = {}) { - val position = cardNumber?.number ?: return - - if (twinsWidget.leapfrogWidget.getViewPositionByIndex(1) != position - 1) { - twinsWidget.leapfrogWidget.leap(animate, onEnd) - } else { - onEnd() - } - } - - private fun availableForBuy(scanResponse: ScanResponse?, walletBalance: OnboardingWalletBalance): Boolean { - scanResponse ?: return false - return store.state.globalState.exchangeManager.availableForBuy(scanResponse, walletBalance.currency) - } - - private fun getActualScanResponse(): ScanResponse { - return store.state.twinCardsState.welcomeOnlyScanResponse - ?: store.state.globalState.onboardingState.onboardingManager?.scanResponse - ?: store.state.detailsState.scanResponse - ?: error("ScanResponse must be not null") - } - - override fun handleOnBackPressed() { - store.dispatch( - TwinCardsAction.OnBackPressed { should, popAction -> - store.dispatch(TwinCardsAction.Confetti.Hide) - showConfetti(false) - if (should) { - switchToCard(TwinCardNumber.First, true, popAction) - } else { - popAction() - } - }, - ) - } -} - -private fun TwinCardNumber?.indexNumber(): String { - return this?.number?.toString() ?: "" -} - -private fun TwinCardNumber?.pairIndexNumber(): String { - return this?.pairNumber()?.number?.toString() ?: "" -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/dialog/TwinningProcessNotCompletedDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/dialog/TwinningProcessNotCompletedDialog.kt deleted file mode 100644 index 60faf916d7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/dialog/TwinningProcessNotCompletedDialog.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.tap.features.onboarding.products.twins.ui.dialog - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.extensions.dispatchDialogHide -import com.tangem.tap.store -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -object TwinningProcessNotCompletedDialog { - fun create(context: Context): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog) - .setMessage(R.string.onboarding_twin_exit_warning) - .setPositiveButton(R.string.warning_button_ok) { _, _ -> } - .setOnDismissListener { store.dispatchDialogHide() } - .create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt new file mode 100644 index 0000000000..ef465fad15 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupDialog.kt @@ -0,0 +1,14 @@ +package com.tangem.tap.features.onboarding.products.wallet.redux + +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.redux.StateDialog + +sealed class BackupDialog : StateDialog { + data class UnfinishedBackupFound( + val scanResponse: ScanResponse? = null, + ) : BackupDialog() + + data class ConfirmDiscardingBackup( + val scanResponse: ScanResponse? = null, + ) : BackupDialog() +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt new file mode 100644 index 0000000000..c3567c8f3a --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/BackupMiddleware.kt @@ -0,0 +1,59 @@ +package com.tangem.tap.features.onboarding.products.wallet.redux + +import com.tangem.common.routing.AppRoute +import com.tangem.core.analytics.Analytics +import com.tangem.tap.* +import com.tangem.tap.common.analytics.events.Onboarding.* +import com.tangem.tap.common.extensions.* +import com.tangem.tap.common.redux.AppState +import com.tangem.tap.features.demo.DemoHelper +import com.tangem.tap.proxy.redux.DaggerGraphState +import kotlinx.coroutines.launch +import org.rekotlin.Middleware + +class BackupMiddleware { + val backupMiddleware: Middleware = { dispatch, state -> + { next -> + { action -> + if (action is BackupAction) handleBackupAction(state, action) + next(action) + } + } + } +} + +@Suppress("LongMethod", "ComplexMethod", "MagicNumber") +private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) { + if (DemoHelper.tryHandle(appState, action)) return + + when (action) { + is BackupAction.DiscardBackup -> { + backupService.discardSavedBackup() + } + is BackupAction.DiscardSavedBackup -> { + mainScope.launch { + backupService.discardSavedBackup() + + val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository) + val cardRepository = store.inject(DaggerGraphState::cardRepository) + val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch + + cardRepository.finishCardActivation(unfinishedBackup.card.cardId) + onboardingRepository.clearUnfinishedFinalizeOnboarding() + Analytics.send(Finished()) + } + } + is BackupAction.ResumeFoundUnfinishedBackup -> { + if (action.unfinishedBackupScanResponse != null) { + store.dispatchNavigationAction { + push( + AppRoute.Onboarding( + scanResponse = action.unfinishedBackupScanResponse, + mode = AppRoute.Onboarding.Mode.ContinueFinalize, + ), + ) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt index a4be295931..bf97ad0ae2 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletAction.kt @@ -1,108 +1,19 @@ package com.tangem.tap.features.onboarding.products.wallet.redux -import android.net.Uri -import com.tangem.common.CompletionResult -import com.tangem.common.card.Card import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.onboarding.data.model.CreateWalletResponse -import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource -import com.tangem.sdk.api.CreateProductWalletTaskResponse -import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action sealed class OnboardingWalletAction : Action { - data object Init : OnboardingWalletAction() - data object GetToCreateWalletStep : OnboardingWalletAction() - data object CreateWallet : OnboardingWalletAction() - data class WalletWasCreated( - val shouldSendAnalyticsEvent: Boolean, - val result: CompletionResult, - ) : OnboardingWalletAction() - - data object Done : OnboardingWalletAction() - data class FinishOnboarding(val scope: CoroutineScope) : OnboardingWalletAction() - - data object ResumeBackup : OnboardingWalletAction() - - data class LoadArtwork(val cardArtworkUriForUnfinishedBackup: Uri? = null) : OnboardingWalletAction() - class SetPrimaryCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() - class SetSecondCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() - class SetThirdCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction() - - data object OnBackPressed : OnboardingWalletAction() - data class WalletSaved(val userWalletId: UserWalletId) : OnboardingWalletAction() } -sealed class OnboardingWallet2Action : OnboardingWalletAction() { - data class Init(val maxProgress: Int) : OnboardingWallet2Action() - data class SetDependencies(val maxProgress: Int) : OnboardingWallet2Action() - - data class CreateWallet( - val callback: (CompletionResult) -> Unit, - ) : OnboardingWallet2Action() - - data class ImportWallet( - val mnemonicComponents: List, - val passphrase: String?, - val seedPhraseSource: SeedPhraseSource, - val callback: (CompletionResult) -> Unit, - ) : OnboardingWallet2Action() - - data class WalletWasCreated( - val result: CompletionResult, - ) : OnboardingWallet2Action() -} - sealed class BackupAction : Action { - data object IntroduceBackup : BackupAction() - data object StartBackup : BackupAction() - data object SkipBackup : BackupAction() - - data object ErrorInBackupCard : BackupAction() - data object StartAddingPrimaryCard : BackupAction() - data object ScanPrimaryCard : BackupAction() - data class SetHasRing(val hasRing: Boolean) : BackupAction() - - /** - * Check for unfinished backup of standard Wallets - * See more GlobalAction.Onboarding.StartForUnfinishedBackup - */ - data object CheckForUnfinishedBackup : BackupAction() - - data object StartAddingBackupCards : BackupAction() - data object AddBackupCard : BackupAction() { - data class Success(val card: Card) : BackupAction() - data class ChangeButtonLoading(val isLoading: Boolean) : BackupAction() - } - - data object FinishAddingBackupCards : BackupAction() - - data object ShowAccessCodeInfoScreen : BackupAction() - data object ShowEnterAccessCodeScreen : BackupAction() - data class CheckAccessCode(val accessCode: String) : BackupAction() - data class SetAccessCodeError(val error: AccessCodeError?) : BackupAction() - data class SaveFirstAccessCode(val accessCode: String) : BackupAction() - data class SaveAccessCodeConfirmation(val accessCodeConfirmation: String) : BackupAction() - data object OnAccessCodeDialogClosed : BackupAction() - - data object PrepareToWritePrimaryCard : BackupAction() - data object WritePrimaryCard : BackupAction() - data class PrepareToWriteBackupCard(val cardNumber: Int) : BackupAction() - data class WriteBackupCard(val cardNumber: Int) : BackupAction() - - data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction() - - data class BackupFinished(val userWalletId: UserWalletId?) : BackupAction() - data object DiscardBackup : BackupAction() data object DiscardSavedBackup : BackupAction() data class ResumeFoundUnfinishedBackup( val unfinishedBackupScanResponse: ScanResponse?, ) : BackupAction() - - data class ResetBackupCard(val cardId: String) : BackupAction() } \ No newline at end of file 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 deleted file mode 100644 index fe64265670..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt +++ /dev/null @@ -1,849 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.redux - -import android.net.Uri -import com.tangem.common.CompletionResult -import com.tangem.common.card.Card -import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.ifNotNull -import com.tangem.common.extensions.toHexString -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRouter -import com.tangem.common.services.Result -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam.ScreensSources -import com.tangem.core.analytics.models.ExceptionAnalyticsEvent -import com.tangem.core.analytics.models.event.OnboardingAnalyticsEvent -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.toWrappedList -import com.tangem.core.ui.message.dialog.Dialogs -import com.tangem.domain.common.extensions.withIOContext -import com.tangem.domain.common.extensions.withMainContext -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS -import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.legacy.UserWalletsListManager -import com.tangem.domain.wallets.models.Artwork -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.CardArtworksProvider -import com.tangem.operations.backup.BackupService -import com.tangem.sdk.api.CreateProductWalletTaskResponse -import com.tangem.sdk.extensions.localizedDescriptionRes -import com.tangem.tap.* -import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.features.onboarding.OnboardingHelper -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.wallet.R -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware - -object OnboardingWalletMiddleware { - val handler = onboardingWalletMiddleware -} - -private const val HIDE_PROGRESS_DELAY = 400L - -private val onboardingWalletMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - when (action) { - is OnboardingWallet2Action -> handleWallet2Action(action) - else -> handleWalletAction(action) - } - next(action) - } - } -} - -@Suppress("LongMethod", "ComplexMethod") -private fun handleWalletAction(action: Action) { - if (action !is OnboardingWalletAction) return - - val globalState = store.state.globalState - val onboardingManager = globalState.onboardingState.onboardingManager - - val scanResponse = onboardingManager?.scanResponse - val card = scanResponse?.card - - val onboardingWalletState = store.state.onboardingWalletState - - when (action) { - OnboardingWalletAction.Init -> { - ifNotNull(onboardingManager, card) { manager, notNullCard -> - mainScope.launch { - if (!manager.isActivationStarted(notNullCard.cardId)) { - Analytics.send(Onboarding.Started()) - } - } - } - when { - card == null -> { - // it's possible when found unfinished backup - store.dispatch(OnboardingWalletAction.ResumeBackup) - } - card.wallets.isNotEmpty() && card.backupStatus == CardDTO.BackupStatus.NoBackup -> { - store.dispatch(OnboardingWalletAction.ResumeBackup) - } - card.wallets.isNotEmpty() && card.backupStatus?.isActive == true -> { - store.dispatch(BackupAction.FinishBackup()) - } - else -> { - store.dispatch(OnboardingWalletAction.GetToCreateWalletStep) - Analytics.send(Onboarding.CreateWallet.ScreenOpened()) - } - } - } - is OnboardingWalletAction.LoadArtwork -> { - scope.launch { - val cardArtwork = when (onboardingManager) { - null -> { - // onboardingManager is null when backup started from previously interrupted - val primaryCardId = backupService.primaryCardId - val cardPublicKey = backupService.primaryPublicKey - if (primaryCardId != null && cardPublicKey != null) { - // uses when no scanResponse and backup state restored - loadArtworkForCard( - cardId = primaryCardId, - cardPublicKey = cardPublicKey, - defaultArtwork = action.cardArtworkUriForUnfinishedBackup, - ) - } else { - action.cardArtworkUriForUnfinishedBackup - } - } - else -> onboardingManager.loadArtworkUrl() - .takeIf { it != Artwork.DEFAULT_IMG_URL } - ?.let { Uri.parse(it) } - } - store.dispatchOnMain(OnboardingWalletAction.SetPrimaryCardArtworkUrl(cardArtwork)) - } - } - is OnboardingWalletAction.CreateWallet -> { - scanResponse ?: return - scope.launch { - val result = tangemSdkManager.createProductWallet( - scanResponse, - globalState.onboardingState.shouldResetOnCreate, - ) - store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(true, result)) - } - } - is OnboardingWalletAction.WalletWasCreated -> { - scanResponse ?: return - when (val result = action.result) { - is CompletionResult.Success -> { - if (action.shouldSendAnalyticsEvent) { - Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) - } - // here we must use updated scanResponse after createWallet & derivation - val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy( - card = result.data.card, - derivedKeys = result.data.derivedKeys, - primaryCard = result.data.primaryCard, - ) - onboardingManager.scanResponse = updatedResponse - - store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) - mainScope.launch { - onboardingManager.startActivation(updatedResponse.card.cardId) - store.dispatchWithMain(OnboardingWalletAction.ResumeBackup) - } - } - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.WalletAlreadyCreated) { - handleActivationError() - } - } - } - } - is OnboardingWalletAction.FinishOnboarding -> { - store.dispatch(GlobalAction.Onboarding.Stop) - navigateToWalletScreen() - store.dispatch(BackupAction.DiscardBackup) - } - is OnboardingWalletAction.ResumeBackup -> { - val newAction = when (val backupState = backupService.currentState) { - BackupService.State.FinalizingPrimaryCard -> BackupAction.PrepareToWritePrimaryCard - is BackupService.State.FinalizingBackupCard -> BackupAction.PrepareToWriteBackupCard(backupState.index) - else -> { - if (onboardingWalletState.backupState.backupStep == null || - onboardingWalletState.backupState.backupStep == BackupStep.InitBackup || - onboardingWalletState.backupState.backupStep == BackupStep.Finished - ) { - Analytics.send(Onboarding.Backup.ScreenOpened()) - - val isWallet2 = scanResponse?.cardTypesResolver?.isWallet2() ?: false - if (isWallet2) BackupAction.StartBackup else BackupAction.IntroduceBackup - } else { - null - } - } - } - newAction?.let { store.dispatch(it) } - } - OnboardingWalletAction.OnBackPressed -> handleOnBackPressed(onboardingWalletState) - else -> Unit - } -} - -private suspend fun handleFinishBackup(scanResponse: ScanResponse, userWallet: UserWallet? = null) { - val backupState = store.state.onboardingWalletState.backupState - val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState) - - val userWalletId = userWallet?.walletId ?: UserWalletIdBuilder.scanResponse(scanResponse).build() - if (backupState.hasRing && userWalletId != null) { - scope.launch { - store.inject(DaggerGraphState::walletsRepository).setHasWalletsWithRing(userWalletId = userWalletId) - } - } - - OnboardingHelper.saveWallet( - alreadyCreatedWallet = userWallet, - scanResponse = updatedScanResponse, - accessCode = backupState.accessCode, - backupCardsIds = backupState.backupCardIds, - hasBackupError = backupState.hasBackupError, - ) -} - -private fun navigateToWalletScreen() { - mainScope.launch { - val settingsRepository = store.inject(DaggerGraphState::settingsRepository) - store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } - if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) { - delay(timeMillis = 1_800) - store.dispatchNavigationAction { push(AppRoute.SaveWallet) } - } - } -} - -private suspend fun readCard(): CompletionResult { - val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes() - - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = shouldSaveAccessCodes, - ) - - return store.inject(DaggerGraphState::scanCardProcessor).scan(analyticsSource = ScreensSources.Intro) -} - -private suspend fun loadArtworkForCard(cardId: String, cardPublicKey: ByteArray, defaultArtwork: Uri?): Uri { - val onlineCardVerifier = store.inject(DaggerGraphState::onlineCardVerifier) - - return when (val cardInfo = onlineCardVerifier.getCardInfo(cardId, cardPublicKey)) { - is Result.Success -> { - val artworkId = cardInfo.data.artwork?.id - if (artworkId.isNullOrEmpty()) { - defaultArtwork ?: Uri.EMPTY - } else { - Uri.parse(CardArtworksProvider.getUrlForArtwork(cardId, cardPublicKey.toHexString(), artworkId)) - } - } - is Result.Failure -> defaultArtwork ?: Uri.EMPTY - } -} - -@Suppress("LongMethod", "ComplexMethod") -private fun handleWallet2Action(action: OnboardingWallet2Action) { - val globalState = store.state.globalState - val onboardingManager = globalState.onboardingState.onboardingManager - - val scanResponse = onboardingManager?.scanResponse - - when (action) { - is OnboardingWallet2Action.Init -> { - if (scanResponse?.cardTypesResolver?.isWallet2() == true) { - store.dispatch(OnboardingWallet2Action.SetDependencies(action.maxProgress)) - } - } - - is OnboardingWallet2Action.CreateWallet -> { - scanResponse ?: return - scope.launch { - val mediateResult = when ( - val result = tangemSdkManager.createProductWallet( - scanResponse, - globalState.onboardingState.shouldResetOnCreate, - ) - ) { - is CompletionResult.Success -> { - Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully()) - store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) - val response = CreateWalletResponse( - card = result.data.card, - derivedKeys = result.data.derivedKeys, - primaryCard = result.data.primaryCard, - ) - CompletionResult.Success(response) - } - - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.WalletAlreadyCreated) { - handleActivationError() - } - CompletionResult.Failure(result.error) - } - } - withMainContext { - action.callback(mediateResult) - } - } - } - - is OnboardingWallet2Action.ImportWallet -> { - scanResponse ?: return - scope.launch { - val mediateResult = when ( - val result = tangemSdkManager.importWallet( - scanResponse = scanResponse, - passphrase = action.passphrase, - mnemonic = action.mnemonicComponents.joinToString(" "), - shouldReset = globalState.onboardingState.shouldResetOnCreate, - ) - ) { - is CompletionResult.Success -> { - val creationType = when (action.seedPhraseSource) { - SeedPhraseSource.IMPORTED -> AnalyticsParam.WalletCreationType.SeedImport - SeedPhraseSource.GENERATED -> AnalyticsParam.WalletCreationType.NewSeed - } - Analytics.send( - event = Onboarding.CreateWallet.WalletCreatedSuccessfully( - creationType = creationType, - seedPhraseLength = action.mnemonicComponents.size, - ), - ) - store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(false)) - val response = CreateWalletResponse( - card = result.data.card, - derivedKeys = result.data.derivedKeys, - primaryCard = result.data.primaryCard, - ) - CompletionResult.Success(response) - } - - is CompletionResult.Failure -> { - if (result.error is TangemSdkError.WalletAlreadyCreated) { - handleActivationError() - } - CompletionResult.Failure(result.error) - } - } - withMainContext { - action.callback(mediateResult) - } - } - } - - is OnboardingWallet2Action.WalletWasCreated -> { - val result = when (val mediateResult = action.result) { - is CompletionResult.Success -> { - val response = CreateProductWalletTaskResponse( - card = mediateResult.data.card, - derivedKeys = mediateResult.data.derivedKeys, - primaryCard = mediateResult.data.primaryCard, - ) - CompletionResult.Success(response) - } - - is CompletionResult.Failure -> { - CompletionResult.Failure(mediateResult.error) - } - } - // do not send analytics event for wallet2 flow, cause its already sent - store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(false, result)) - } - - else -> Unit - } -} - -private fun handleActivationError() { - store.dispatchDialogShow( - OnboardingDialog.WalletActivationError( - onConfirm = { - store.dispatch(GlobalAction.Onboarding.ShouldResetCardOnCreate(true)) - }, - ), - ) -} - -private fun updateScanResponseAfterBackup(scanResponse: ScanResponse, backupState: BackupState): ScanResponse { - val card = if (backupState.backupCardsNumber > 0) { - val cardsCount = backupState.backupCardsNumber - scanResponse.card.copy( - backupStatus = CardDTO.BackupStatus.Active(cardCount = cardsCount), - isAccessCodeSet = true, - ) - } else { - scanResponse.card - } - return scanResponse.copy(card = card) -} - -class BackupMiddleware { - val backupMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - if (action is BackupAction) handleBackupAction(state, action) - next(action) - } - } - } -} - -@Suppress("LongMethod", "ComplexMethod", "MagicNumber") -private fun handleBackupAction(appState: () -> AppState?, action: BackupAction) { - if (DemoHelper.tryHandle(appState, action)) return - val globalState = appState()?.globalState ?: return - val onboardingWalletState = appState()?.onboardingWalletState ?: return - - val backupState = onboardingWalletState.backupState - var scanResponse = globalState.onboardingState.onboardingManager?.scanResponse - val card = scanResponse?.card - - when (action) { - is BackupAction.StartBackup -> { - Analytics.send(Onboarding.Backup.Started()) - backupService.discardSavedBackup() - val primaryCard = scanResponse?.primaryCard - if (primaryCard != null) { - backupService.setPrimaryCard(primaryCard) - store.dispatch(BackupAction.StartAddingBackupCards) - } else { - store.dispatch(BackupAction.StartAddingPrimaryCard) - } - } - is BackupAction.ScanPrimaryCard -> { - val iconScanRes = if (scanResponse?.cardTypesResolver?.isRing() == true) { - R.drawable.img_hand_scan_ring - } else { - null - } - backupService.readPrimaryCard(iconScanRes = iconScanRes, cardId = card?.cardId) { result -> - when (result) { - is CompletionResult.Success -> { - store.dispatchOnMain(BackupAction.StartAddingBackupCards) - } - is CompletionResult.Failure -> Unit - } - } - } - is BackupAction.AddBackupCard -> { - store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(true)) - backupService.addBackupCard { result -> - backupService.skipCompatibilityChecks = false - store.inject(DaggerGraphState::cardSdkConfigRepository).sdk.config.filter.cardIdFilter = null - store.dispatchOnMain(BackupAction.AddBackupCard.ChangeButtonLoading(false)) - when (result) { - is CompletionResult.Success -> { - updateArtworks(backupService.addedBackupCardsCount, result.data) - store.dispatchOnMain(BackupAction.AddBackupCard.Success(result.data)) - } - is CompletionResult.Failure -> { - when (val error = result.error) { - is TangemSdkError.CardVerificationFailed -> { - Analytics.send( - event = OnboardingAnalyticsEvent.Onboarding.OfflineAttestationFailed( - ScreensSources.Backup, - ), - ) - - val resource = error.localizedDescriptionRes() - val resId = resource.resId ?: com.tangem.core.ui.R.string.common_unknown_error - val resArgs = resource.args.map { it.value } - - store.inject(DaggerGraphState::uiMessageSender).send( - message = Dialogs.cardVerificationFailed( - errorDescription = resourceReference(id = resId, resArgs.toWrappedList()), - onRequestSupport = { - mainScope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.CardAttestationFailed) - } - }, - ), - ) - } - is TangemSdkError.BackupFailedNotEmptyWallets -> { - store.dispatchOnMain( - GlobalAction.ShowDialog( - BackupDialog.ResetBackupCard(error.cardId), - ), - ) - } - is TangemSdkError.IssuerSignatureLoadingFailed -> { - store.dispatchOnMain( - GlobalAction.ShowDialog(BackupDialog.AttestationFailed), - ) - } - else -> Analytics.sendException(ExceptionAnalyticsEvent(error)) - } - } - } - } - } - is BackupAction.FinishAddingBackupCards -> { - if (backupService.addedBackupCardsCount == backupState.maxBackupCards) { - store.dispatchOnMain(BackupAction.ShowAccessCodeInfoScreen) - } else { - store.dispatchOnMain(GlobalAction.ShowDialog(BackupDialog.AddMoreBackupCards)) - } - } - is BackupAction.ShowEnterAccessCodeScreen -> { - Analytics.send(Onboarding.Backup.SettingAccessCodeStarted()) - } - is BackupAction.CheckAccessCode -> { - Analytics.send(Onboarding.Backup.AccessCodeEntered()) - if (action.accessCode.length < 4) { - store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodeTooShort)) - } else { - store.dispatch(BackupAction.SetAccessCodeError(null)) - store.dispatch(BackupAction.SaveFirstAccessCode(action.accessCode)) - } - } - is BackupAction.SaveAccessCodeConfirmation -> { - Analytics.send(Onboarding.Backup.AccessCodeReEntered()) - if (action.accessCodeConfirmation == backupState.accessCode) { - store.dispatch(BackupAction.SetAccessCodeError(null)) - backupService.setAccessCode(action.accessCodeConfirmation) - store.dispatch(BackupAction.PrepareToWritePrimaryCard) - } else { - store.dispatch(BackupAction.SetAccessCodeError(AccessCodeError.CodesDoNotMatch)) - } - } - is BackupAction.WritePrimaryCard -> { - val isRing = scanResponse?.cardTypesResolver?.isRing() == true - val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null - - store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing)) - - tangemSdkManager.changeProductType(isRing) - backupService.proceedBackup(iconScanRes = iconScanRes) { result -> - when (result) { - is CompletionResult.Success -> { - store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(1)) - } - is CompletionResult.Failure -> Unit - } - tangemSdkManager.clearProductType() - } - } - is BackupAction.WriteBackupCard -> { - val cardIndex = if (action.cardNumber > 0) action.cardNumber - 1 else action.cardNumber - val backupCard = backupState.backupCards.getOrNull(cardIndex) - val isRing = backupCard.isRing() - val iconScanRes = if (isRing) R.drawable.img_hand_scan_ring else null - - store.dispatchOnMain(BackupAction.SetHasRing(hasRing = isRing)) - - tangemSdkManager.changeProductType(isRing) - backupService.proceedBackup(iconScanRes = iconScanRes) { result -> - when (result) { - is CompletionResult.Success -> { - val backupValidator = BackupValidator() - if (!backupValidator.isValidBackupStatus(CardDTO(result.data))) { - store.dispatchOnMain(BackupAction.ErrorInBackupCard) - } - if (backupService.currentState == BackupService.State.Finished) { - store.dispatchOnMain(BackupAction.FinishBackup()) - } else { - store.dispatchOnMain(BackupAction.PrepareToWriteBackupCard(action.cardNumber + 1)) - } - } - is CompletionResult.Failure -> { - when (val error = result.error) { - is TangemSdkError.BackupFailedNotEmptyWallets -> { - store.dispatchOnMain( - GlobalAction.ShowDialog( - BackupDialog.ResetBackupCard(error.cardId), - ), - ) - } - } - } - } - - tangemSdkManager.clearProductType() - } - } - is BackupAction.DiscardBackup -> { - backupService.discardSavedBackup() - } - is BackupAction.DiscardSavedBackup -> { - mainScope.launch { - backupService.primaryCardId?.let { // always null for onboarding v2 - Analytics.send(Onboarding.Finished()) - store.state.globalState.onboardingState.onboardingManager?.finishActivation(it) - } - backupService.discardSavedBackup() - - val isOnboardingV2Enabled = store.inject(DaggerGraphState::onboardingV2FeatureToggles) - .isOnboardingV2Enabled - - if (isOnboardingV2Enabled) { - val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository) - val cardRepository = store.inject(DaggerGraphState::cardRepository) - val unfinishedBackup = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch - - cardRepository.finishCardActivation(unfinishedBackup.card.cardId) - onboardingRepository.clearUnfinishedFinalizeOnboarding() - Analytics.send(Onboarding.Finished()) - } - } - } - is BackupAction.CheckForUnfinishedBackup -> { - val isOnboardingV2Enabled = store.inject(DaggerGraphState::onboardingV2FeatureToggles).isOnboardingV2Enabled - - if (isOnboardingV2Enabled) { - val onboardingRepository = store.inject(DaggerGraphState::onboardingRepository) - - mainScope.launch { - val onboardingScanResponse = onboardingRepository.getUnfinishedFinalizeOnboarding() ?: return@launch - store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound(onboardingScanResponse))) - } - } else if (backupService.hasIncompletedBackup) { - store.dispatch(GlobalAction.ShowDialog(BackupDialog.UnfinishedBackupFound())) - } - } - is BackupAction.ResumeFoundUnfinishedBackup -> { - if (action.unfinishedBackupScanResponse != null) { - // onboarding V2 - store.dispatchNavigationAction { - push( - AppRoute.Onboarding( - scanResponse = action.unfinishedBackupScanResponse, - mode = AppRoute.Onboarding.Mode.ContinueFinalize, - ), - ) - } - } else { - store.dispatch( - GlobalAction.Onboarding.StartForUnfinishedBackup( - addedBackupCardsCount = backupService.addedBackupCardsCount, - ), - ) - - store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) } - } - } - is BackupAction.SkipBackup -> { - Analytics.send(Onboarding.Backup.Skipped()) - Analytics.send(Onboarding.Finished()) - - scope.launch { - launch { - store.state.globalState.onboardingState.onboardingManager?.finishActivation( - cardIds = gatherCardIds(backupState, card), - ) - } - - with(scanResponse) { - if (this == null) { - when (val result = readCard()) { - is CompletionResult.Success -> handleFinishBackup(result.data) - is CompletionResult.Failure -> store.dispatchNavigationAction(AppRouter::pop) - } - } else { - handleFinishBackup(scanResponse = this) - } - } - } - } - is BackupAction.FinishBackup -> { - scope.launch { - if (action.withAnalytics) { - Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber)) - } - - var userWallet: UserWallet? = null - if (scanResponse != null) { - scanResponse = updateScanResponseAfterBackup(scanResponse!!, backupState) - userWallet = createUserWallet( - scanResponse = requireNotNull(value = scanResponse, lazyMessage = { "ScanResponse is null" }), - backupState = backupState, - ) - } else { - delay(HIDE_PROGRESS_DELAY) - - when (val result = readCard()) { - is CompletionResult.Failure -> { - store.dispatchNavigationAction(AppRouter::pop) - return@launch - } - is CompletionResult.Success -> { - scanResponse = result.data - userWallet = createUserWallet(scanResponse = result.data, backupState = backupState) - } - } - } - - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - when (backupState.startedSource) { - BackupStartedSource.Onboarding -> saveWallet( - userWalletsListManager = userWalletsListManager, - userWallet = userWallet, - scanResponse = scanResponse, - backupState = backupState, - ) - BackupStartedSource.CreateBackup -> updateWallet( - userWalletsListManager = userWalletsListManager, - userWallet = userWallet, - backupState = backupState, - ) - } - - scope.launch { - if (userWallet.scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported) { - store.inject(DaggerGraphState::walletsRepository).markWallet2WasCreated(userWallet.walletId) - } - } - - val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull { - if (store.state.globalState.onboardingState.onboardingManager?.isActivationFinished(it) == true) { - null - } else { - it - } - } - - // All cardIds may already be activated if the backup was skipped before. - if (notActivatedCardIds.isEmpty()) { - delay(1000) - store.dispatchWithMain(BackupAction.BackupFinished(userWallet.walletId)) - return@launch - } - - Analytics.send(Onboarding.Finished()) - - store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds) - handleFinishBackup(requireNotNull(scanResponse), userWallet) - store.dispatchWithMain(BackupAction.BackupFinished(userWalletId = userWallet.walletId)) - } - } - - is BackupAction.ResetBackupCard -> { - scope.launch { tangemSdkManager.resetToFactorySettings(action.cardId, false) } - } - - else -> Unit - } -} - -private fun Card?.isRing(): Boolean { - return this?.let { RING_BATCH_IDS.contains(batchId) || batchId.startsWith(RING_BATCH_PREFIX) } ?: false -} - -private suspend fun saveWallet( - userWalletsListManager: UserWalletsListManager, - userWallet: UserWallet, - scanResponse: ScanResponse?, - backupState: BackupState, -) { - userWalletsListManager.save( - userWallet = userWallet.copy( - scanResponse = updateScanResponseAfterBackup( - scanResponse = requireNotNull( - value = scanResponse, - lazyMessage = { "ScanResponse is null" }, - ), - backupState = backupState, - ), - ), - canOverride = true, - ) -} - -private suspend fun updateWallet( - userWalletsListManager: UserWalletsListManager, - userWallet: UserWallet, - backupState: BackupState, -) { - userWalletsListManager.update( - userWalletId = userWallet.walletId, - update = { wallet -> - wallet.copy( - scanResponse = updateScanResponseAfterBackup( - scanResponse = wallet.scanResponse, - backupState = backupState, - ), - ) - }, - ) -} - -private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: BackupState): UserWallet { - val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory).create(scanResponse) - - return requireNotNull( - value = userWalletBuilder - .backupCardsIds(backupState.backupCardIds.toSet()) - .hasBackupError(backupState.hasBackupError) - .build(), - lazyMessage = { "User wallet not created" }, - ) -} - -fun updateArtworks(addedBackupCardsCount: Int, card: Card) { - mainScope.launch { - withIOContext { - val imageUri = loadArtworkForCard(card.cardId, card.cardPublicKey, Uri.EMPTY) - when (addedBackupCardsCount) { - 1 -> { - store.dispatchOnMain(OnboardingWalletAction.SetSecondCardArtworkUrl(imageUri)) - } - 2 -> { - store.dispatchOnMain(OnboardingWalletAction.SetThirdCardArtworkUrl(imageUri)) - } - } - } - } -} - -internal fun gatherCardIds(backupState: BackupState, card: CardDTO?): List { - return (listOf(backupState.primaryCardId, card?.cardId) + backupState.backupCardIds) - .filterNotNull() - .distinct() -} - -private fun handleOnBackPressed(state: OnboardingWalletState) { - when (state.backupState.backupStep) { - null, BackupStep.InitBackup, BackupStep.ScanOriginCard, BackupStep.AddBackupCards, BackupStep.EnterAccessCode, - BackupStep.ReenterAccessCode, BackupStep.SetAccessCode, BackupStep.WritePrimaryCard, - -> { - showInterruptOnboardingDialog() - } - is BackupStep.WriteBackupCard -> { - store.dispatch(GlobalAction.ShowDialog(BackupDialog.BackupInProgress)) - } - BackupStep.Finished -> { - OnboardingHelper.onInterrupted() - store.dispatchNavigationAction(AppRouter::pop) - } - } -} - -private fun showInterruptOnboardingDialog() { - store.dispatchDialogShow( - OnboardingDialog.InterruptOnboarding( - onOk = { - OnboardingHelper.onInterrupted() - store.dispatch(BackupAction.DiscardBackup) - store.dispatchNavigationAction(AppRouter::pop) - }, - ), - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt deleted file mode 100644 index d34a661c97..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.redux - -import com.tangem.domain.redux.OnboardingManageTokensAction -import com.tangem.tap.backupService -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction -import org.rekotlin.Action - -object OnboardingWalletReducer { - fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state) -} - -@Suppress("CyclomaticComplexMethod") -private fun internalReduce(action: Action, appState: AppState): OnboardingWalletState { - val state = appState.onboardingWalletState - val backupState = state.backupState - - return when (action) { - is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state) - is BackupAction.BackupFinished -> { - state.copy( - step = if (action.userWalletId != null && backupState.startedSource == BackupStartedSource.Onboarding) { - OnboardingWalletStep.ManageTokens - } else { - OnboardingWalletStep.Done - }, - userWalletId = action.userWalletId, - ) - } - is BackupAction -> state.copy(backupState = BackupReducer.reduce(action = action, state = state.backupState)) - is OnboardingWallet2Action -> OnboardingWallet2Reducer.reduce(action, state) - is OnboardingWalletAction.GetToCreateWalletStep -> state.copy( - step = OnboardingWalletStep.CreateWallet, - ) - is OnboardingWalletAction.ResumeBackup -> state.copy(step = OnboardingWalletStep.Backup) - is OnboardingWalletAction.SetPrimaryCardArtworkUrl -> { - state.copy( - walletImages = state.walletImages.copy(primaryCardImage = action.artworkUri), - ) - } - is OnboardingWalletAction.SetSecondCardArtworkUrl -> { - state.copy( - walletImages = state.walletImages.copy(secondCardImage = action.artworkUri), - ) - } - is OnboardingWalletAction.SetThirdCardArtworkUrl -> { - state.copy( - walletImages = state.walletImages.copy(thirdCardImage = action.artworkUri), - ) - } - is OnboardingManageTokensAction.CurrenciesSaved -> state.copy(step = OnboardingWalletStep.Done) - is OnboardingWalletAction.WalletSaved -> state.copy( - step = when (backupState.startedSource) { - BackupStartedSource.Onboarding -> OnboardingWalletStep.ManageTokens - BackupStartedSource.CreateBackup -> OnboardingWalletStep.Done - }, - userWalletId = action.userWalletId, - ) - is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done) - else -> state - } -} - -private object ReducerForGlobalAction { - - private const val MAX_BACKUP_CARDS = 2 - - fun reduce(action: GlobalAction.Onboarding, state: OnboardingWalletState): OnboardingWalletState { - return when (action) { - is GlobalAction.Onboarding.Start -> { - OnboardingWalletState( - backupState = state.backupState.copy( - maxBackupCards = MAX_BACKUP_CARDS, - canSkipBackup = action.canSkipBackup, - startedSource = action.source, - ), - ) - } - is GlobalAction.Onboarding.StartForUnfinishedBackup -> { - OnboardingWalletState( - backupState = state.backupState.copy( - maxBackupCards = action.addedBackupCardsCount, - canSkipBackup = false, - isInterruptedBackup = true, - ), - ) - } - else -> state - } - } -} - -private object OnboardingWallet2Reducer { - fun reduce(action: OnboardingWallet2Action, state: OnboardingWalletState): OnboardingWalletState = when (action) { - is OnboardingWallet2Action.SetDependencies -> state.copy( - wallet2State = OnboardingWallet2State( - maxProgress = action.maxProgress, - ), - ) - else -> state - } -} - -private object BackupReducer { - @Suppress("ComplexMethod", "LongMethod") - fun reduce(action: BackupAction, state: BackupState): BackupState { - return when (action) { - is BackupAction.IntroduceBackup -> BackupState( - backupStep = BackupStep.InitBackup, - maxBackupCards = 2, - canSkipBackup = state.canSkipBackup, - startedSource = state.startedSource, - ) - BackupAction.StartAddingPrimaryCard -> { - state.copy( - backupStep = BackupStep.ScanOriginCard, - maxBackupCards = 2, - canSkipBackup = state.canSkipBackup, - ) - } - BackupAction.StartAddingBackupCards -> { - state.copy( - backupStep = BackupStep.AddBackupCards, - maxBackupCards = 2, - canSkipBackup = state.canSkipBackup, - ) - } - is BackupAction.AddBackupCard.Success -> { - state.copy( - backupCards = state.backupCards + action.card, - backupCardsNumber = state.backupCardsNumber + 1, - ) - } - is BackupAction.AddBackupCard.ChangeButtonLoading -> state.copy(showBtnLoading = action.isLoading) - BackupAction.ShowAccessCodeInfoScreen -> state.copy(backupStep = BackupStep.SetAccessCode) - BackupAction.ShowEnterAccessCodeScreen -> state.copy( - backupStep = BackupStep.EnterAccessCode, - accessCodeError = null, - ) - is BackupAction.SaveFirstAccessCode -> state.copy( - backupStep = BackupStep.ReenterAccessCode, - accessCode = action.accessCode, - ) - is BackupAction.SetAccessCodeError -> state.copy(accessCodeError = action.error) - is BackupAction.PrepareToWritePrimaryCard -> if (state.primaryCardId == null) { - state.copy( - primaryCardId = backupService.primaryCardId, - primaryCardBatchId = backupService.primaryCardBatchId, - backupCardIds = backupService.backupCardIds, - backupCardBatchIds = backupService.backupCardsBatchIds, - backupCardsNumber = backupService.backupCardIds.size, - backupStep = BackupStep.WritePrimaryCard, - ) - } else { - state.copy(backupStep = BackupStep.WritePrimaryCard) - } - is BackupAction.PrepareToWriteBackupCard -> if (state.primaryCardId == null) { - state.copy( - primaryCardId = backupService.primaryCardId, - primaryCardBatchId = backupService.primaryCardBatchId, - backupCardIds = backupService.backupCardIds, - backupCardBatchIds = backupService.backupCardsBatchIds, - backupCardsNumber = backupService.backupCardIds.size, - backupStep = BackupStep.WriteBackupCard(action.cardNumber), - ) - } else { - state.copy(backupStep = BackupStep.WriteBackupCard(action.cardNumber)) - } - is BackupAction.ErrorInBackupCard -> state.copy(hasBackupError = true) - is BackupAction.SkipBackup -> state.copy(backupStep = BackupStep.Finished) - is BackupAction.FinishBackup -> state.copy(backupStep = BackupStep.Finished) - BackupAction.OnAccessCodeDialogClosed -> state.copy(backupStep = BackupStep.AddBackupCards) - BackupAction.DiscardBackup -> BackupState() - is BackupAction.SetHasRing -> { - // Don't update if state.has ring is already true - if (state.hasRing) { - state - } else { - state.copy(hasRing = action.hasRing) - } - } - else -> state - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt deleted file mode 100644 index 8d8f92f601..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletState.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.redux - -import android.graphics.Bitmap -import android.net.Uri -import com.tangem.common.card.Card -import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.StateDialog -import com.tangem.domain.wallets.models.UserWalletId -import org.rekotlin.StateType - -/** -[REDACTED_AUTHOR] - */ -data class OnboardingWalletState( - val step: OnboardingWalletStep = OnboardingWalletStep.None, - val wallet2State: OnboardingWallet2State? = null, - val backupState: BackupState = BackupState(), - val walletImages: WalletImages = WalletImages(), - val showConfetti: Boolean = false, - val isRingOnboarding: Boolean = false, - val userWalletId: UserWalletId? = null, -) : StateType { - - @Suppress("MagicNumber") - fun getMaxProgress(): Int { - val baseProgress = 7 - return getWallet2Progress() + baseProgress - } - - @Suppress("ComplexMethod", "MagicNumber") - fun getProgressStep(): Int { - val progressByStep = when (step) { - OnboardingWalletStep.CreateWallet, OnboardingWalletStep.None -> 1 - OnboardingWalletStep.Backup -> { - when (backupState.backupStep) { - null -> 2 - BackupStep.InitBackup -> 2 - BackupStep.ScanOriginCard -> 3 - BackupStep.AddBackupCards -> 4 - BackupStep.EnterAccessCode -> 4 - BackupStep.ReenterAccessCode -> 4 - BackupStep.SetAccessCode -> 4 - BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5 - BackupStep.Finished -> 6 - } - } - OnboardingWalletStep.ManageTokens -> 6 - OnboardingWalletStep.Done -> getMaxProgress() - } - - return getWallet2Progress() + progressByStep - } - - private fun getWallet2Progress(): Int = wallet2State?.maxProgress ?: 0 -} - -data class WalletImages( - val primaryCardImage: Uri? = null, - val secondCardImage: Uri? = null, - val thirdCardImage: Uri? = null, -) - -data class OnboardingWallet2State( - val maxProgress: Int, -) - -enum class OnboardingWalletStep { - None, CreateWallet, Backup, ManageTokens, Done -} - -data class BackupState( - val primaryCardId: String? = null, - val primaryCardBatchId: String? = null, - val backupCardsNumber: Int = 0, - val backupCards: List = emptyList(), - val backupCardIds: List = emptyList(), - val backupCardBatchIds: List = emptyList(), - @Transient - val backupCardsArtworks: Map = emptyMap(), - val accessCode: String = "", - val accessCodeError: AccessCodeError? = null, - val backupStep: BackupStep? = null, - val maxBackupCards: Int = 2, - val canSkipBackup: Boolean = true, - val isInterruptedBackup: Boolean = false, - val showBtnLoading: Boolean = false, - val hasBackupError: Boolean = false, - val startedSource: BackupStartedSource = BackupStartedSource.Onboarding, - val hasRing: Boolean = false, -) - -enum class BackupStartedSource { - Onboarding, CreateBackup -} - -enum class AccessCodeError { - CodeTooShort, CodesDoNotMatch -} - -typealias CardId = String - -sealed class BackupStep { - data object InitBackup : BackupStep() - data object ScanOriginCard : BackupStep() - data object AddBackupCards : BackupStep() - data object SetAccessCode : BackupStep() - data object EnterAccessCode : BackupStep() - data object ReenterAccessCode : BackupStep() - data object WritePrimaryCard : BackupStep() - data class WriteBackupCard(val cardNumber: Int) : BackupStep() - data object Finished : BackupStep() -} - -sealed class BackupDialog : StateDialog { - data object AttestationFailed : BackupDialog() - data object AddMoreBackupCards : BackupDialog() - data object BackupInProgress : BackupDialog() - - data class UnfinishedBackupFound( - val scanResponse: ScanResponse? = null, - ) : BackupDialog() - - data class ConfirmDiscardingBackup( - val scanResponse: ScanResponse? = null, - ) : BackupDialog() - - data class ResetBackupCard(val cardId: String) : BackupDialog() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt deleted file mode 100644 index 0927376b54..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupAnimator.kt +++ /dev/null @@ -1,314 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui - -import com.google.android.material.button.MaterialButton -import com.tangem.tap.common.extensions.show -import com.tangem.tap.common.postUiDelayBg -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState -import com.tangem.tap.features.onboarding.products.wallet.ui.BackupCardType.* -import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding - -/** -[REDACTED_AUTHOR] - */ -interface BackupAnimator { - fun updateBackupState(backupState: BackupState) - fun setupCreateWalletState() - fun showBackupIntro(state: BackupState) - fun showScanOriginCard() - fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) - fun showWritePrimaryCard(state: BackupState) - fun showWriteBackupCard(state: BackupState, backupCard: Int) - fun showSuccess(onEnd: () -> Unit) -} - -class WalletBackupAnimator( - private val cardsWidget: WalletCardsWidget, -) : BackupAnimator { - - private val viewState: State = when (cardsWidget.leapfrogWidget.getViewsCount()) { - 2 -> State.TwoCards - 3 -> State.ThreeCards - else -> throw UnsupportedOperationException() - } - - private var currentStep = -1 - private var firstBackupCardAnimated = false - private var secondBackupCardAnimated = false - - override fun updateBackupState(backupState: BackupState) { - } - - override fun showBackupIntro(state: BackupState) { - currentStep = STEP_BACKUP_INTRO - - ORIGIN.show() - FIRST_BACKUP.show() - SECOND_BACKUP.show() - - cardsWidget.toWelcome() - } - - override fun setupCreateWalletState() { - currentStep = STEP_CREATE_WALLET - - cardsWidget.toFolded(false) - - ORIGIN.alpha(1f) - FIRST_BACKUP.alpha(1f) - SECOND_BACKUP.alpha(1f) - } - - override fun showScanOriginCard() { - currentStep = STEP_SCAN_ORIGIN_CARD - - cardsWidget.toFolded() - } - - @Suppress("MagicNumber") - override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) { - when (backupCardsCounts) { - 0 -> { - when (currentStep) { - UNDEFINED, STEP_CREATE_WALLET -> { - cardsWidget.toFolded(false) { cardsWidget.toFan() } - - FIRST_BACKUP.animateAlpha(0.6f, 0) - SECOND_BACKUP.animateAlpha(0.2f, 400) - } - STEP_BACKUP_INTRO -> { - cardsWidget.toFolded(duration = 300) { cardsWidget.toFan() } - FIRST_BACKUP.animateAlpha(0.6f, 400, 250) - SECOND_BACKUP.animateAlpha(0.2f, 400, 250) - } - STEP_SCAN_ORIGIN_CARD -> { - cardsWidget.toFan() - FIRST_BACKUP.animateAlpha(0.6f, 400, 250) - SECOND_BACKUP.animateAlpha(0.2f, 400, 250) - } - } - } - 1 -> { - cardsWidget.toFan(false) - FIRST_BACKUP.alpha(1f) - SECOND_BACKUP.alpha(0.2f) - } - 2 -> { - cardsWidget.toFan(false) - FIRST_BACKUP.alpha(1f) - SECOND_BACKUP.alpha(1f) - } - } - - currentStep = STEP_ADD_BACKUP_CARDS - } - - @Suppress("MagicNumber") - override fun showWritePrimaryCard(state: BackupState) { - currentStep = STEP_WRITE_PRIMARY_CARD - - cardsWidget.toLeapfrog( - onEndFold = { - FIRST_BACKUP.alpha(0.4f) - SECOND_BACKUP.alpha(0.2f) - }, - ) - } - - @Suppress("MagicNumber") - override fun showWriteBackupCard(state: BackupState, backupCard: Int) { - when (viewState) { - State.TwoCards -> { - if (firstBackupCardAnimated) return - firstBackupCardAnimated = true - - val delay = if (currentStep == UNDEFINED) 700L else 0 - postUiDelayBg(delay) { - FIRST_BACKUP.alpha(1f) - cardsWidget.leapfrogWidget.leap { - ORIGIN.alpha(0.4f) - } - } - } - State.ThreeCards -> { - when (backupCard) { - 1 -> { - if (firstBackupCardAnimated) return - firstBackupCardAnimated = true - - FIRST_BACKUP.alpha(1f) - cardsWidget.leapfrogWidget.leap { - SECOND_BACKUP.alpha(0.4f) - ORIGIN.alpha(0.2f) - } - } - 2 -> { - if (secondBackupCardAnimated) return - secondBackupCardAnimated = true - - /* - * !!! Workaround !!! - * For interrupted backup - * */ - if (!firstBackupCardAnimated) { - showWriteBackupCard(state, backupCard = 1) - return - } - - SECOND_BACKUP.alpha(1f) - cardsWidget.leapfrogWidget.leap { - ORIGIN.alpha(0.4f) - FIRST_BACKUP.alpha(0.2f) - } - } - } - } - } - currentStep = STEP_WRITE_BACKUP_CARD - } - - override fun showSuccess(onEnd: () -> Unit) { - currentStep = STEP_SUCCESS - cardsWidget.leapfrogWidget.fold(onEnd = onEnd) - } - - private enum class State { - ThreeCards, - TwoCards, - } - - private fun BackupCardType.animateAlpha(alpha: Float, duration: Long = 0L, startDelay: Long = 0L) { - when (this) { - ORIGIN -> { - cardsWidget.getOriginCardView().animate().apply { - alpha(alpha) - this.startDelay = startDelay - this.duration = duration - } - } - FIRST_BACKUP -> { - cardsWidget.getFirstBackupCardView().animate().apply { - alpha(alpha) - this.startDelay = startDelay - this.duration = duration - } - } - SECOND_BACKUP -> { - if (viewState != State.ThreeCards) return - cardsWidget.getSecondBackupCardView().animate().apply { - alpha(alpha) - this.startDelay = startDelay - this.duration = duration - } - } - } - } - - private fun BackupCardType.alpha(alpha: Float) { - when (this) { - ORIGIN -> cardsWidget.getOriginCardView().alpha = alpha - FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().alpha = alpha - SECOND_BACKUP -> { - if (viewState != State.ThreeCards) return - cardsWidget.getSecondBackupCardView().alpha = alpha - } - } - } - - private fun BackupCardType.show() { - when (this) { - ORIGIN -> cardsWidget.getOriginCardView().show() - FIRST_BACKUP -> cardsWidget.getFirstBackupCardView().show() - SECOND_BACKUP -> { - if (viewState != State.ThreeCards) return - cardsWidget.getSecondBackupCardView().show() - } - } - } - - companion object { - private const val UNDEFINED = -1 - private const val STEP_CREATE_WALLET = 0 - private const val STEP_BACKUP_INTRO = 1 - private const val STEP_SCAN_ORIGIN_CARD = 2 - private const val STEP_ADD_BACKUP_CARDS = 3 - private const val STEP_WRITE_PRIMARY_CARD = 4 - private const val STEP_WRITE_BACKUP_CARD = 5 - private const val STEP_SUCCESS = 6 - } -} - -class TestBackupAnimation( - private val animator: BackupAnimator, - binding: FragmentOnboardingWalletBinding, -) : BackupAnimator { - - private lateinit var state: BackupState - private var steps = 0 - - init { - init(binding, this) - } - - @Suppress("MagicNumber") - private fun setStep(step: Int, onStepUpdate: (Int) -> Unit = {}) { - steps = step - when (steps) { - 0 -> setupCreateWalletState() - 1 -> showBackupIntro(state) - 2 -> showScanOriginCard() - 3 -> showAddBackupCards(state, 0) - 4 -> showAddBackupCards(state, 1) - 5 -> showAddBackupCards(state, 2) - 6 -> showWritePrimaryCard(state) - 7 -> showWriteBackupCard(state, 1) - 8 -> showWriteBackupCard(state, 2) - else -> setStep(0) - } - onStepUpdate(steps) - } - - fun onNext(onStepUpdate: (Int) -> Unit) { - steps++ - setStep(steps, onStepUpdate) - } - - override fun updateBackupState(backupState: BackupState) { - this.state = backupState - } - - override fun setupCreateWalletState() { - animator.setupCreateWalletState() - } - - override fun showBackupIntro(state: BackupState) { - animator.showBackupIntro(state) - } - - override fun showScanOriginCard() { - animator.showScanOriginCard() - } - - override fun showAddBackupCards(state: BackupState, backupCardsCounts: Int) { - animator.showAddBackupCards(state, backupCardsCounts) - } - - override fun showWritePrimaryCard(state: BackupState) { - animator.showWritePrimaryCard(state) - } - - override fun showWriteBackupCard(state: BackupState, backupCard: Int) { - animator.showWriteBackupCard(state, backupCard) - } - - override fun showSuccess(onEnd: () -> Unit) { - animator.showSuccess(onEnd) - } - - companion object { - fun init(binding: FragmentOnboardingWalletBinding, animator: TestBackupAnimation) { - val button = MaterialButton(binding.onboardingRoot.context).apply { text = "Next" } - button.setOnClickListener { animator.onNext { button.text = "$it" } } - binding.onboardingRoot.addView(button, 0) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt deleted file mode 100644 index 6ea3bf1a25..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/BackupInfoAdapter.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.annotation.StringRes -import androidx.recyclerview.widget.RecyclerView -import com.tangem.tap.common.extensions.getString -import com.tangem.wallet.R -import com.tangem.wallet.databinding.ItemBackupInfoAdapterBinding - -class BackupInfoAdapter : RecyclerView.Adapter() { - - private val data = backupInfoSnippets - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BackupInfoViewHolder = BackupInfoViewHolder( - ItemBackupInfoAdapterBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false, - ), - ) - - override fun getItemCount(): Int = data.size - - override fun onBindViewHolder(holder: BackupInfoViewHolder, position: Int) = holder.binding.run { - tvHeader.text = root.getString(data[position].header) - tvBody.text = root.getString(data[position].body) - } -} - -private class BackupInfo( - @StringRes val header: Int, - @StringRes val body: Int, -) - -private val backupInfoSnippets = listOf( - BackupInfo( - R.string.onboarding_wallet_info_title_first, - R.string.onboarding_wallet_info_subtitle_first, - ), - BackupInfo( - R.string.onboarding_wallet_info_title_second, - R.string.onboarding_wallet_info_subtitle_second, - ), - BackupInfo( - R.string.onboarding_wallet_info_title_third, - R.string.onboarding_wallet_info_subtitle_third, - ), - BackupInfo( - R.string.onboarding_wallet_info_title_fourth, - R.string.onboarding_wallet_info_subtitle_fourth, - ), - -) - -class BackupInfoViewHolder(val binding: ItemBackupInfoAdapterBinding) : - RecyclerView.ViewHolder(binding.root) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt deleted file mode 100644 index 475a6d64b9..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingSeedPhraseStateHandler.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui - -import android.app.Activity -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.runtime.collectAsState -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.windowsize.rememberWindowSize -import com.tangem.feature.onboarding.api.OnboardingSeedPhraseScreen -import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState -import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -internal class OnboardingSeedPhraseStateHandler( - private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhraseScreen(), - private val activity: Activity, -) { - - fun newState( - walletFragment: OnboardingWalletFragment, - state: OnboardingWalletState, - seedPhraseViewModel: SeedPhraseViewModel, - ) { - if (state.step == OnboardingWalletStep.CreateWallet && !seedPhraseViewModel.isFinished) { - switchToWallet2SeedPhraseOnboarding(walletFragment, seedPhraseViewModel, state.getMaxProgress()) - } else { - switchToWalletOnboarding(walletFragment, state) - } - } - - private fun switchToWalletOnboarding(walletFragment: OnboardingWalletFragment, state: OnboardingWalletState) { - walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.hide() - walletFragment.pbBinding.pbState.show() - walletFragment.binding.onboardingWalletContainer.show() - walletFragment.handleOnboardingStep(state) - } - - private fun switchToWallet2SeedPhraseOnboarding( - walletFragment: OnboardingWalletFragment, - viewModel: SeedPhraseViewModel, - onboardingWalletMaxProgress: Int, - ) { - walletFragment.pbBinding.pbState.hide() - walletFragment.binding.onboardingWalletContainer.hide() - walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.show() - walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.setContent { - val subScreen = viewModel.currentScreen.collectAsState().value - setMainScreenToolbarTitle(walletFragment, subScreen) - - TangemTheme( - isDark = isSystemInDarkTheme(), - windowSize = rememberWindowSize(activity = activity), - ) { - onboardingSeedPhraseApi.ScreenContent( - uiState = viewModel.uiState, - subScreen = subScreen, - progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress, - ) - } - } - } - - private fun setMainScreenToolbarTitle(walletFragment: OnboardingWalletFragment, subScreen: SeedPhraseScreen) { - val titleResId = when (subScreen) { - SeedPhraseScreen.Intro -> R.string.wallet_title - SeedPhraseScreen.AboutSeedPhrase, - SeedPhraseScreen.YourSeedPhrase, - SeedPhraseScreen.CheckSeedPhrase, - -> R.string.onboarding_create_wallet_header - SeedPhraseScreen.ImportSeedPhrase -> R.string.onboarding_seed_intro_button_import - } - - walletFragment.binding.toolbar.title = walletFragment.getString(titleResId) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt deleted file mode 100644 index 99ecf0792a..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ /dev/null @@ -1,691 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui - -import android.content.res.ColorStateList -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.util.TypedValue -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import android.widget.FrameLayout -import android.widget.ImageView -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.ui.Modifier -import androidx.core.view.MenuProvider -import androidx.fragment.app.viewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.transition.TransitionManager -import by.kirich1409.viewbindingdelegate.viewBinding -import coil.load -import com.arkivanov.decompose.defaultComponentContext -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.tabs.TabLayoutMediator -import com.tangem.common.CardIdFormatter -import com.tangem.common.CompletionResult -import com.tangem.common.core.CardIdDisplayFormat -import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.Basic -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.di.RootAppComponentContext -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.extensions.setStatusBarColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.windowsize.rememberWindowSize -import com.tangem.datasource.utils.isNullOrEmpty -import com.tangem.domain.common.util.cardTypesResolver -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.models.scan.isRing -import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.onboarding.data.model.CreateWalletResponse -import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel -import com.tangem.features.managetokens.component.OnboardingManageTokensComponent -import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget -import com.tangem.sdk.ui.widget.leapfrogWidget.PropertyCalculator -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.* -import com.tangem.tap.features.BaseFragment -import com.tangem.tap.features.FragmentOnBackPressedHandler -import com.tangem.tap.features.addBackPressHandler -import com.tangem.tap.features.onboarding.OnboardingMenuProvider -import com.tangem.tap.features.onboarding.products.wallet.redux.* -import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.utils.Provider -import com.tangem.wallet.R -import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding -import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding -import com.tangem.wallet.databinding.ViewOnboardingProgressBinding -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch -import org.rekotlin.StoreSubscriber -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LargeClass", "MagicNumber") -@AndroidEntryPoint -class OnboardingWalletFragment : - BaseFragment(R.layout.fragment_onboarding_wallet), - StoreSubscriber, - FragmentOnBackPressedHandler { - - @Inject - internal lateinit var uiDependencies: UiDependencies - - @Inject - internal lateinit var onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory - - @Inject - @RootAppComponentContext - internal lateinit var rootComponentContext: AppComponentContext - - private var onboardingManageTokensComponent: OnboardingManageTokensComponent? = null - private lateinit var onboardingComponentContext: AppComponentContext - - internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind) - internal val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind) - - internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer } - private val bindingManageTokens by lazy { binding.onboardingManageTokensContainer } - - private val canSkipBackup by lazy { arguments?.getBoolean(AppRoute.OnboardingWallet.CAN_SKIP_BACKUP_KEY) ?: true } - - private lateinit var seedPhraseStateHandler: OnboardingSeedPhraseStateHandler - - private val seedPhraseViewModel by viewModels() - - private lateinit var cardsWidget: WalletCardsWidget - private var seedPhraseRouter: SeedPhraseRouter? = null - private var accessCodeDialog: AccessCodeDialog? = null - - private lateinit var animator: BackupAnimator - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - seedPhraseStateHandler = OnboardingSeedPhraseStateHandler(activity = requireActivity()) - - val newSeedPhraseRouter = makeSeedPhraseRouter() - seedPhraseRouter = newSeedPhraseRouter - seedPhraseViewModel.setRouter(newSeedPhraseRouter) - seedPhraseViewModel.setMediator(makeSeedPhraseMediator()) - - onboardingComponentContext = rootComponentContext.childByContext( - componentContext = defaultComponentContext(onBackPressedDispatcher = null), - ) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - val scaleFactor = getDeviceScaleFactor() - initCardsWidget(createLeapfrogWidget(binding.flCardsContainer, scaleFactor), scaleFactor) - - binding.viewPagerBackupInfo.adapter = BackupInfoAdapter() - TabLayoutMediator( - binding.tabLayoutBackupInfo, - binding.viewPagerBackupInfo, - ) { tab, position -> - // Some implementation - }.attach() - - (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) - - addBackPressHandler(handler = this) - - store.dispatch(OnboardingWallet2Action.Init(seedPhraseViewModel.maxProgress)) - store.dispatch(OnboardingWalletAction.Init) - store.dispatch( - OnboardingWalletAction.LoadArtwork( - cardArtworkUriForUnfinishedBackup = requireContext().resourceUri(R.drawable.card_placeholder_wallet), - ), - ) - } - - override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider( - scanResponseProvider = Provider { - store.state.globalState.onboardingState.onboardingManager?.scanResponse - ?: error("ScanResponse must be not null") - }, - ) - - private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) { - val viewBackupCount = flCardsContainer.childCount - 1 - if (viewBackupCount <= 0) return@with - if (viewBackupCount == backupCardsCounts) return@with - - cardsWidget.toFolded(false) - - if (viewBackupCount > backupCardsCounts) { - flCardsContainer.removeViews(0, viewBackupCount - backupCardsCounts) - } else { - flCardsContainer.inflate(R.layout.view_onboarding_card, true) - } - - val scaleFactor = getDeviceScaleFactor() - initCardsWidget(createLeapfrogWidget(flCardsContainer, scaleFactor), scaleFactor) - } - - private fun createLeapfrogWidget(container: FrameLayout, deviceScaleFactor: Float): LeapfrogWidget { - val leapfrogCalculator = PropertyCalculator( - yTranslationFactor = when (container.childCount) { - 3 -> 25f - else -> 35f - } * deviceScaleFactor, - ) - return LeapfrogWidget(container, leapfrogCalculator) - } - - private fun initCardsWidget(leapfrogWidget: LeapfrogWidget, deviceScaleFactor: Float, isTest: Boolean = false) { - cardsWidget = WalletCardsWidget(leapfrogWidget, deviceScaleFactor) - animator = if (isTest) { - TestBackupAnimation(WalletBackupAnimator(cardsWidget), binding) - } else { - WalletBackupAnimator(cardsWidget) - } - } - - override fun onStart() { - super.onStart() - store.subscribe(this) { state -> - state.skipRepeats { oldState, newState -> - oldState.onboardingWalletState == newState.onboardingWalletState - }.select { it.onboardingWalletState } - } - setStatusBarColor(R.color.background_primary) - } - - override fun onStop() { - super.onStop() - onboardingManageTokensComponent = null - store.unsubscribe(this) - } - - override fun newState(state: OnboardingWalletState) { - Handler(Looper.getMainLooper()).post { - if (activity == null || view == null) return@post - animator.updateBackupState(state.backupState) - requireActivity().invalidateOptionsMenu() - - pbBinding.pbState.max = state.getMaxProgress() - pbBinding.pbState.progress = state.getProgressStep() - - when { - state.wallet2State != null -> { - seedPhraseStateHandler.newState(this, state, seedPhraseViewModel) - seedPhraseViewModel.setCardArtworkUri( - cardArtworkUri = state.walletImages.primaryCardImage.toString(), - ) - updateWalletImagesState(state.walletImages) - } - else -> { - updateWalletImagesState(state.walletImages) - handleOnboardingStep(state) - } - } - } - } - - private fun updateWalletImagesState(walletImages: WalletImages) { - if (!walletImages.primaryCardImage.isNullOrEmpty()) { - loadImageIntoImageView(walletImages.primaryCardImage, binding.imvFrontCard) - } - if (!walletImages.secondCardImage.isNullOrEmpty()) { - loadImageIntoImageView(walletImages.secondCardImage, binding.imvFirstBackupCard) - } - if (!walletImages.thirdCardImage.isNullOrEmpty()) { - loadImageIntoImageView(walletImages.thirdCardImage, binding.imvSecondBackupCard) - } - } - - private fun loadImageIntoImageView(uri: Uri?, view: ImageView) { - view.load(uri) { - placeholder(R.drawable.card_placeholder_black) - error(R.drawable.card_placeholder_black) - fallback(R.drawable.card_placeholder_black) - } - } - - internal fun handleOnboardingStep(state: OnboardingWalletState) { - when (state.step) { - OnboardingWalletStep.CreateWallet -> setupCreateWalletState() - OnboardingWalletStep.Backup -> setBackupState(state = state.backupState) - OnboardingWalletStep.ManageTokens -> setManageTokensState(requireNotNull(state.userWalletId)) - OnboardingWalletStep.Done -> showSuccess() - else -> {} - } - } - - private fun initializeOnboardingManageTokensComponent(userWalletId: UserWalletId) { - onboardingManageTokensComponent = onboardingManageTokensComponentFactory.create( - context = onboardingComponentContext, - params = OnboardingManageTokensComponent.Params(userWalletId = userWalletId), - onDone = {}, - ) - } - - private fun setManageTokensState(userWalletId: UserWalletId) { - with(binding) { - tvHeader.show() - tvBody.show() - viewPagerBackupInfo.hide() - tabLayoutBackupInfo.hide() - onboardingWalletContainer.show() - layoutButtonsCommon.btnWalletAlternativeAction.hide() - onboardingWalletContainer.hide() - toolbar.title = getText(R.string.onboarding_add_tokens) - } - pbBinding.pbState.show() - bindingSeedPhrase.onboardingSeedPhraseContainer.hide() - if (onboardingManageTokensComponent == null) { - initializeOnboardingManageTokensComponent(userWalletId) - } - with(bindingManageTokens) { - onboardingManageTokensContainer.hide() - onboardingManageTokensContainer.show() - onboardingManageTokensContainer.setContent { - TangemTheme( - isDark = isSystemInDarkTheme(), - windowSize = rememberWindowSize(activity = requireActivity()), - ) { - onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize()) - } - } - } - } - - private fun setupCreateWalletState() = with(binding) { - layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet) - layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24) - - layoutButtonsCommon.btnWalletMainAction.setOnClickListener { - Analytics.send(Onboarding.CreateWallet.ButtonCreateWallet()) - store.dispatch(OnboardingWalletAction.CreateWallet) - } - layoutButtonsCommon.btnWalletAlternativeAction.hide() - - toolbar.title = getText(R.string.onboarding_getting_started) - - tvHeader.setText(R.string.onboarding_create_wallet_header) - tvBody.setText(R.string.onboarding_create_wallet_body) - - animator.setupCreateWalletState() - } - - private fun setBackupState(state: BackupState) { - when (state.backupStep) { - null -> Unit - BackupStep.InitBackup -> showBackupIntro(state) - BackupStep.ScanOriginCard -> showScanOriginCard() - BackupStep.AddBackupCards -> showAddBackupCards(state) - BackupStep.SetAccessCode -> showSetAccessCode() - BackupStep.EnterAccessCode -> showEnterAccessCode(state) - BackupStep.ReenterAccessCode -> showReenterAccessCode(state) - is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state) - is BackupStep.WriteBackupCard -> showWriteBackupCard(state) - BackupStep.Finished -> { - // don't need to navigate here" - // showSuccess() - } - } - } - - private fun showBackupIntro(state: BackupState) = with(binding) { - imvFirstBackupCard.show() - imvSecondBackupCard.show() - - tvHeader.hide() - tvBody.hide() - viewPagerBackupInfo.show() - tabLayoutBackupInfo.show() - - with(layoutButtonsCommon) { - btnWalletMainAction.text = getText(R.string.onboarding_button_backup_now) - btnWalletMainAction.icon = null - btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.StartBackup) } - - btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup) - btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.SkipBackup) } - btnWalletAlternativeAction.show(state.canSkipBackup && canSkipBackup) - } - animator.showBackupIntro(state) - } - - private fun showScanOriginCard() = with(binding) { - prepareBackupView() - tvHeader.text = getText(R.string.onboarding_title_scan_origin_card) - tvBody.text = getString( - R.string.onboarding_subtitle_scan_primary, - ) - - with(layoutButtonsCommon) { - btnWalletMainAction.text = getString(R.string.onboarding_button_scan_origin_card) - btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24) - btnWalletAlternativeAction.hide() - btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.ScanPrimaryCard) } - } - - animator.showScanOriginCard() - } - - private fun showAddBackupCards(state: BackupState) = with(binding) { - prepareBackupView() - - accessCodeDialog?.dismiss() - accessCodeDialog = null - - layoutButtonsAddCards.root.show() - layoutButtonsCommon.root.hide() - layoutButtonsAddCards.btnAddCard.setIconResource(R.drawable.ic_tangem_24) - if (state.showBtnLoading) { - layoutButtonsAddCards.btnAddCard.iconTint = ColorStateList.valueOf( - resources.getColor(R.color.button_secondary), - ) - layoutButtonsAddCards.btnAddCard.text = "" - layoutButtonsAddCards.btnAddCard.isEnabled = false - layoutButtonsAddCards.btnProgress.show() - } else { - layoutButtonsAddCards.btnAddCard.iconTint = ColorStateList.valueOf( - resources.getColor(R.color.icon_primary_1), - ) - layoutButtonsAddCards.btnProgress.hide() - layoutButtonsAddCards.btnAddCard.text = getText(R.string.onboarding_button_add_backup_card) - layoutButtonsAddCards.btnAddCard.isEnabled = true - } - if (state.backupCardsNumber < state.maxBackupCards) { - layoutButtonsAddCards.btnAddCard.setOnClickListener { - store.dispatch(BackupAction.AddBackupCard) - } - } else { - layoutButtonsAddCards.btnAddCard.isEnabled = false - } - - layoutButtonsAddCards.btnContinue.text = getText(R.string.onboarding_button_finalize_backup) - layoutButtonsAddCards.btnContinue.setOnClickListener { store.dispatch(BackupAction.FinishAddingBackupCards) } - layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0 - when (state.backupCardsNumber) { - 0 -> { - tvHeader.text = getText(R.string.onboarding_title_no_backup_cards) - tvBody.text = getText(R.string.onboarding_subtitle_no_backup_cards) - } - 1 -> { - tvHeader.text = getText(R.string.onboarding_title_one_backup_card) - tvBody.text = getText(R.string.onboarding_subtitle_one_backup_card) - } - 2 -> { - tvHeader.text = getText(R.string.onboarding_title_two_backup_cards) - tvBody.text = getText(R.string.onboarding_subtitle_two_backup_cards) - } - else -> {} - } - - animator.showAddBackupCards(state, state.backupCardsNumber) - } - - private fun prepareBackupView() = with(binding) { - toolbar.title = getText(R.string.onboarding_navbar_title_creating_backup) - - tvHeader.show() - tvBody.show() - viewPagerBackupInfo.hide() - tabLayoutBackupInfo.hide() - - imvFirstBackupCard.show() - imvSecondBackupCard.show() - } - - private fun showSetAccessCode() { - accessCodeDialog = AccessCodeDialog(requireContext()).apply { - dismissWithAnimation = true - create() - setOnCancelListener { - store.dispatch(BackupAction.OnAccessCodeDialogClosed) - } - - setOnShowListener { setSecurityMode(true) } - - setOnDismissListener { setSecurityMode(false) } - - show() - showInfoScreen() - val view = - delegate.findViewById(com.google.android.material.R.id.design_bottom_sheet) - (view?.parent as? ViewGroup)?.let { TransitionManager.beginDelayedTransition(it) } - behavior.state = BottomSheetBehavior.STATE_EXPANDED - window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) - view?.let { it.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT } - } - } - - private fun showEnterAccessCode(state: BackupState) { - accessCodeDialog?.showEnterAccessCode() - accessCodeDialog?.showError(state.accessCodeError) - } - - private fun showReenterAccessCode(state: BackupState) { - accessCodeDialog?.showReenterAccessCode() - accessCodeDialog?.showError(state.accessCodeError) - } - - private fun showWritePrimaryCard(state: BackupState) = with(binding) { - accessCodeDialog?.dismiss() - - reInitCardsWidgetIfNeeded(state.backupCardsNumber) - prepareViewForFinalizeStep() - - val isRing = state.primaryCardBatchId != null && isRing(batchId = state.primaryCardBatchId) - - tvHeader.text = getText( - if (isRing) R.string.common_origin_ring else R.string.common_origin_card, - ) - - tvBody.text = if (isRing) { - getString(R.string.onboarding_subtitle_scan_ring) - } else { - val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) - getString( - R.string.onboarding_subtitle_scan_primary_card_format, - state.primaryCardId?.let { cardIdFormatter.getFormattedCardId(it) }, - ) - } - - layoutButtonsCommon.btnWalletMainAction.text = getText( - if (isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin, - ) - - layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24) - layoutButtonsCommon.btnWalletMainAction.setOnClickListener { store.dispatch(BackupAction.WritePrimaryCard) } - - animator.showWritePrimaryCard(state) - } - - private fun prepareViewForFinalizeStep() = with(binding) { - toolbar.title = getText(R.string.onboarding_button_finalize_backup) - - layoutButtonsAddCards.root.hide() - layoutButtonsCommon.root.show() - layoutButtonsCommon.btnWalletAlternativeAction.hide() - - imvCardBackground.hide() - imvFirstBackupCard.show() - imvSecondBackupCard.show() - } - - private fun showWriteBackupCard(state: BackupState) = with(binding) { - reInitCardsWidgetIfNeeded(state.backupCardsNumber) - prepareViewForFinalizeStep() - - val cardNumber = (state.backupStep as? BackupStep.WriteBackupCard)?.cardNumber ?: 1 - - val isRing = isRing(batchId = state.backupCardBatchIds[cardNumber - 1]) - tvHeader.text = getString( - if (isRing) R.string.onboarding_title_backup_ring else R.string.onboarding_title_backup_card, - ) - - tvBody.text = if (isRing) { - getString(R.string.onboarding_subtitle_scan_ring) - } else { - val cardIdFormatter = CardIdFormatter(CardIdDisplayFormat.LastMasked(4)) - getString( - R.string.onboarding_subtitle_scan_backup_card_format, - cardIdFormatter.getFormattedCardId(state.backupCardIds[cardNumber - 1]), - ) - } - - layoutButtonsCommon.btnWalletMainAction.text = getString( - if (isRing) R.string.onboarding_button_backup_ring else R.string.onboarding_button_backup_origin, - ) - - layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24) - layoutButtonsCommon.btnWalletMainAction.setOnClickListener { - store.dispatch(BackupAction.WriteBackupCard(cardNumber)) - } - - animator.showWriteBackupCard(state, cardNumber) - } - - private fun showSuccess() = with(binding) { - toolbar.title = getString(R.string.onboarding_done_header) - tvHeader.text = getText(R.string.onboarding_done_header) - - tvHeader.show() - tvBody.show() - viewPagerBackupInfo.hide() - tabLayoutBackupInfo.hide() - onboardingWalletContainer.show() - bindingManageTokens.onboardingManageTokensContainer.hide() - - tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding) - layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet) - layoutButtonsCommon.btnWalletMainAction.icon = null - layoutButtonsCommon.btnWalletAlternativeAction.hide() - layoutButtonsCommon.btnWalletMainAction.setOnClickListener { - showConfetti(false) - store.dispatch(OnboardingWalletAction.FinishOnboarding(scope = requireActivity().lifecycleScope)) - } - - animator.showSuccess { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { - flCardsContainer.hide() - imvCardBackground.hide() - showConfetti(true) - imvSuccess.alpha = 0f - imvSuccess.show() - imvSuccess.animate()?.alpha(1f)?.duration = 400 - } - } - } - - internal fun showConfetti(show: Boolean) { - if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { - with(binding.vConfetti) { - lavConfetti.show(show) - - if (show) { - lavConfetti.playAnimation() - } else { - lavConfetti.cancelAnimation() - } - } - } - } - - override fun handleOnBackPressed() { - // workaround to use right navigation back for toolbar back btn on seed phrase flow - val isWallet2 = - store.state.globalState.onboardingState.onboardingManager?.scanResponse?.cardTypesResolver?.isWallet2() - ?: false - val seedPhraseRouter = seedPhraseRouter - if (seedPhraseRouter != null && isWallet2) { - seedPhraseRouter.navigateBack() - } else { - legacyOnBackHandler() - } - } - - private fun legacyOnBackHandler() { - store.dispatch(OnboardingWalletAction.OnBackPressed) - } - - private fun getDeviceScaleFactor(): Float { - val typedValue = TypedValue() - resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true) - return typedValue.float - } - - private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter( - onBack = ::legacyOnBackHandler, - onOpenChat = { - Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro)) - - val scanResponse = requireNotNull(store.state.globalState.onboardingState.onboardingManager?.scanResponse) - - val getCardInfoUseCase = store.inject(DaggerGraphState::getCardInfoUseCase) - val cardInfo = requireNotNull(getCardInfoUseCase.invoke(scanResponse).getOrNull()) - - scope.launch { - store.inject(DaggerGraphState::sendFeedbackEmailUseCase) - .invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo)) - } - }, - onOpenUriClick = { uri -> - store.dispatchOpenUrl(uri.toString()) - }, - ) - - private fun makeSeedPhraseMediator(): SeedPhraseMediator { - return object : SeedPhraseMediator { - override fun createWallet(callback: (CompletionResult) -> Unit) { - store.dispatch(OnboardingWallet2Action.CreateWallet(callback)) - } - - override fun onWalletCreated(result: CompletionResult) { - store.dispatch(OnboardingWallet2Action.WalletWasCreated(result)) - } - - override fun importWallet( - mnemonicComponents: List, - passphrase: String?, - seedPhraseSource: SeedPhraseSource, - callback: (CompletionResult) -> Unit, - ) { - store.dispatch( - OnboardingWallet2Action.ImportWallet( - mnemonicComponents, - passphrase, - seedPhraseSource, - callback, - ), - ) - } - - override fun allowScreenshots(allow: Boolean) { - setSecurityMode(value = !allow) - } - } - } - - private fun setSecurityMode(value: Boolean) { - Timber.d("Security mode: ${if (value) "enabled" else "disabled"}") - - if (value) { - requireActivity().window.setFlags( - WindowManager.LayoutParams.FLAG_SECURE, - WindowManager.LayoutParams.FLAG_SECURE, - ) - } else { - requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt deleted file mode 100644 index 4a06365222..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/WalletCardsWidget.kt +++ /dev/null @@ -1,240 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui - -import android.animation.AnimatorSet -import android.animation.ObjectAnimator -import android.animation.PropertyValuesHolder -import android.view.View -import android.widget.ImageView -import androidx.core.animation.doOnEnd -import com.tangem.sdk.ui.widget.leapfrogWidget.LeapView -import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget - -class WalletCardsWidget( - val leapfrogWidget: LeapfrogWidget, - private val deviceScaleFactor: Float = 1f, -) { - - private val animDuration: Long = 400 - - private var currentState: WidgetState? = null - - fun toWelcome(animate: Boolean = true, onEnd: () -> Unit = {}) { - if (currentState == WidgetState.WELCOME) return - - currentState = WidgetState.WELCOME - - val animator = createAnimator(animate, onEnd = onEnd) - animator.playTogether(createAnimators(::createWelcomeProperties)) - leapfrogWidget.fold { animator.start() } - } - - fun toFolded(animate: Boolean = true, duration: Long = animDuration, onEnd: () -> Unit = {}) { - if (currentState == WidgetState.FOLDED) return - - currentState = WidgetState.FOLDED - - val animator = createAnimator(animate, duration) { - leapfrogWidget.fold(animate, onEnd) - } - animator.playTogether(createAnimators(::createLeapfrogProperties)) - animator.start() - } - - fun toFan(animate: Boolean = true, onEnd: () -> Unit = {}) { - if (currentState == WidgetState.FAN) return - - currentState = WidgetState.FAN - - val animator = createAnimator(animate, onEnd = onEnd) - animator.playTogether(createAnimators(::createFanProperties)) - leapfrogWidget.fold { animator.start() } - } - - fun toLeapfrog(animate: Boolean = true, onEndFold: () -> Unit = {}, onEnd: () -> Unit = {}) { - if (currentState == WidgetState.LEAPFROG) return - - currentState = WidgetState.LEAPFROG - - val onAnimatorEnd = { - leapfrogWidget.fold(animate) { - leapfrogWidget.initViews() - onEndFold() - leapfrogWidget.unfold(animate, onEnd) - } - } - val animator = createAnimator(animate, onEnd = onAnimatorEnd) - animator.playTogether(createAnimators(::createLeapfrogProperties)) - animator.start() - } - - private fun createAnimators(propertyFactory: (BackupCardType) -> CardProperties): List { - return getCardTypesTypes().map { createCardAnimator(it, propertyFactory(it)) } - } - - @Suppress("MagicNumber") - private fun getCardTypesTypes(): List = when (leapfrogWidget.getViewsCount()) { - 2 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP) - 3 -> listOf(BackupCardType.ORIGIN, BackupCardType.FIRST_BACKUP, BackupCardType.SECOND_BACKUP) - else -> throw UnsupportedOperationException() - } - - private fun createAnimator(animate: Boolean, duration: Long = animDuration, onEnd: () -> Unit): AnimatorSet { - return AnimatorSet().apply { - this.duration = if (animate) duration else 0 - doOnEnd { onEnd() } - } - } - - private fun createCardAnimator(cardType: BackupCardType, properties: CardProperties): ObjectAnimator { - val view = getLeapViewByCard(cardType).view - val animator = ObjectAnimator.ofPropertyValuesHolder( - view, - *properties.createValuesHolders().toTypedArray(), - ) - view.elevation = properties.elevation - return animator - } - - @Suppress("MagicNumber") - private fun createWelcomeProperties(cardType: BackupCardType): CardProperties { - return when (cardType) { - BackupCardType.ORIGIN -> CardProperties( - xTranslation = 440f * deviceScaleFactor, - yTranslation = 70f, - rotation = 75f, - elevation = 2f, - scale = .85f * deviceScaleFactor, - ) - BackupCardType.FIRST_BACKUP -> CardProperties( - xTranslation = 10f * deviceScaleFactor, - yTranslation = -30f, - rotation = 100f, - elevation = 1f, - scale = .85f * deviceScaleFactor, - ) - BackupCardType.SECOND_BACKUP -> CardProperties( - xTranslation = -440f * deviceScaleFactor, - yTranslation = -30f, - rotation = 70f, - elevation = 0f, - scale = .85f * deviceScaleFactor, - ) - } - } - - @Suppress("MagicNumber") - private fun createFanProperties(cardType: BackupCardType): CardProperties { - return when (cardType) { - BackupCardType.ORIGIN -> CardProperties( - xTranslation = 0f, - yTranslation = 30f, - rotation = 5f, - elevation = 2f, - scale = 1f * deviceScaleFactor, - ) - BackupCardType.FIRST_BACKUP -> CardProperties( - xTranslation = 0f, - yTranslation = -50f, - rotation = -5f, - elevation = 1f, - scale = 0.9f * deviceScaleFactor, - ) - BackupCardType.SECOND_BACKUP -> CardProperties( - xTranslation = 10f, - yTranslation = -120f, - rotation = -20f, - elevation = 0f, - scale = 0.8f * deviceScaleFactor, - ) - } - } - - @Suppress("MagicNumber") - private fun createLeapfrogProperties(cardType: BackupCardType): CardProperties { - return when (cardType) { - BackupCardType.ORIGIN -> CardProperties( - xTranslation = 0f, - yTranslation = 0f, - rotation = 0f, - elevation = 2f, - scale = 1f * deviceScaleFactor, - ) - BackupCardType.FIRST_BACKUP -> CardProperties( - xTranslation = 0f, - yTranslation = 0f, - rotation = 0f, - elevation = 1f, - scale = 0.9f * deviceScaleFactor, - ) - BackupCardType.SECOND_BACKUP -> CardProperties( - xTranslation = 0f, - yTranslation = 0f, - rotation = 0f, - elevation = 0f, - scale = 0.8f * deviceScaleFactor, - ) - } - } - - @Throws(UnsupportedOperationException::class) - fun getOriginCardView(): ImageView { - return getLeapViewByCard(BackupCardType.ORIGIN).view as ImageView - } - - @Throws(UnsupportedOperationException::class) - fun getFirstBackupCardView(): ImageView { - return getLeapViewByCard(BackupCardType.FIRST_BACKUP).view as ImageView - } - - @Throws(UnsupportedOperationException::class) - fun getSecondBackupCardView(): ImageView { - return getLeapViewByCard(BackupCardType.SECOND_BACKUP).view as ImageView - } - - @Suppress("MagicNumber") - @Throws(UnsupportedOperationException::class) - private fun getLeapViewByCard(cardType: BackupCardType): LeapView { - fun getByIndex(index: Int): LeapView { - return leapfrogWidget.getViewByPosition(leapfrogWidget.getViewPositionByIndex(index)) - } - - return when (leapfrogWidget.getViewsCount()) { - 2 -> when (cardType) { - BackupCardType.ORIGIN -> getByIndex(1) - BackupCardType.FIRST_BACKUP -> getByIndex(0) - BackupCardType.SECOND_BACKUP -> throw UnsupportedOperationException() - } - 3 -> when (cardType) { - BackupCardType.ORIGIN -> getByIndex(2) - BackupCardType.FIRST_BACKUP -> getByIndex(1) - BackupCardType.SECOND_BACKUP -> getByIndex(0) - } - else -> throw UnsupportedOperationException() - } - } - - enum class WidgetState { WELCOME, FOLDED, FAN, LEAPFROG } -} - -enum class BackupCardType { ORIGIN, FIRST_BACKUP, SECOND_BACKUP } - -private data class CardProperties( - val xTranslation: Float = 0f, - val yTranslation: Float = 0f, - val rotation: Float = 0f, - val elevation: Float = 0f, - val scale: Float = 1.1f, -) { - - fun createValuesHolders(): List { - return listOf( - PropertyValuesHolder.ofFloat(View.TRANSLATION_X, xTranslation), - PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, yTranslation), - PropertyValuesHolder.ofFloat(View.ROTATION, rotation), - PropertyValuesHolder.ofFloat(View.SCALE_X, scale), - PropertyValuesHolder.ofFloat(View.SCALE_Y, scale), - ) - } - - companion object -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt deleted file mode 100644 index bdeec1a22b..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AccessCodeDialog.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.content.Context -import android.content.DialogInterface -import android.os.Bundle -import android.view.LayoutInflater -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.tangem.tap.common.extensions.hide -import com.tangem.tap.common.extensions.show -import com.tangem.tap.features.onboarding.products.wallet.redux.AccessCodeError -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.LayoutBackupAccessCodeBinding - -class AccessCodeDialog(context: Context) : BottomSheetDialog(context) { - - private var binding: LayoutBackupAccessCodeBinding? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = LayoutBackupAccessCodeBinding - .inflate(LayoutInflater.from(context)) - setContentView(binding!!.root) - behavior.state = BottomSheetBehavior.STATE_EXPANDED - } - - override fun setOnDismissListener(listener: DialogInterface.OnDismissListener?) { - super.setOnDismissListener { - listener?.onDismiss(it) - binding = null - } - } - - fun showInfoScreen() = binding?.run { - behavior.state = BottomSheetBehavior.STATE_EXPANDED - accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title) - layoutBackupAccessCodeInfo.root.show() - layoutBackupAccessCodeSubmit.root.hide() - layoutBackupAccessCodeInfo.btnAccessCodeCreate.setOnClickListener { - store.dispatch(BackupAction.ShowEnterAccessCodeScreen) - } - } - - fun showEnterAccessCode() = binding?.run { - layoutBackupAccessCodeInfo.root.hide() - layoutBackupAccessCodeSubmit.root.show() - accessCodeTitle.text = context.getText(R.string.onboarding_access_code_intro_title) - - with(layoutBackupAccessCodeSubmit) { - btnAccessCodeSubmit.text = context.getText(R.string.common_continue) - btnAccessCodeSubmit.setOnClickListener { - store.dispatch(BackupAction.CheckAccessCode(etAccessCode.text.toString())) - } - } - } - - fun showReenterAccessCode() = binding?.run { - layoutBackupAccessCodeInfo.root.hide() - layoutBackupAccessCodeSubmit.root.show() - accessCodeTitle.text = context.getText(R.string.onboarding_access_code_repeat_code_title) - with(layoutBackupAccessCodeSubmit) { - etAccessCode.setText("") - btnAccessCodeSubmit.text = context.getText(R.string.common_submit) - btnAccessCodeSubmit.setOnClickListener { - store.dispatch(BackupAction.SaveAccessCodeConfirmation(etAccessCode.text.toString())) - } - } - } - - fun showError(error: AccessCodeError?) = binding?.layoutBackupAccessCodeSubmit?.run { - when (error) { - AccessCodeError.CodeTooShort -> - tilAccessCode.error = context.getString(R.string.onboarding_access_code_too_short) - AccessCodeError.CodesDoNotMatch -> - tilAccessCode.error = context.getString(R.string.onboarding_access_codes_doesnt_match) - null -> tilAccessCode.error = null - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt deleted file mode 100644 index 07a64cc81e..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AddMoreBackupCardsDialog.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object AddMoreBackupCardsDialog { - fun create(context: Context): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(R.string.common_warning) - setMessage(R.string.onboarding_alert_message_not_max_backup_cards_added) - setPositiveButton(R.string.common_continue) { _, _ -> - store.dispatch(BackupAction.ShowAccessCodeInfoScreen) - } - setNegativeButton(R.string.common_cancel) { _, _ -> - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt deleted file mode 100644 index 5b6c8b8c85..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/AttestationFailedDialog.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.store -import com.tangem.wallet.R - -internal object AttestationFailedDialog { - - fun create(context: Context): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(R.string.common_error) - setMessage(R.string.issuer_signature_loading_failed) - setPositiveButton(R.string.common_ok) { dialog, _ -> - dialog.dismiss() - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt deleted file mode 100644 index 5094c1f4e9..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/BackupInProgressDialog.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object BackupInProgressDialog { - fun create(context: Context): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(R.string.common_warning) - setMessage(R.string.onboarding_backup_exit_warning) - setPositiveButton(R.string.warning_button_ok) { _, _ -> - store.dispatch(GlobalAction.HideDialog) - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/InterruptOnboardingDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/InterruptOnboardingDialog.kt deleted file mode 100644 index f0249cfcd2..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/InterruptOnboardingDialog.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.app.Dialog -import android.content.Context -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.tap.common.extensions.dispatchDialogHide -import com.tangem.tap.features.onboarding.OnboardingDialog -import com.tangem.tap.store -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -object InterruptOnboardingDialog { - fun create(context: Context, dialog: OnboardingDialog.InterruptOnboarding): Dialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(context.getString(R.string.onboarding_exit_alert_title)) - setMessage(context.getString(R.string.onboarding_exit_alert_message)) - setPositiveButton(R.string.common_ok) { _, _ -> dialog.onOk() } - setNegativeButton(R.string.common_cancel) { _, _ -> } - setOnDismissListener { store.dispatchDialogHide() } - setCancelable(false) - }.create() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt deleted file mode 100644 index 90458e6f63..0000000000 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/dialogs/ResetBackupCardDialog.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs - -import android.content.Context -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.tangem.core.analytics.Analytics -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.redux.global.GlobalAction -import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction -import com.tangem.tap.store -import com.tangem.wallet.R - -object ResetBackupCardDialog { - fun create(context: Context, cardId: String): AlertDialog { - return MaterialAlertDialogBuilder(context, R.style.CustomMaterialDialog).apply { - setTitle(R.string.common_attention) - setMessage(R.string.onboarding_linking_error_card_with_wallets) - setPositiveButton(R.string.common_cancel) { _, _ -> - Analytics.send(Onboarding.Backup.ResetCancelEvent) - } - setNegativeButton(R.string.card_settings_action_sheet_reset) { _, _ -> - Analytics.send(Onboarding.Backup.ResetPerformEvent) - store.dispatch(BackupAction.ResetBackupCard(cardId)) - } - setOnDismissListener { - store.dispatch(GlobalAction.HideDialog) - } - }.create() - } -} \ No newline at end of file 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 c829c5ad78..d7a44e57fd 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 @@ -33,10 +33,6 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent import com.tangem.tap.features.home.api.HomeComponent -import com.tangem.tap.features.onboarding.products.note.OnboardingNoteFragment -import com.tangem.tap.features.onboarding.products.otherCards.OnboardingOtherCardsFragment -import com.tangem.tap.features.onboarding.products.twins.ui.OnboardingTwinsFragment -import com.tangem.tap.features.onboarding.products.wallet.ui.OnboardingWalletFragment import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child @@ -423,11 +419,7 @@ internal class ChildFactory @Inject constructor( componentFactory = nftSendComponentFactory, ) } - is AppRoute.OnboardingNote, is AppRoute.SaveWallet, - is AppRoute.OnboardingOther, - is AppRoute.OnboardingTwins, - is AppRoute.OnboardingWallet, -> error("Unsupported route: $route") } // endregion @@ -545,18 +537,6 @@ internal class ChildFactory @Inject constructor( componentFactory = manageTokensComponentFactory, ) } - is AppRoute.OnboardingNote -> { - route.asFragmentChild(Provider { OnboardingNoteFragment() }) - } - is AppRoute.OnboardingOther -> { - route.asFragmentChild(Provider { OnboardingOtherCardsFragment() }) - } - is AppRoute.OnboardingTwins -> { - route.asFragmentChild(Provider { OnboardingTwinsFragment() }) - } - is AppRoute.OnboardingWallet -> { - route.asFragmentChild(Provider { OnboardingWalletFragment() }) - } is AppRoute.QrScanning -> { val source = when (route.source) { is AppRoute.QrScanning.Source.Send -> SourceType.SEND 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 ca72fdcd9a..24ed3c80f7 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 @@ -40,27 +40,6 @@ sealed class AppRoute(val path: String) : Route { val isTosAccepted: Boolean, ) : AppRoute(path = "/disclaimer${if (isTosAccepted) "/tos_accepted" else ""}") - @Serializable - data object OnboardingNote : AppRoute(path = "/onboarding/note") - - @Serializable - data class OnboardingWallet( - val canSkipBackup: Boolean = true, - ) : AppRoute(path = "/onboarding/wallet${if (canSkipBackup) "/skippable" else ""}"), RouteBundleParams { - - override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - const val CAN_SKIP_BACKUP_KEY = "canSkipBackup" - } - } - - @Serializable - data object OnboardingTwins : AppRoute(path = "/onboarding/twins") - - @Serializable - data object OnboardingOther : AppRoute(path = "/onboarding/other") - @Serializable data object Wallet : AppRoute(path = "/wallet") 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 eb8297c289..57f18c60fc 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 @@ -7,10 +7,6 @@ "name": "ONRAMP_ENABLED", "version": "undefined" }, - { - "name": "ONBOARDING_CODE_REFACTORING_ENABLED", - "version": "5.22.0" - }, { "name": "NAVIGATION_REFACTORING", "version": "5.23.0" @@ -47,10 +43,6 @@ "name": "ASK_BIOMETRY_REFACTORING_ENABLED", "version": "5.22.0" }, - { - "name": "NOTE_REFACTORING_ENABLED", - "version": "5.23.0" - }, { "name": "NEW_ARTWORK_LOADING", "version": "5.24.0" @@ -59,10 +51,6 @@ "name": "NEW_ATTESTATION_ENABLED", "version": "undefined" }, - { - "name": "TWIN_REFACTORING_ENABLED", - "version": "5.23.0" - }, { "name": "SEND_V2_ENABLED", "version": "5.23.0" diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt index 1bc55396bb..a9d9660b18 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt @@ -1,16 +1,8 @@ package com.tangem.domain.redux -import com.tangem.domain.models.scan.ScanResponse import org.rekotlin.Action sealed interface LegacyAction : Action { - /** - * Initiate an onboarding process. - * For resuming unfinished backup of standard Wallet see - * BackupAction.CheckForUnfinishedBackup, GlobalAction.Onboarding.StartForUnfinishedBackup - */ - data class StartOnboardingProcess(val scanResponse: ScanResponse, val canSkipBackup: Boolean = true) : LegacyAction - data object PrepareDetailsScreen : LegacyAction } \ No newline at end of file diff --git a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt index dbeb952224..8a3ef86745 100644 --- a/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt +++ b/features/onboarding-v2/api/src/main/kotlin/com/tangem/features/onboarding/v2/OnboardingV2FeatureToggles.kt @@ -1,8 +1,5 @@ package com.tangem.features.onboarding.v2 interface OnboardingV2FeatureToggles { - val isOnboardingV2Enabled: Boolean val isVisaOnboardingEnabled: Boolean - val isNoteRefactoringEnabled: Boolean - val isTwinRefactoringEnabled: Boolean } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt index 80bbb18b70..9bd61c926e 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/DefaultOnboardingV2FeatureToggles.kt @@ -6,12 +6,6 @@ import javax.inject.Inject internal class DefaultOnboardingV2FeatureToggles @Inject constructor( private val featureTogglesManager: FeatureTogglesManager, ) : OnboardingV2FeatureToggles { - override val isOnboardingV2Enabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("ONBOARDING_CODE_REFACTORING_ENABLED") override val isVisaOnboardingEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("VISA_ONBOARDING_ENABLED") - override val isNoteRefactoringEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("NOTE_REFACTORING_ENABLED") - override val isTwinRefactoringEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("TWIN_REFACTORING_ENABLED") } \ No newline at end of file diff --git a/features/onboarding/.gitignore b/features/onboarding/.gitignore deleted file mode 100644 index 42afabfd2a..0000000000 --- a/features/onboarding/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts deleted file mode 100644 index 6d048e8c4d..0000000000 --- a/features/onboarding/build.gradle.kts +++ /dev/null @@ -1,64 +0,0 @@ -plugins { - alias(deps.plugins.android.library) - alias(deps.plugins.kotlin.android) - alias(deps.plugins.kotlin.kapt) - alias(deps.plugins.kotlin.serialization) - alias(deps.plugins.hilt.android) - id("configuration") -} - -android { - namespace = "com.tangem.feature.onboarding" -} - -dependencies { - /** Core modules */ - implementation(project(":common")) - implementation(project(":domain:models")) - implementation(project(":core:config-toggles")) - implementation(project(":core:datasource")) - implementation(project(":core:analytics")) - implementation(projects.core.analytics.models) - implementation(project(":core:utils")) - implementation(project(":core:ui")) - implementation(project(":core:res")) - implementation(project(":data:common")) - - /** Tangem libraries */ - implementation(tangemDeps.card.core) - implementation(tangemDeps.card.android) { - exclude(module = "joda-time") - } - - /** AndroidX libraries */ - implementation(deps.androidx.core.ktx) - implementation(deps.androidx.appCompat) - implementation(deps.androidx.fragment.ktx) - implementation(deps.androidx.constraintLayout) - implementation(deps.androidx.activity.compose) - implementation(deps.lifecycle.runtime.ktx) - implementation(deps.lifecycle.common.java8) - implementation(deps.lifecycle.viewModel.ktx) - - /** Compose libraries */ - implementation(deps.compose.material) - implementation(deps.compose.material3) - implementation(deps.compose.animation) - implementation(deps.compose.foundation) - implementation(deps.compose.ui) - implementation(deps.compose.ui.tooling) - implementation(deps.compose.coil) - implementation(deps.compose.shimmer) - - implementation(tangemDeps.card.core) - - /** Other libraries */ - implementation(deps.compose.shimmer) - implementation(deps.kotlin.immutable.collections) - implementation(deps.kotlin.serialization) - implementation(deps.timber) - - /** DI */ - implementation(deps.hilt.android) - kapt(deps.hilt.kapt) -} \ No newline at end of file diff --git a/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt b/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt deleted file mode 100644 index f78ccaf2ce..0000000000 --- a/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt +++ /dev/null @@ -1,2048 +0,0 @@ -abandon -ability -able -about -above -absent -absorb -abstract -absurd -abuse -access -accident -account -accuse -achieve -acid -acoustic -acquire -across -act -action -actor -actress -actual -adapt -add -addict -address -adjust -admit -adult -advance -advice -aerobic -affair -afford -afraid -again -age -agent -agree -ahead -aim -air -airport -aisle -alarm -album -alcohol -alert -alien -all -alley -allow -almost -alone -alpha -already -also -alter -always -amateur -amazing -among -amount -amused -analyst -anchor -ancient -anger -angle -angry -animal -ankle -announce -annual -another -answer -antenna -antique -anxiety -any -apart -apology -appear -apple -approve -april -arch -arctic -area -arena -argue -arm -armed -armor -army -around -arrange -arrest -arrive -arrow -art -artefact -artist -artwork -ask -aspect -assault -asset -assist -assume -asthma -athlete -atom -attack -attend -attitude -attract -auction -audit -august -aunt -author -auto -autumn -average -avocado -avoid -awake -aware -away -awesome -awful -awkward -axis -baby -bachelor -bacon -badge -bag -balance -balcony -ball -bamboo -banana -banner -bar -barely -bargain -barrel -base -basic -basket -battle -beach -bean -beauty -because -become -beef -before -begin -behave -behind -believe -below -belt -bench -benefit -best -betray -better -between -beyond -bicycle -bid -bike -bind -biology -bird -birth -bitter -black -blade -blame -blanket -blast -bleak -bless -blind -blood -blossom -blouse -blue -blur -blush -board -boat -body -boil -bomb -bone -bonus -book -boost -border -boring -borrow -boss -bottom -bounce -box -boy -bracket -brain -brand -brass -brave -bread -breeze -brick -bridge -brief -bright -bring -brisk -broccoli -broken -bronze -broom -brother -brown -brush -bubble -buddy -budget -buffalo -build -bulb -bulk -bullet -bundle -bunker -burden -burger -burst -bus -business -busy -butter -buyer -buzz -cabbage -cabin -cable -cactus -cage -cake -call -calm -camera -camp -can -canal -cancel -candy -cannon -canoe -canvas -canyon -capable -capital -captain -car -carbon -card -cargo -carpet -carry -cart -case -cash -casino -castle -casual -cat -catalog -catch -category -cattle -caught -cause -caution -cave -ceiling -celery -cement -census -century -cereal -certain -chair -chalk -champion -change -chaos -chapter -charge -chase -chat -cheap -check -cheese -chef -cherry -chest -chicken -chief -child -chimney -choice -choose -chronic -chuckle -chunk -churn -cigar -cinnamon -circle -citizen -city -civil -claim -clap -clarify -claw -clay -clean -clerk -clever -click -client -cliff -climb -clinic -clip -clock -clog -close -cloth -cloud -clown -club -clump -cluster -clutch -coach -coast -coconut -code -coffee -coil -coin -collect -color -column -combine -come -comfort -comic -common -company -concert -conduct -confirm -congress -connect -consider -control -convince -cook -cool -copper -copy -coral -core -corn -correct -cost -cotton -couch -country -couple -course -cousin -cover -coyote -crack -cradle -craft -cram -crane -crash -crater -crawl -crazy -cream -credit -creek -crew -cricket -crime -crisp -critic -crop -cross -crouch -crowd -crucial -cruel -cruise -crumble -crunch -crush -cry -crystal -cube -culture -cup -cupboard -curious -current -curtain -curve -cushion -custom -cute -cycle -dad -damage -damp -dance -danger -daring -dash -daughter -dawn -day -deal -debate -debris -decade -december -decide -decline -decorate -decrease -deer -defense -define -defy -degree -delay -deliver -demand -demise -denial -dentist -deny -depart -depend -deposit -depth -deputy -derive -describe -desert -design -desk -despair -destroy -detail -detect -develop -device -devote -diagram -dial -diamond -diary -dice -diesel -diet -differ -digital -dignity -dilemma -dinner -dinosaur -direct -dirt -disagree -discover -disease -dish -dismiss -disorder -display -distance -divert -divide -divorce -dizzy -doctor -document -dog -doll -dolphin -domain -donate -donkey -donor -door -dose -double -dove -draft -dragon -drama -drastic -draw -dream -dress -drift -drill -drink -drip -drive -drop -drum -dry -duck -dumb -dune -during -dust -dutch -duty -dwarf -dynamic -eager -eagle -early -earn -earth -easily -east -easy -echo -ecology -economy -edge -edit -educate -effort -egg -eight -either -elbow -elder -electric -elegant -element -elephant -elevator -elite -else -embark -embody -embrace -emerge -emotion -employ -empower -empty -enable -enact -end -endless -endorse -enemy -energy -enforce -engage -engine -enhance -enjoy -enlist -enough -enrich -enroll -ensure -enter -entire -entry -envelope -episode -equal -equip -era -erase -erode -erosion -error -erupt -escape -essay -essence -estate -eternal -ethics -evidence -evil -evoke -evolve -exact -example -excess -exchange -excite -exclude -excuse -execute -exercise -exhaust -exhibit -exile -exist -exit -exotic -expand -expect -expire -explain -expose -express -extend -extra -eye -eyebrow -fabric -face -faculty -fade -faint -faith -fall -false -fame -family -famous -fan -fancy -fantasy -farm -fashion -fat -fatal -father -fatigue -fault -favorite -feature -february -federal -fee -feed -feel -female -fence -festival -fetch -fever -few -fiber -fiction -field -figure -file -film -filter -final -find -fine -finger -finish -fire -firm -first -fiscal -fish -fit -fitness -fix -flag -flame -flash -flat -flavor -flee -flight -flip -float -flock -floor -flower -fluid -flush -fly -foam -focus -fog -foil -fold -follow -food -foot -force -forest -forget -fork -fortune -forum -forward -fossil -foster -found -fox -fragile -frame -frequent -fresh -friend -fringe -frog -front -frost -frown -frozen -fruit -fuel -fun -funny -furnace -fury -future -gadget -gain -galaxy -gallery -game -gap -garage -garbage -garden -garlic -garment -gas -gasp -gate -gather -gauge -gaze -general -genius -genre -gentle -genuine -gesture -ghost -giant -gift -giggle -ginger -giraffe -girl -give -glad -glance -glare -glass -glide -glimpse -globe -gloom -glory -glove -glow -glue -goat -goddess -gold -good -goose -gorilla -gospel -gossip -govern -gown -grab -grace -grain -grant -grape -grass -gravity -great -green -grid -grief -grit -grocery -group -grow -grunt -guard -guess -guide -guilt -guitar -gun -gym -habit -hair -half -hammer -hamster -hand -happy -harbor -hard -harsh -harvest -hat -have -hawk -hazard -head -health -heart -heavy -hedgehog -height -hello -helmet -help -hen -hero -hidden -high -hill -hint -hip -hire -history -hobby -hockey -hold -hole -holiday -hollow -home -honey -hood -hope -horn -horror -horse -hospital -host -hotel -hour -hover -hub -huge -human -humble -humor -hundred -hungry -hunt -hurdle -hurry -hurt -husband -hybrid -ice -icon -idea -identify -idle -ignore -ill -illegal -illness -image -imitate -immense -immune -impact -impose -improve -impulse -inch -include -income -increase -index -indicate -indoor -industry -infant -inflict -inform -inhale -inherit -initial -inject -injury -inmate -inner -innocent -input -inquiry -insane -insect -inside -inspire -install -intact -interest -into -invest -invite -involve -iron -island -isolate -issue -item -ivory -jacket -jaguar -jar -jazz -jealous -jeans -jelly -jewel -job -join -joke -journey -joy -judge -juice -jump -jungle -junior -junk -just -kangaroo -keen -keep -ketchup -key -kick -kid -kidney -kind -kingdom -kiss -kit -kitchen -kite -kitten -kiwi -knee -knife -knock -know -lab -label -labor -ladder -lady -lake -lamp -language -laptop -large -later -latin -laugh -laundry -lava -law -lawn -lawsuit -layer -lazy -leader -leaf -learn -leave -lecture -left -leg -legal -legend -leisure -lemon -lend -length -lens -leopard -lesson -letter -level -liar -liberty -library -license -life -lift -light -like -limb -limit -link -lion -liquid -list -little -live -lizard -load -loan -lobster -local -lock -logic -lonely -long -loop -lottery -loud -lounge -love -loyal -lucky -luggage -lumber -lunar -lunch -luxury -lyrics -machine -mad -magic -magnet -maid -mail -main -major -make -mammal -man -manage -mandate -mango -mansion -manual -maple -marble -march -margin -marine -market -marriage -mask -mass -master -match -material -math -matrix -matter -maximum -maze -meadow -mean -measure -meat -mechanic -medal -media -melody -melt -member -memory -mention -menu -mercy -merge -merit -merry -mesh -message -metal -method -middle -midnight -milk -million -mimic -mind -minimum -minor -minute -miracle -mirror -misery -miss -mistake -mix -mixed -mixture -mobile -model -modify -mom -moment -monitor -monkey -monster -month -moon -moral -more -morning -mosquito -mother -motion -motor -mountain -mouse -move -movie -much -muffin -mule -multiply -muscle -museum -mushroom -music -must -mutual -myself -mystery -myth -naive -name -napkin -narrow -nasty -nation -nature -near -neck -need -negative -neglect -neither -nephew -nerve -nest -net -network -neutral -never -news -next -nice -night -noble -noise -nominee -noodle -normal -north -nose -notable -note -nothing -notice -novel -now -nuclear -number -nurse -nut -oak -obey -object -oblige -obscure -observe -obtain -obvious -occur -ocean -october -odor -off -offer -office -often -oil -okay -old -olive -olympic -omit -once -one -onion -online -only -open -opera -opinion -oppose -option -orange -orbit -orchard -order -ordinary -organ -orient -original -orphan -ostrich -other -outdoor -outer -output -outside -oval -oven -over -own -owner -oxygen -oyster -ozone -pact -paddle -page -pair -palace -palm -panda -panel -panic -panther -paper -parade -parent -park -parrot -party -pass -patch -path -patient -patrol -pattern -pause -pave -payment -peace -peanut -pear -peasant -pelican -pen -penalty -pencil -people -pepper -perfect -permit -person -pet -phone -photo -phrase -physical -piano -picnic -picture -piece -pig -pigeon -pill -pilot -pink -pioneer -pipe -pistol -pitch -pizza -place -planet -plastic -plate -play -please -pledge -pluck -plug -plunge -poem -poet -point -polar -pole -police -pond -pony -pool -popular -portion -position -possible -post -potato -pottery -poverty -powder -power -practice -praise -predict -prefer -prepare -present -pretty -prevent -price -pride -primary -print -priority -prison -private -prize -problem -process -produce -profit -program -project -promote -proof -property -prosper -protect -proud -provide -public -pudding -pull -pulp -pulse -pumpkin -punch -pupil -puppy -purchase -purity -purpose -purse -push -put -puzzle -pyramid -quality -quantum -quarter -question -quick -quit -quiz -quote -rabbit -raccoon -race -rack -radar -radio -rail -rain -raise -rally -ramp -ranch -random -range -rapid -rare -rate -rather -raven -raw -razor -ready -real -reason -rebel -rebuild -recall -receive -recipe -record -recycle -reduce -reflect -reform -refuse -region -regret -regular -reject -relax -release -relief -rely -remain -remember -remind -remove -render -renew -rent -reopen -repair -repeat -replace -report -require -rescue -resemble -resist -resource -response -result -retire -retreat -return -reunion -reveal -review -reward -rhythm -rib -ribbon -rice -rich -ride -ridge -rifle -right -rigid -ring -riot -ripple -risk -ritual -rival -river -road -roast -robot -robust -rocket -romance -roof -rookie -room -rose -rotate -rough -round -route -royal -rubber -rude -rug -rule -run -runway -rural -sad -saddle -sadness -safe -sail -salad -salmon -salon -salt -salute -same -sample -sand -satisfy -satoshi -sauce -sausage -save -say -scale -scan -scare -scatter -scene -scheme -school -science -scissors -scorpion -scout -scrap -screen -script -scrub -sea -search -season -seat -second -secret -section -security -seed -seek -segment -select -sell -seminar -senior -sense -sentence -series -service -session -settle -setup -seven -shadow -shaft -shallow -share -shed -shell -sheriff -shield -shift -shine -ship -shiver -shock -shoe -shoot -shop -short -shoulder -shove -shrimp -shrug -shuffle -shy -sibling -sick -side -siege -sight -sign -silent -silk -silly -silver -similar -simple -since -sing -siren -sister -situate -six -size -skate -sketch -ski -skill -skin -skirt -skull -slab -slam -sleep -slender -slice -slide -slight -slim -slogan -slot -slow -slush -small -smart -smile -smoke -smooth -snack -snake -snap -sniff -snow -soap -soccer -social -sock -soda -soft -solar -soldier -solid -solution -solve -someone -song -soon -sorry -sort -soul -sound -soup -source -south -space -spare -spatial -spawn -speak -special -speed -spell -spend -sphere -spice -spider -spike -spin -spirit -split -spoil -sponsor -spoon -sport -spot -spray -spread -spring -spy -square -squeeze -squirrel -stable -stadium -staff -stage -stairs -stamp -stand -start -state -stay -steak -steel -stem -step -stereo -stick -still -sting -stock -stomach -stone -stool -story -stove -strategy -street -strike -strong -struggle -student -stuff -stumble -style -subject -submit -subway -success -such -sudden -suffer -sugar -suggest -suit -summer -sun -sunny -sunset -super -supply -supreme -sure -surface -surge -surprise -surround -survey -suspect -sustain -swallow -swamp -swap -swarm -swear -sweet -swift -swim -swing -switch -sword -symbol -symptom -syrup -system -table -tackle -tag -tail -talent -talk -tank -tape -target -task -taste -tattoo -taxi -teach -team -tell -ten -tenant -tennis -tent -term -test -text -thank -that -theme -then -theory -there -they -thing -this -thought -three -thrive -throw -thumb -thunder -ticket -tide -tiger -tilt -timber -time -tiny -tip -tired -tissue -title -toast -tobacco -today -toddler -toe -together -toilet -token -tomato -tomorrow -tone -tongue -tonight -tool -tooth -top -topic -topple -torch -tornado -tortoise -toss -total -tourist -toward -tower -town -toy -track -trade -traffic -tragic -train -transfer -trap -trash -travel -tray -treat -tree -trend -trial -tribe -trick -trigger -trim -trip -trophy -trouble -truck -true -truly -trumpet -trust -truth -try -tube -tuition -tumble -tuna -tunnel -turkey -turn -turtle -twelve -twenty -twice -twin -twist -two -type -typical -ugly -umbrella -unable -unaware -uncle -uncover -under -undo -unfair -unfold -unhappy -uniform -unique -unit -universe -unknown -unlock -until -unusual -unveil -update -upgrade -uphold -upon -upper -upset -urban -urge -usage -use -used -useful -useless -usual -utility -vacant -vacuum -vague -valid -valley -valve -van -vanish -vapor -various -vast -vault -vehicle -velvet -vendor -venture -venue -verb -verify -version -very -vessel -veteran -viable -vibrant -vicious -victory -video -view -village -vintage -violin -virtual -virus -visa -visit -visual -vital -vivid -vocal -voice -void -volcano -volume -vote -voyage -wage -wagon -wait -walk -wall -walnut -want -warfare -warm -warrior -wash -wasp -waste -water -wave -way -wealth -weapon -wear -weasel -weather -web -wedding -weekend -weird -welcome -west -wet -whale -what -wheat -wheel -when -where -whip -whisper -wide -width -wife -wild -will -win -window -wine -wing -wink -winner -winter -wire -wisdom -wise -wish -witness -wolf -woman -wonder -wood -wool -word -work -world -worry -worth -wrap -wreck -wrestle -wrist -write -wrong -yard -year -yellow -you -young -youth -zebra -zero -zone -zoo \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseApi.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseApi.kt deleted file mode 100644 index 43ac6afa71..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseApi.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.feature.onboarding.api - -import androidx.compose.runtime.Composable -import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen - -/** -[REDACTED_AUTHOR] - */ -interface OnboardingSeedPhraseApi { - @Suppress("TopLevelComposableFunctions") - @Composable - fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt deleted file mode 100644 index 51d9740d8c..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/api/OnboardingSeedPhraseScreen.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.tangem.feature.onboarding.api - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.StrokeCap -import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState -import com.tangem.feature.onboarding.presentation.wallet2.ui.* -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseScreen - -/** -[REDACTED_AUTHOR] - */ -class OnboardingSeedPhraseScreen : OnboardingSeedPhraseApi { - - @Composable - override fun ScreenContent(uiState: OnboardingSeedPhraseState, subScreen: SeedPhraseScreen, progress: Float) { - BackHandler(onBack = uiState.onBackClick) - Column { - TangemLinearProgressIndicator( - progress = { progress }, - color = TangemTheme.colors.icon.primary1, - backgroundColor = TangemTheme.colors.icon.primary1.copy(alpha = 0.4f), - strokeCap = StrokeCap.Round, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(top = TangemTheme.dimens.size8) - .padding(horizontal = TangemTheme.dimens.size16), - ) - Content(subScreen, uiState) - } - } -} - -@Composable -private fun Content(screen: SeedPhraseScreen, uiState: OnboardingSeedPhraseState) { - when (screen) { - SeedPhraseScreen.Intro -> { - IntroScreen( - state = uiState.introState, - ) - } - SeedPhraseScreen.AboutSeedPhrase -> { - AboutSeedPhraseScreen( - state = uiState.aboutState, - ) - } - SeedPhraseScreen.YourSeedPhrase -> { - YourSeedPhraseScreen( - state = uiState.yourSeedPhraseState, - ) - } - SeedPhraseScreen.CheckSeedPhrase -> { - CheckSeedPhraseScreen( - state = uiState.checkSeedPhraseState, - ) - } - SeedPhraseScreen.ImportSeedPhrase -> { - ImportSeedPhraseScreen( - state = uiState.importSeedPhraseState, - ) - } - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt deleted file mode 100644 index 09bc1f8964..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.feature.onboarding.data - -import com.tangem.crypto.bip39.DefaultMnemonic -import com.tangem.crypto.bip39.EntropyLength -import com.tangem.crypto.bip39.Mnemonic -import com.tangem.crypto.bip39.Wordlist -import com.tangem.feature.onboarding.domain.models.MnemonicType - -/** -[REDACTED_AUTHOR] - */ -class DefaultMnemonicRepository( - private val bip39Wordlist: Wordlist, -) : MnemonicRepository { - - private val bip39Words: Set by lazy { bip39Wordlist.words.toHashSet() } - - override fun getWordsDictionary(): Set = bip39Words - - override fun generateDefaultMnemonic(): Mnemonic { - return DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist) - } - - override fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic { - return when (mnemonicType) { - MnemonicType.Mnemonic12 -> DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist) - MnemonicType.Mnemonic24 -> DefaultMnemonic(EntropyLength.Bits256Length, bip39Wordlist) - } - } - - override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist) -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt deleted file mode 100644 index 389ca4d949..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.feature.onboarding.data - -import com.tangem.common.core.TangemSdkError -import com.tangem.crypto.bip39.Mnemonic -import com.tangem.feature.onboarding.domain.models.MnemonicType - -/** -[REDACTED_AUTHOR] - */ -interface MnemonicRepository { - - fun getWordsDictionary(): Set - - @Throws(TangemSdkError.MnemonicException::class) - fun generateDefaultMnemonic(): Mnemonic - - @Throws(TangemSdkError.MnemonicException::class) - fun generateMnemonic(mnemonicType: MnemonicType): Mnemonic - - @Throws(TangemSdkError.MnemonicException::class) - fun createMnemonic(mnemonicString: String): Mnemonic -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/model/CreateWalletResponse.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/model/CreateWalletResponse.kt deleted file mode 100644 index 76f4027a1b..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/model/CreateWalletResponse.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.onboarding.data.model - -import com.tangem.common.extensions.ByteArrayKey -import com.tangem.domain.models.scan.CardDTO -import com.tangem.operations.backup.PrimaryCard -import com.tangem.operations.derivation.ExtendedPublicKeysMap - -/** -[REDACTED_AUTHOR] - */ -data class CreateWalletResponse( - val card: CardDTO, - val derivedKeys: Map = mapOf(), - val primaryCard: PrimaryCard? = null, -) \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/di/OnboardingRouterModule.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/di/OnboardingRouterModule.kt deleted file mode 100644 index acc54e7b9b..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/di/OnboardingRouterModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.feature.onboarding.di - -import com.tangem.feature.onboarding.navigation.DefaultOnboardingRouter -import com.tangem.feature.onboarding.navigation.OnboardingRouter -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.scopes.ActivityScoped - -@Module -@InstallIn(ActivityComponent::class) -internal object OnboardingRouterModule { - - @Provides - @ActivityScoped - fun provideOnboardingRouter(): OnboardingRouter { - return DefaultOnboardingRouter() - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt deleted file mode 100644 index a0ef98bf30..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.feature.onboarding.domain - -import com.tangem.common.core.TangemSdkError -import com.tangem.crypto.bip39.Mnemonic -import com.tangem.crypto.bip39.MnemonicErrorResult -import com.tangem.feature.onboarding.data.MnemonicRepository -import com.tangem.feature.onboarding.domain.models.MnemonicType -import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField -import com.tangem.utils.extensions.isNotWhitespace -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList - -/** -[REDACTED_AUTHOR] - */ -internal class DefaultSeedPhraseInteractor( - private val repository: MnemonicRepository, -) : SeedPhraseInteractor { - - private val partWordFinder: PartWordFinder = PartWordFinder() - - override suspend fun generateMnemonic(): Result { - return runCatching { - repository.generateDefaultMnemonic() - } - } - - override suspend fun generateMnemonics(mnemonicTypes: List): Result> { - return runCatching { - mnemonicTypes.associateWith { - repository.generateMnemonic(it) - } - } - } - - override suspend fun isWordMatch(mnemonicComponents: List?, field: SeedPhraseField, word: String): Boolean { - return mnemonicComponents?.get(field.index) == word - } - - override suspend fun validateMnemonicString(text: String): Result> { - val inputWords = text.split("\\s+".toRegex()).toSet() - val wordsDictionary = repository.getWordsDictionary() - - val invalidWords = inputWords - .filter { it.isNotEmpty() && !wordsDictionary.contains(it) } - .toSet() - - return if (invalidWords.isNotEmpty()) { - Result.failure(SeedPhraseError.InvalidWords(invalidWords)) - } else { - try { - val mnemonic = repository.createMnemonic(text) - Result.success(mnemonic.mnemonicComponents) - } catch (ex: TangemSdkError.MnemonicException) { - val error = ex.mnemonicResult.mapToError() - Result.failure(error) - } - } - } - - override suspend fun getSuggestions( - text: String, - hasSelection: Boolean, - cursorPosition: Int, - ): ImmutableList { - if (text.isEmpty() || cursorPosition == 0 || hasSelection) return persistentListOf() - val word = partWordFinder.getLeadPartOfWord(text, cursorPosition) - ?: return persistentListOf() - - val suggestions = repository.getWordsDictionary() - .filter { it.startsWith(word, ignoreCase = false) && it != word } - .toPersistentList() - - return suggestions - } - - override suspend fun insertSuggestionWord( - text: String, - suggestion: String, - cursorPosition: Int, - ): InsertSuggestionResult { - val textPartLead = text.substring(0, cursorPosition) - val textPartLast = text.substring(cursorPosition, text.length) - val wordPartLast = partWordFinder.getLastPartOfWord(text, suggestion, cursorPosition) - - val textBuilder = StringBuilder() - .append(textPartLead) - .append(wordPartLast) - - if (textPartLast.isEmpty() || textPartLast[0].isNotWhitespace()) { - textBuilder.append(SeedPhraseInteractor.MNEMONIC_DELIMITER) - } - textBuilder.append(textPartLast) - - val textWithSuggestion = textBuilder.toString() - val newCursorPosition = cursorPosition + wordPartLast.length + SeedPhraseInteractor.MNEMONIC_DELIMITER.length - - return InsertSuggestionResult(textWithSuggestion, newCursorPosition) - } -} - -private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) { - MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidWordCount - MnemonicErrorResult.InvalidEntropyLength -> SeedPhraseError.InvalidEntropyLength - MnemonicErrorResult.InvalidWordsFile -> SeedPhraseError.InvalidWordsFile - MnemonicErrorResult.InvalidChecksum -> SeedPhraseError.InvalidChecksum - MnemonicErrorResult.MnenmonicCreationFailed -> SeedPhraseError.MnenmonicCreationFailed - MnemonicErrorResult.NormalizationFailed -> SeedPhraseError.NormalizationFailed - MnemonicErrorResult.UnsupportedLanguage -> SeedPhraseError.UnsupportedLanguage - is MnemonicErrorResult.InvalidWords -> SeedPhraseError.InvalidWords(this.words) - MnemonicErrorResult.InvalidMnemonic -> SeedPhraseError.InvalidMnemonic -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt deleted file mode 100644 index e36bc935bd..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.feature.onboarding.domain - -import com.tangem.common.module.ModuleError -import com.tangem.common.module.ModuleErrorCode -import com.tangem.common.module.ModuleMessage - -/** -[REDACTED_AUTHOR] - * For each pool of errors, use the codes lying in range 100 (200..299, 300..399, etc) - */ -abstract class OnboardingModuleError( - subCode: Int, - override val message: String, - override val data: Any?, -) : ModuleMessage, ModuleError() { - override val code: Int = ModuleErrorCode.ONBOARDING + subCode - - companion object { - internal const val ERROR_CODE_SEED_PHRASE = 100 - } -} - -sealed class SeedPhraseError( - subCode: Int = 0, - message: String? = null, - data: Any? = null, -) : OnboardingModuleError( - subCode = ERROR_CODE_SEED_PHRASE + subCode, - message = message ?: this::class.java.simpleName, - data = data, -) { - object InvalidWordCount : SeedPhraseError(subCode = 0) - object InvalidEntropyLength : SeedPhraseError(subCode = 1) - object InvalidWordsFile : SeedPhraseError(subCode = 2) - object InvalidChecksum : SeedPhraseError(subCode = 3) - object MnenmonicCreationFailed : SeedPhraseError(subCode = 4) - object NormalizationFailed : SeedPhraseError(subCode = 5) - object UnsupportedLanguage : SeedPhraseError(subCode = 6) - data class InvalidWords(val words: Set) : SeedPhraseError(subCode = 7) - object InvalidMnemonic : SeedPhraseError(subCode = 8) -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt deleted file mode 100644 index 97ca9cd8d9..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.feature.onboarding.domain - -import com.tangem.utils.extensions.isNotWhitespace - -/** -[REDACTED_AUTHOR] - * Class which provides helper functions to determine lead or last part of words for suggestions - */ -internal class PartWordFinder { - - /** - * Searches and return part of a word on the left side of the [cursorPosition] into the [text] - */ - fun getLeadPartOfWord(text: String, cursorPosition: Int): String? { - val charText = text.toCharArray().asList() - val cursorIsOutOfTextRange = cursorPosition > charText.size || cursorPosition < 0 - val cursorAtStartOfText = cursorPosition == 0 - val hadWhiteSpaceOnLeftOfCursor = charText[cursorPosition - 1].isWhitespace() - val cursorInMiddleOfWord = charText.size > cursorPosition && charText[cursorPosition].isNotWhitespace() - - @Suppress("ComplexCondition") - if (cursorIsOutOfTextRange || cursorAtStartOfText || hadWhiteSpaceOnLeftOfCursor || cursorInMiddleOfWord) { - return null - } - - for (charIndex in cursorPosition - 1 downTo 0) { - val char = charText[charIndex] - when { - charIndex == 0 -> { - return text.substring(charIndex, cursorPosition) - } - char.isWhitespace() -> { - return text.substring(charIndex + 1, cursorPosition) - } - } - } - - return null - } - - /** - * Returns a substring of the [word] which excludes the first word part found on the left of [cursorPosition] in - * the [text] - */ - fun getLastPartOfWord(text: String, word: String, cursorPosition: Int): String { - val leadPart = getLeadPartOfWord(text, cursorPosition) ?: return "" - - return word.substring(leadPart.length, word.length) - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt deleted file mode 100644 index 37e0c182da..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.feature.onboarding.domain - -import com.tangem.crypto.bip39.Mnemonic -import com.tangem.feature.onboarding.domain.models.MnemonicType -import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField -import kotlinx.collections.immutable.ImmutableList - -/** -[REDACTED_AUTHOR] - */ -interface SeedPhraseInteractor { - suspend fun generateMnemonic(): Result - suspend fun generateMnemonics(mnemonicTypes: List): Result> - suspend fun isWordMatch(mnemonicComponents: List?, field: SeedPhraseField, word: String): Boolean - suspend fun validateMnemonicString(text: String): Result> - suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): ImmutableList - suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult - - companion object { - const val MNEMONIC_DELIMITER = " " - } -} - -data class InsertSuggestionResult( - val text: String, - val cursorPosition: Int, -) \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt deleted file mode 100644 index 108700efe3..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.feature.onboarding.domain.di - -import com.tangem.feature.onboarding.data.MnemonicRepository -import com.tangem.feature.onboarding.domain.DefaultSeedPhraseInteractor -import com.tangem.feature.onboarding.domain.SeedPhraseInteractor -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class OnboardingDomainModule { - - @Provides - @Singleton - fun provideSeedPhraseInteractor(repository: MnemonicRepository): SeedPhraseInteractor { - return DefaultSeedPhraseInteractor(repository) - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/models/MnemonicType.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/models/MnemonicType.kt deleted file mode 100644 index ea4a74f87f..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/models/MnemonicType.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.feature.onboarding.domain.models - -enum class MnemonicType { - Mnemonic12, Mnemonic24; -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/DefaultOnboardingRouter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/DefaultOnboardingRouter.kt deleted file mode 100644 index 0d34ac7cd3..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/DefaultOnboardingRouter.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.tangem.feature.onboarding.navigation - -class DefaultOnboardingRouter : OnboardingRouter \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt deleted file mode 100644 index b6bf1b0f60..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/navigation/OnboardingRouter.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.feature.onboarding.navigation - -/** - * Onboarding router - */ -// TODO: Move to onboarding api module [REDACTED_JIRA] -interface OnboardingRouter \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt deleted file mode 100644 index 9ae8093ab2..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/analytics/SeedPhraseEvents.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent - -sealed class SeedPhraseEvents( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent(ONBOARDING_SEED_PHRASE, event, params) { - - object ButtonGenerateSeedPhrase : SeedPhraseEvents("Button - Generate Seed Phrase") - - object ButtonImportWallet : SeedPhraseEvents("Button - Import Wallet") - object ButtonReadMore : SeedPhraseEvents("Button - Read More") - object ButtonImport : SeedPhraseEvents("Button - Import") - - object IntroScreenOpened : SeedPhraseEvents("Seed Intro Screen Opened") - object GenerationScreenOpened : SeedPhraseEvents("Seed Generation Screen Opened") - object CheckingScreenOpened : SeedPhraseEvents("Seed Checking Screen Opened") - object ImportScreenOpened : SeedPhraseEvents("Import Seed Phrase Screen Opened") - - companion object { - const val ONBOARDING_SEED_PHRASE = "Onboarding / Seed Phrase" - } -} - -sealed class CreateWalletEvents( - event: String, - params: Map = mapOf(), -) : AnalyticsEvent(ONBOARDING_CREATE_WALLET, event, params) { - - object OnboardingSeedButtonCreateWallet : CreateWalletEvents("Button - Create Wallet") - - object OnboardingSeedButtonOtherOptions : CreateWalletEvents("Button - Other Options") - companion object { - const val ONBOARDING_CREATE_WALLET = "Onboarding / Create Wallet" - } -} - -enum class SeedPhraseSource { - IMPORTED, GENERATED -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt deleted file mode 100644 index 842a7a5141..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.model - -/** -[REDACTED_AUTHOR] - */ -data class ButtonState( - val enabled: Boolean = true, - val isClickable: Boolean = true, - val showProgress: Boolean = false, - val onClick: () -> Unit, -) \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt deleted file mode 100644 index db8f0d469b..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.model - -import androidx.annotation.StringRes - -data class DescriptionResource( - @StringRes val titleRes: Int, - @StringRes val subTitleRes: Int, -) \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt deleted file mode 100644 index a8300c9a76..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.model - -import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.feature.onboarding.domain.SeedPhraseError -import com.tangem.feature.onboarding.domain.models.MnemonicType -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf - -/** -[REDACTED_AUTHOR] - */ -data class OnboardingSeedPhraseState( - val introState: IntroState, - val aboutState: AboutState, - val yourSeedPhraseState: YourSeedPhraseState, - val checkSeedPhraseState: CheckSeedPhraseState, - val importSeedPhraseState: ImportSeedPhraseState, - val menuButtonChat: ButtonState, - val onBackClick: () -> Unit, -) - -data class IntroState( - val cardImageUrl: String? = null, - val buttonCreateWallet: ButtonState, - val buttonOtherOptions: ButtonState, -) - -data class AboutState( - val buttonReadMoreAboutSeedPhrase: ButtonState, - val buttonGenerateSeedPhrase: ButtonState, - val buttonImportSeedPhrase: ButtonState, -) - -data class YourSeedPhraseState( - val segmentSeedState: SegmentSeedState, - val mnemonicGridItems: PersistentList = persistentListOf(), - val buttonContinue: ButtonState, -) - -data class SegmentSeedState( - val seedSegments: PersistentList, - val selectedSeedType: SegmentSeedType, - val onSelectType: (SegmentSeedType) -> Unit, -) - -enum class SegmentSeedType(val count: Int) { - SEED_12(count = 12), SEED_24(count = 24); - - fun toMnemonicType(): MnemonicType { - return when (this) { - SEED_12 -> MnemonicType.Mnemonic12 - SEED_24 -> MnemonicType.Mnemonic24 - } - } - - companion object { - fun fromMnemonicType(mnemonicType: MnemonicType): SegmentSeedType { - return when (mnemonicType) { - MnemonicType.Mnemonic12 -> SEED_12 - MnemonicType.Mnemonic24 -> SEED_24 - } - } - } -} - -data class MnemonicGridItem( - val index: Int, - val mnemonic: String, -) - -data class CheckSeedPhraseState( - val tvSecondPhrase: TextFieldState, - val tvSeventhPhrase: TextFieldState, - val tvEleventhPhrase: TextFieldState, - val buttonCreateWallet: ButtonState, -) - -data class ImportSeedPhraseState( - val fieldSeedPhrase: TextFieldState, - val fieldPassphrase: TextFieldState, - val onSuggestedPhraseClick: (Int) -> Unit, - val onPassphraseInfoClick: () -> Unit, - val buttonCreateWallet: ButtonState, - val invalidWords: Set = emptySet(), - val suggestionsList: ImmutableList = persistentListOf(), - val error: SeedPhraseError? = null, - val bottomSheetConfig: TangemBottomSheetConfig? = null, -) - -data class ShowPassphraseInfoBottomSheetContent(val onOkClick: () -> Unit) : TangemBottomSheetConfigContent - -data class TextFieldState( - val onTextFieldValueChanged: (TextFieldValue) -> Unit, - val textFieldValue: TextFieldValue = TextFieldValue(), - val label: String? = null, - val isError: Boolean = false, - val isFocused: Boolean = false, - val onFocusChanged: (Boolean) -> Unit = {}, -) \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt deleted file mode 100644 index 91db0f6407..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.model - -import androidx.compose.ui.text.input.TextFieldValue - -/** -[REDACTED_AUTHOR] - */ -data class UiActions( - val introActions: IntroUiAction, - val aboutActions: AboutUiAction, - val yourSeedPhraseActions: YourSeedPhraseUiAction, - val checkSeedPhraseActions: CheckSeedPhraseUiAction, - val importSeedPhraseActions: ImportSeedPhraseUiAction, - val menuChatClick: () -> Unit, - val menuNavigateBackClick: () -> Unit, -) - -data class IntroUiAction( - val buttonCreateWalletClick: () -> Unit, - val buttonOtherOptionsClick: () -> Unit, -) - -data class AboutUiAction( - val buttonReadMoreAboutSeedPhraseClick: () -> Unit, - val buttonGenerateSeedPhraseClick: () -> Unit, - val buttonImportSeedPhraseClick: () -> Unit, -) - -data class YourSeedPhraseUiAction( - val buttonContinueClick: () -> Unit, - val onSelectType: (SegmentSeedType) -> Unit, -) - -data class CheckSeedPhraseUiAction( - val secondTextFieldAction: TextFieldUiAction, - val seventhTextFieldAction: TextFieldUiAction, - val eleventhTextFieldAction: TextFieldUiAction, - val buttonCreateWalletClick: () -> Unit, -) - -data class ImportSeedPhraseUiAction( - val phraseTextFieldAction: TextFieldUiAction, - val passTextFieldAction: TextFieldUiAction, - val suggestedPhraseClick: (Int) -> Unit, - val onPassphraseInfoClick: () -> Unit, - val buttonCreateWalletClick: () -> Unit, -) - -data class TextFieldUiAction( - val onTextFieldChanged: (TextFieldValue) -> Unit, - val onFocusChanged: (Boolean) -> Unit = {}, -) - - - -enum class SeedPhraseField(val index: Int) { - Second(index = 1), - Seventh(index = 6), - Eleventh(index = 10), -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt deleted file mode 100644 index e0cab80df9..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt +++ /dev/null @@ -1,151 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui - -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.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.res.painterResource -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerW8 -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.onboarding.R -import com.tangem.feature.onboarding.presentation.wallet2.model.AboutState -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun AboutSeedPhraseScreen(state: AboutState, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxSize(), - ) { - Box( - modifier = Modifier.weight(weight = 0.8f), - ) { - EditIconBlock() - } - Box( - modifier = Modifier.weight(weight = 1.2f), - ) { - Column { - OnboardingDescriptionBlock { - Description( - titleRes = R.string.onboarding_seed_intro_title, - subTitleRes = R.string.onboarding_seed_intro_message, - ) - } - ReadMoreBlock(state) - } - } - Box { - OnboardingActionBlock( - firstActionContent = { - SecondaryButton( - modifier = Modifier - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_generate), - enabled = state.buttonGenerateSeedPhrase.enabled, - showProgress = state.buttonGenerateSeedPhrase.showProgress, - onClick = state.buttonGenerateSeedPhrase.onClick, - ) - }, - secondActionContent = { - SecondaryButton( - modifier = Modifier - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.onboarding_seed_intro_button_import), - enabled = state.buttonImportSeedPhrase.enabled, - showProgress = state.buttonImportSeedPhrase.showProgress, - onClick = state.buttonImportSeedPhrase.onClick, - ) - }, - ) - } - } -} - -@Composable -private fun EditIconBlock() { - Column( - modifier = Modifier - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - Image( - modifier = Modifier - .size(TangemTheme.dimens.size44), - painter = painterResource(id = R.drawable.ic_onboarding_text_edit_56), - contentDescription = null, - ) - SpacerH32() - Box( - modifier = Modifier - .background( - color = TangemTheme.colors.icon.warning.copy(alpha = 0.12f), - shape = RoundedCornerShape(TangemTheme.dimens.radius8), - ), - ) { - Text( - modifier = Modifier - .padding( - horizontal = TangemTheme.dimens.size12, - vertical = TangemTheme.dimens.size4, - ), - text = stringResourceSafe(id = R.string.onboarding_seed_phrase_intro_legacy), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.body1, - ) - } - } -} - -@Composable -private fun ReadMoreBlock(state: AboutState) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = TangemTheme.dimens.size16), - ) { - val shape = TangemTheme.shapes.roundedCornersLarge - Row( - modifier = Modifier - .align(Alignment.Center) - .clip(shape) - .border( - width = TangemTheme.dimens.size1, - color = TangemTheme.colors.stroke.primary, - shape = shape, - ) - .clickable(onClick = state.buttonReadMoreAboutSeedPhrase.onClick) - .padding( - horizontal = TangemTheme.dimens.size16, - vertical = TangemTheme.dimens.size8, - ), - ) { - Icon( - painter = painterResource(id = R.drawable.ic_arrow_top_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.primary1, - ) - SpacerW8() - Text( - text = stringResourceSafe(id = R.string.onboarding_seed_button_read_more), - color = TangemTheme.colors.text.primary1, - ) - } - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt deleted file mode 100644 index bf29010072..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.R -import com.tangem.core.ui.components.OutlineTextField -import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseState -import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock - -/** -[REDACTED_AUTHOR] - */ -@Suppress("ReusedModifierInstance") -@Composable -fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxSize(), - ) { - LazyColumn( - modifier = modifier - .weight(1f) - .fillMaxWidth(), - ) { - item { - OnboardingDescriptionBlock( - modifier = Modifier - .weight(1f) - .padding(top = TangemTheme.dimens.size48) - .padding(horizontal = TangemTheme.dimens.size16), - ) { - Description( - titleRes = R.string.onboarding_seed_user_validation_title, - subTitleRes = R.string.onboarding_seed_user_validation_message, - ) - } - } - item { - CheckSeedPhraseBlock( - modifier = Modifier - .imePadding() - .weight(1f) - .padding(top = TangemTheme.dimens.size20) - .padding(horizontal = TangemTheme.dimens.size16), - state = state, - ) - } - } - OnboardingActionBlock( - firstActionContent = { - PrimaryButtonIconEnd( - modifier = Modifier - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet), - iconResId = R.drawable.ic_tangem_24, - enabled = state.buttonCreateWallet.enabled, - showProgress = state.buttonCreateWallet.showProgress, - onClick = state.buttonCreateWallet.onClick, - ) - }, - ) - } -} - -@Composable -private fun CheckSeedPhraseBlock(state: CheckSeedPhraseState, modifier: Modifier = Modifier) { - Column(modifier) { - CheckSeedPhraseTextField(state = state.tvSecondPhrase, imeAction = ImeAction.Next) - CheckSeedPhraseTextField(state = state.tvSeventhPhrase, imeAction = ImeAction.Next) - CheckSeedPhraseTextField(state = state.tvEleventhPhrase, imeAction = ImeAction.Done) - } -} - -@Composable -private fun CheckSeedPhraseTextField(state: TextFieldState, imeAction: ImeAction) { - val focusManager = LocalFocusManager.current - - DisableContextMenu { - OutlineTextField( - value = state.textFieldValue, - onValueChange = state.onTextFieldValueChanged, - label = state.label, - isError = state.isError, - keyboardOptions = KeyboardOptions( - autoCorrectEnabled = false, - keyboardType = KeyboardType.Password, - imeAction = imeAction, - ), - keyboardActions = KeyboardActions( - onDone = { focusManager.clearFocus() }, - onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) }, - ), - ) - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt deleted file mode 100644 index 7c03fef7bc..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ /dev/null @@ -1,268 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui - -import android.content.res.Configuration -import androidx.compose.animation.* -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.input.KeyboardType -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 androidx.compose.ui.unit.IntOffset -import com.tangem.core.ui.components.* -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.onboarding.R -import com.tangem.feature.onboarding.presentation.wallet2.model.ButtonState -import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState -import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation -import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseErrorConverter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) { - val keyboard by keyboardAsState() - Box( - modifier = modifier.fillMaxSize(), - ) { - Column { - OnboardingDescriptionBlock( - modifier = Modifier.padding(top = TangemTheme.dimens.size16), - ) { - DescriptionSubTitleText(text = stringResourceSafe(id = R.string.onboarding_seed_import_message)) - } - PhraseBlock( - modifier = Modifier - .padding(top = TangemTheme.dimens.size62) - .padding(horizontal = TangemTheme.dimens.size16), - state = state, - ) - - PassphraseBlock( - modifier = Modifier.padding(horizontal = TangemTheme.dimens.size16), - passphraseField = state.fieldPassphrase, - onPassphraseInfoClick = state.onPassphraseInfoClick, - ) - - OnboardingActionBlock( - modifier = Modifier.padding(top = TangemTheme.dimens.size16), - firstActionContent = { - PrimaryButtonIconEnd( - modifier = Modifier - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_import), - iconResId = R.drawable.ic_tangem_24, - enabled = state.buttonCreateWallet.enabled, - showProgress = state.buttonCreateWallet.showProgress, - onClick = state.buttonCreateWallet.onClick, - ) - }, - ) - } - - if (keyboard is Keyboard.Opened) { - SuggestionsBlock( - modifier = Modifier - .align(Alignment.BottomCenter) - .imePadding(), - suggestionsList = state.suggestionsList, - onClick = { index -> state.onSuggestedPhraseClick(index) }, - ) - } - - if (state.bottomSheetConfig != null) { - PassphraseInfoBottomSheet(config = state.bottomSheetConfig) - } - } -} - -@Composable -private fun PassphraseBlock( - passphraseField: TextFieldState, - onPassphraseInfoClick: () -> Unit, - modifier: Modifier = Modifier, -) { - OutlineTextFieldWithIcon( - modifier = modifier.fillMaxWidth(), - value = passphraseField.textFieldValue, - onValueChange = passphraseField.onTextFieldValueChanged, - iconResId = R.drawable.ic_information_24, - iconColor = TangemTheme.colors.icon.informative, - label = stringResourceSafe(id = R.string.common_passphrase), - placeholder = stringResourceSafe(id = R.string.send_optional_field), - onIconClick = onPassphraseInfoClick, - keyboardOptions = KeyboardOptions( - autoCorrectEnabled = false, - keyboardType = KeyboardType.Password, - ), - ) -} - -@Composable -private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) { - // TODO: replace by TextReference - val errorConverter = remember { SeedPhraseErrorConverter() } - - Column( - modifier = modifier.fillMaxWidth(), - ) { - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size142), - value = state.fieldSeedPhrase.textFieldValue, - onValueChange = state.fieldSeedPhrase.onTextFieldValueChanged, - textStyle = TangemTheme.typography.body1, - singleLine = false, - visualTransformation = InvalidWordsColorTransformation( - wordsToBrush = state.invalidWords, - style = SpanStyle(color = TangemTheme.colors.text.warning), - ), - colors = TangemTextFieldsDefault.defaultTextFieldColors, - keyboardOptions = KeyboardOptions( - autoCorrectEnabled = false, - keyboardType = KeyboardType.Password, - ), - ) - - Box( - modifier = Modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size32), - ) { - val message = errorConverter.convert(LocalContext.current to state.error) - if (message != null) { - Text( - modifier = Modifier.fillMaxSize(), - text = message, - style = TangemTheme.typography.caption2.copy( - color = TangemTheme.colors.text.warning, - ), - ) - } - } - } -} - -@Suppress("ReusedModifierInstance") -@Composable -private fun SuggestionsBlock( - suggestionsList: ImmutableList, - onClick: (Int) -> Unit, - modifier: Modifier = Modifier, -) { - AnimatedVisibility( - modifier = modifier, - enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }), - exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(), - visible = suggestionsList.isNotEmpty(), - ) { - LazyRow( - contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16), - ) { - items(suggestionsList.size) { index -> - SuggestionButton( - modifier = Modifier.rowPadding( - index = index, - rowSize = suggestionsList.size, - outSide = TangemTheme.dimens.size0, - inSide = TangemTheme.dimens.size4, - ), - text = suggestionsList[index], - onClick = { onClick(index) }, - ) - } - } - } -} - -@Composable -private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) { - Box(modifier = modifier.clickable(onClick = onClick)) { - Notifier( - text = text, - backgroundColor = TangemTheme.colors.icon.primary1, - textColor = TangemTheme.colors.text.primary2, - ) - } -} - -private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) { - 0 -> this.padding(start = outSide, end = inSide) - rowSize - 1 -> this.padding(start = inSide, end = outSide) - else -> this.padding(horizontal = inSide) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SuggestionsBlockPreview( - @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, -) { - TangemThemePreview { - SuggestionsBlock( - suggestionsList = suggestions, - onClick = {}, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ImportSeedPhraseScreenPreview( - @PreviewParameter(SuggestionsPreviewParamsProvider::class) suggestions: ImmutableList, -) { - TangemThemePreview { - Box(modifier = Modifier.background(TangemTheme.colors.background.primary)) { - ImportSeedPhraseScreen( - ImportSeedPhraseState( - fieldSeedPhrase = TextFieldState(onTextFieldValueChanged = {}), - fieldPassphrase = TextFieldState(onTextFieldValueChanged = {}), - onSuggestedPhraseClick = {}, - onPassphraseInfoClick = {}, - buttonCreateWallet = ButtonState( - enabled = true, - isClickable = true, - showProgress = false, - onClick = {}, - ), - suggestionsList = suggestions, - ), - ) - } - } -} - -private class SuggestionsPreviewParamsProvider : CollectionPreviewParameterProvider>( - collection = listOf( - persistentListOf( - "one", - "each", - "explorer", - "unknown", - ), - ), -) \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt deleted file mode 100644 index 5691a1b124..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.PrimaryButtonIconEnd -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.feature.onboarding.R -import com.tangem.feature.onboarding.presentation.wallet2.model.IntroState -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun IntroScreen(state: IntroState, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxSize(), - ) { - Box( - modifier = Modifier.weight(weight = 1.5f), - ) { - CardImageBlock(state.cardImageUrl) - } - Box( - modifier = Modifier.weight(weight = 1f), - ) { - OnboardingDescriptionBlock { - Description( - titleRes = R.string.onboarding_create_wallet_options_title, - subTitleRes = R.string.onboarding_create_wallet_options_message, - ) - } - } - Box { - OnboardingActionBlock( - firstActionContent = { - PrimaryButtonIconEnd( - modifier = Modifier - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.onboarding_create_wallet_button_create_wallet), - iconResId = R.drawable.ic_tangem_24, - enabled = state.buttonCreateWallet.enabled, - showProgress = state.buttonCreateWallet.showProgress, - onClick = state.buttonCreateWallet.onClick, - ) - }, - secondActionContent = { - SecondaryButton( - modifier = Modifier - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.onboarding_create_wallet_options_button_options), - enabled = state.buttonOtherOptions.enabled, - showProgress = state.buttonOtherOptions.showProgress, - onClick = state.buttonOtherOptions.onClick, - ) - }, - ) - } - } -} - -@Composable -private fun CardImageBlock(cardImageUrl: String? = null) { - Box(modifier = Modifier.fillMaxSize()) { - val cardImageModifier = Modifier - .fillMaxSize() - .align(Alignment.Center) - - SubcomposeAsyncImage( - modifier = cardImageModifier.padding(horizontal = TangemTheme.dimens.size34), - model = ImageRequest.Builder(LocalContext.current) - .data(cardImageUrl) - .crossfade(true) - .build(), - loading = { CardPlaceHolder(cardImageModifier) }, - error = { CardPlaceHolder(cardImageModifier) }, - contentDescription = null, - ) - } -} - -@Composable -private fun CardPlaceHolder(modifier: Modifier = Modifier) { - Image( - modifier = modifier, - painter = painterResource(id = R.drawable.card_placeholder_black), - contentScale = ContentScale.Fit, - contentDescription = null, - ) -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt deleted file mode 100644 index a2912a2666..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/PassphraseInfoBottomSheet.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.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.layout.size -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -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.core.ui.res.TangemThemePreview -import com.tangem.feature.onboarding.R -import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent - -@Composable -fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.primary, - ) { content: ShowPassphraseInfoBottomSheetContent -> - PassphraseInfoBottomSheetContent(content) - } -} - -@Composable -fun PassphraseInfoBottomSheetContent(content: ShowPassphraseInfoBottomSheetContent) { - Column( - modifier = Modifier - .background(color = TangemTheme.colors.background.primary) - .fillMaxWidth(), - ) { - Icon( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(top = TangemTheme.dimens.size40) - .size(TangemTheme.dimens.size48), - painter = painterResource(id = R.drawable.ic_information_24), - tint = TangemTheme.colors.icon.accent, - contentDescription = null, - ) - - Text( - text = stringResourceSafe(id = R.string.common_passphrase), - modifier = Modifier - .padding(top = TangemTheme.dimens.size40) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h2, - ) - - Text( - text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description), - modifier = Modifier - .padding(top = TangemTheme.dimens.size16) - .padding(horizontal = TangemTheme.dimens.size24) - .align(Alignment.CenterHorizontally), - color = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2, - textAlign = TextAlign.Center, - ) - - PrimaryButton( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.size16) - .padding(top = TangemTheme.dimens.size40) - .padding(bottom = TangemTheme.dimens.size32) - .fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_ok), - onClick = content.onOkClick, - ) - } -} - -@Preview -@Composable -private fun PassphraseInfoBottomSheetContentPreview() { - TangemThemePreview { - PassphraseInfoBottomSheetContent(ShowPassphraseInfoBottomSheetContent { }) - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt deleted file mode 100644 index 2f7f864d6e..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.extensions.pluralStringResourceSafe -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.onboarding.R -import com.tangem.feature.onboarding.presentation.wallet2.model.* -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionTitleText -import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun YourSeedPhraseScreen(state: YourSeedPhraseState, modifier: Modifier = Modifier) { - Box( - modifier = modifier.fillMaxSize(), - ) { - Column( - modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing92) - .verticalScroll(rememberScrollState()) - .fillMaxSize(), - ) { - SegmentSeedBlock( - modifier = Modifier.padding(top = TangemTheme.dimens.spacing20), - state = state.segmentSeedState, - ) - - TitleBlock(state.segmentSeedState) - - SeedPhraseGridBlock( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing20) - .padding(horizontal = TangemTheme.dimens.size16), - mnemonicGridItems = state.mnemonicGridItems, - ) - } - OnboardingActionBlock( - modifier = Modifier.align(Alignment.BottomCenter), - firstActionContent = { - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_continue), - onClick = state.buttonContinue.onClick, - ) - }, - ) - } -} - -@Composable -private fun TitleBlock(state: SegmentSeedState) { - Box( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.size36) - .padding(top = TangemTheme.dimens.spacing20) - .fillMaxWidth(), - ) { - Column { - DescriptionTitleText(text = stringResourceSafe(id = R.string.onboarding_seed_generate_title)) - SpacerH16() - DescriptionSubTitleText( - text = pluralStringResourceSafe( - id = R.plurals.onboarding_seed_generate_message_words_count, - count = state.selectedSeedType.count, - state.selectedSeedType.count, - ), - ) - } - } -} - -@Composable -private fun SegmentSeedBlock(state: SegmentSeedState, modifier: Modifier = Modifier) { - SegmentedButtons( - modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing76), - config = state.seedSegments, - initialSelectedItem = state.selectedSeedType, - onClick = { - state.onSelectType(it) - }, - ) { - Text( - text = pluralStringResourceSafe( - id = R.plurals.onboarding_seed_generate_words_count, - count = it.count, - it.count, - ), - modifier = Modifier - .padding(vertical = TangemTheme.dimens.spacing10) - .fillMaxWidth(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList, modifier: Modifier = Modifier) { - VerticalGrid( - modifier = modifier, - items = mnemonicGridItems, - columns = 2, - ) { item -> - Row( - modifier = Modifier.padding(all = TangemTheme.dimens.size8), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.width(TangemTheme.dimens.size40), - text = "${item.index}.", - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - Text( - text = item.mnemonic, - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - ) - } - } -} - -@Composable -private inline fun VerticalGrid( - items: ImmutableList, - columns: Int, - modifier: Modifier = Modifier, - crossinline content: @Composable (T) -> Unit, -) { - val columnLength = items.size / columns - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - for (index in 0 until columns) { - Column { - for (i in 0 until columnLength) { - val item = items[index * columnLength + i] - content(item) - } - } - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun YourSeedPhraseScreenPreview_Light() { - TangemThemePreview { - YourSeedPhraseScreen( - state = YourSeedPhraseState( - segmentSeedState = SegmentSeedState( - seedSegments = persistentListOf( - SegmentSeedType.SEED_12, - SegmentSeedType.SEED_24, - ), - selectedSeedType = SegmentSeedType.SEED_24, - onSelectType = {}, - ), - mnemonicGridItems = (1..24).map { MnemonicGridItem(it, it.toString()) }.toPersistentList(), - buttonContinue = ButtonState( - enabled = true, - isClickable = true, - showProgress = false, - onClick = {}, - ), - ), - modifier = Modifier - .fillMaxSize() - .background(color = TangemTheme.colors.background.primary), - ) - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt deleted file mode 100644 index 986bcac496..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.res.TangemTheme - -/** -[REDACTED_AUTHOR] - */ -@Composable -fun OnboardingActionBlock( - modifier: Modifier = Modifier, - firstActionContent: @Composable (() -> Unit)? = null, - secondActionContent: @Composable (() -> Unit)? = null, -) { - if (firstActionContent == null && secondActionContent == null) return - - Box( - modifier = modifier - .padding(horizontal = TangemTheme.dimens.size16) - .padding( - top = TangemTheme.dimens.size8, - bottom = TangemTheme.dimens.size32, - ), - ) { - Column( - modifier = Modifier.align(Alignment.BottomCenter), - ) { - firstActionContent?.invoke() - if (firstActionContent != null && secondActionContent != null) SpacerH12() - secondActionContent?.invoke() - } - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt deleted file mode 100644 index 52806b14c3..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui.components - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.sp -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme - -/** - * UI component witch provides single and carousel description for the onboarding process - */ -@Composable -fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composable () -> Unit) { - Box( - modifier = modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.size36), - ) { - content() - } -} - -@Composable -fun Description(@StringRes titleRes: Int, @StringRes subTitleRes: Int) { - Column { - DescriptionTitleText(text = stringResourceSafe(id = titleRes)) - SpacerH16() - DescriptionSubTitleText(text = stringResourceSafe(id = subTitleRes)) - } -} - -@Composable -fun DescriptionTitleText(text: String) { - Text( - text = text, - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) -} - -@Composable -fun DescriptionSubTitleText(text: String) { - // the text style made similar to TextViewOnboarding.Body - Text( - text = text, - style = TangemTheme.typography.body1.copy( - lineHeight = 20.sp, - letterSpacing = 0.03.sp, - ), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), - ) -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt deleted file mode 100644 index aad9659b9d..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders - -import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState -import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField - -/** -[REDACTED_AUTHOR] - */ -class CheckSeedPhraseStateBuilder { - - fun updateTextField( - uiState: OnboardingSeedPhraseState, - field: SeedPhraseField, - textFieldValue: TextFieldValue, - ): OnboardingSeedPhraseState = when (field) { - SeedPhraseField.Second -> uiState.copy( - checkSeedPhraseState = uiState.checkSeedPhraseState.copy( - tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy( - textFieldValue = textFieldValue, - ), - ), - ) - SeedPhraseField.Seventh -> uiState.copy( - checkSeedPhraseState = uiState.checkSeedPhraseState.copy( - tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy( - textFieldValue = textFieldValue, - ), - ), - ) - SeedPhraseField.Eleventh -> uiState.copy( - checkSeedPhraseState = uiState.checkSeedPhraseState.copy( - tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy( - textFieldValue = textFieldValue, - ), - ), - ) - } - - fun updateTextFieldError( - uiState: OnboardingSeedPhraseState, - field: SeedPhraseField, - hasError: Boolean, - ): OnboardingSeedPhraseState = when (field) { - SeedPhraseField.Second -> uiState.copy( - checkSeedPhraseState = uiState.checkSeedPhraseState.copy( - tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy( - isError = hasError, - ), - ), - ) - SeedPhraseField.Seventh -> uiState.copy( - checkSeedPhraseState = uiState.checkSeedPhraseState.copy( - tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy( - isError = hasError, - ), - ), - ) - SeedPhraseField.Eleventh -> uiState.copy( - checkSeedPhraseState = uiState.checkSeedPhraseState.copy( - tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy( - isError = hasError, - ), - ), - ) - } - - fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState { - return uiState.copy( - checkSeedPhraseState = uiState.checkSeedPhraseState.copy( - buttonCreateWallet = uiState.checkSeedPhraseState.buttonCreateWallet.copy( - enabled = enabled, - ), - ), - ) - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt deleted file mode 100644 index 5562ff3af5..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders - -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.TextFieldValue -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.feature.onboarding.domain.InsertSuggestionResult -import com.tangem.feature.onboarding.domain.SeedPhraseError -import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState -import com.tangem.feature.onboarding.presentation.wallet2.model.ShowPassphraseInfoBottomSheetContent -import kotlinx.collections.immutable.ImmutableList - -/** -[REDACTED_AUTHOR] - */ -class ImportSeedPhraseStateBuilder { - - fun updateSeedPhraseTextField( - uiState: OnboardingSeedPhraseState, - textFieldValue: TextFieldValue, - ): OnboardingSeedPhraseState = uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - fieldSeedPhrase = uiState.importSeedPhraseState.fieldSeedPhrase.copy( - textFieldValue = textFieldValue, - ), - ), - ) - - fun updatePassPhraseTextField( - uiState: OnboardingSeedPhraseState, - textFieldValue: TextFieldValue, - ): OnboardingSeedPhraseState = uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - fieldPassphrase = uiState.importSeedPhraseState.fieldPassphrase.copy( - textFieldValue = textFieldValue, - ), - ), - ) - - fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState { - return uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - buttonCreateWallet = uiState.importSeedPhraseState.buttonCreateWallet.copy( - enabled = enabled, - ), - ), - ) - } - - fun updateInvalidWords(uiState: OnboardingSeedPhraseState, invalidWords: Set): OnboardingSeedPhraseState = - uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - invalidWords = invalidWords, - ), - ) - - fun updateSuggestions( - uiState: OnboardingSeedPhraseState, - suggestions: ImmutableList, - ): OnboardingSeedPhraseState = uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - suggestionsList = suggestions, - ), - ) - - fun showPassphraseInfoBottomSheet( - uiState: OnboardingSeedPhraseState, - onDismiss: () -> Unit, - ): OnboardingSeedPhraseState { - return uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - bottomSheetConfig = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = onDismiss, - content = ShowPassphraseInfoBottomSheetContent(onDismiss), - ), - ), - ) - } - - fun dismissPassphraseBottomSheet(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState { - return uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - bottomSheetConfig = uiState.importSeedPhraseState.bottomSheetConfig?.copy( - isShown = false, - ), - ), - ) - } - - fun insertSuggestionWord( - uiState: OnboardingSeedPhraseState, - insertResult: InsertSuggestionResult, - ): OnboardingSeedPhraseState { - val newTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.copy( - text = insertResult.text, - selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition), - ) - return updateSeedPhraseTextField(uiState, newTextFieldValue) - } - - fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState { - return uiState.copy( - importSeedPhraseState = uiState.importSeedPhraseState.copy( - error = error, - ), - ) - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt deleted file mode 100644 index 3a27500c46..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt +++ /dev/null @@ -1,197 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders - -import com.tangem.feature.onboarding.presentation.wallet2.model.* -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf - -/** -[REDACTED_AUTHOR] - */ -class StateBuilder( - private val uiActions: UiActions, -) { - - val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder() - val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder() - - fun init(selectedSeedType: SegmentSeedType): OnboardingSeedPhraseState = OnboardingSeedPhraseState( - introState = getInitialIntroState(), - aboutState = getInitialAboutState(), - yourSeedPhraseState = getInitialYourSeedPhraseState(selectedSeedType), - checkSeedPhraseState = getInitialCheckSeedPhraseState(), - importSeedPhraseState = getInitialImportSeedPhraseState(), - menuButtonChat = ButtonState( - onClick = uiActions.menuChatClick, - ), - onBackClick = uiActions.menuNavigateBackClick, - ) - - private fun getInitialImportSeedPhraseState(): ImportSeedPhraseState { - return ImportSeedPhraseState( - fieldSeedPhrase = TextFieldState( - onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged, - ), - fieldPassphrase = TextFieldState( - onTextFieldValueChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.importSeedPhraseActions.passTextFieldAction.onFocusChanged, - ), - onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick, - onPassphraseInfoClick = uiActions.importSeedPhraseActions.onPassphraseInfoClick, - buttonCreateWallet = ButtonState( - enabled = false, - onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick, - ), - ) - } - - private fun getInitialCheckSeedPhraseState(): CheckSeedPhraseState { - return CheckSeedPhraseState( - tvSecondPhrase = TextFieldState( - label = "2", - isFocused = true, - onTextFieldValueChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onFocusChanged, - ), - tvSeventhPhrase = TextFieldState( - label = "7", - isFocused = false, - onTextFieldValueChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onFocusChanged, - ), - tvEleventhPhrase = TextFieldState( - label = "11", - isFocused = false, - onTextFieldValueChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onFocusChanged, - ), - buttonCreateWallet = ButtonState( - enabled = false, - onClick = uiActions.checkSeedPhraseActions.buttonCreateWalletClick, - ), - ) - } - - private fun getInitialYourSeedPhraseState(selectedSeedType: SegmentSeedType): YourSeedPhraseState { - return YourSeedPhraseState( - segmentSeedState = SegmentSeedState( - seedSegments = persistentListOf( - SegmentSeedType.SEED_12, - SegmentSeedType.SEED_24, - ), - selectedSeedType = selectedSeedType, - onSelectType = uiActions.yourSeedPhraseActions.onSelectType, - ), - buttonContinue = ButtonState( - onClick = uiActions.yourSeedPhraseActions.buttonContinueClick, - ), - ) - } - - private fun getInitialAboutState(): AboutState { - return AboutState( - buttonReadMoreAboutSeedPhrase = ButtonState( - onClick = uiActions.aboutActions.buttonReadMoreAboutSeedPhraseClick, - ), - buttonGenerateSeedPhrase = ButtonState( - onClick = uiActions.aboutActions.buttonGenerateSeedPhraseClick, - ), - buttonImportSeedPhrase = ButtonState( - onClick = uiActions.aboutActions.buttonImportSeedPhraseClick, - ), - ) - } - - private fun getInitialIntroState(): IntroState { - return IntroState( - buttonCreateWallet = ButtonState( - onClick = uiActions.introActions.buttonCreateWalletClick, - ), - buttonOtherOptions = ButtonState( - onClick = uiActions.introActions.buttonOtherOptionsClick, - ), - ) - } - - fun setCardArtwork(uiState: OnboardingSeedPhraseState, cardArtwork: String): OnboardingSeedPhraseState = - uiState.copy( - introState = uiState.introState.copy( - cardImageUrl = cardArtwork, - ), - ) - - fun generateMnemonicComponents(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState { - return uiState.copy( - aboutState = uiState.aboutState.copy( - buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy( - showProgress = true, - ), - buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy( - enabled = false, - ), - ), - ) - } - - fun mnemonicGenerated( - uiState: OnboardingSeedPhraseState, - mnemonicGridItems: PersistentList, - ): OnboardingSeedPhraseState { - return updateMnemonicComponents(uiState, mnemonicGridItems) - .copy( - aboutState = uiState.aboutState.copy( - buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy( - showProgress = false, - ), - buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy( - enabled = true, - ), - ), - ) - } - - fun selectSeedType( - uiState: OnboardingSeedPhraseState, - mnemonicGridItems: PersistentList, - selectedSeedType: SegmentSeedType, - ): OnboardingSeedPhraseState = uiState.copy( - yourSeedPhraseState = uiState.yourSeedPhraseState.copy( - mnemonicGridItems = mnemonicGridItems, - segmentSeedState = uiState.yourSeedPhraseState.segmentSeedState.copy( - selectedSeedType = selectedSeedType, - ), - ), - ) - - fun clearCheckSeedPhraseState(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState = uiState.copy( - checkSeedPhraseState = uiState.checkSeedPhraseState.copy( - tvSecondPhrase = TextFieldState( - label = "2", - isFocused = true, - onTextFieldValueChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onFocusChanged, - ), - tvSeventhPhrase = TextFieldState( - label = "7", - isFocused = false, - onTextFieldValueChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onFocusChanged, - ), - tvEleventhPhrase = TextFieldState( - label = "11", - isFocused = false, - onTextFieldValueChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onTextFieldChanged, - onFocusChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onFocusChanged, - ), - ), - ) - - private fun updateMnemonicComponents( - uiState: OnboardingSeedPhraseState, - mnemonicGridItems: PersistentList, - ): OnboardingSeedPhraseState = uiState.copy( - yourSeedPhraseState = uiState.yourSeedPhraseState.copy( - mnemonicGridItems = mnemonicGridItems, - ), - ) -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt deleted file mode 100644 index 7d6f3a4685..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.viewmodel - -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.OffsetMapping -import androidx.compose.ui.text.input.TransformedText -import androidx.compose.ui.text.input.VisualTransformation -import com.tangem.utils.extensions.isSingleItem - -/** -[REDACTED_AUTHOR] - */ -class InvalidWordsColorTransformation( - private val wordsToBrush: Set, - private val style: SpanStyle, -) : VisualTransformation { - - override fun filter(text: AnnotatedString): TransformedText { - val plainText = text.text - if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) { - return plainText.annotate().toTransformedText() - } - - val wordList = makeSeparatedWordlist(plainText) - val annotatedString = makeAnnotatedString(wordList) - - return annotatedString.toTransformedText() - } - - private fun makeSeparatedWordlist(plainText: String): List { - val plainTextCharArray = plainText.toCharArray() - val wordlist = mutableListOf() - var wordBuilder = StringBuilder() - - for (index in plainTextCharArray.indices) { - val char = plainTextCharArray[index] - if (char.isWhitespace()) { - if (wordBuilder.isEmpty()) { - wordlist.add(WORD_SEPARATOR) - } else { - wordlist.add(wordBuilder.toString()) - wordBuilder = StringBuilder() - wordlist.add(WORD_SEPARATOR) - } - } else { - if (index == plainTextCharArray.lastIndex) { - wordBuilder.append(char) - wordlist.add(wordBuilder.toString()) - wordBuilder = StringBuilder() - } else { - wordBuilder.append(char) - } - } - } - return wordlist.toList() - } - - private fun makeAnnotatedString(wordList: List): AnnotatedString = buildAnnotatedString { - wordList.forEach { - if (it == WORD_SEPARATOR) { - append(WHITE_SPACE) - } else if (wordsToBrush.contains(it)) { - append(it.annotate()) - } else { - append(it) - } - } - } - - private fun String.annotate(): AnnotatedString = AnnotatedString(this, style) - - private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity) - - companion object { - private const val WHITE_SPACE = " " - private const val WORD_SEPARATOR = "_$@-*" - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt deleted file mode 100644 index 9a44793b6f..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.viewmodel - -import android.content.Context -import com.tangem.common.module.ModuleMessageConverter -import com.tangem.feature.onboarding.R -import com.tangem.feature.onboarding.domain.SeedPhraseError - -/** -[REDACTED_AUTHOR] - */ -class SeedPhraseErrorConverter : ModuleMessageConverter, String?> { - - override fun convert(source: Pair): String? { - val (context, message) = source - - val convertedMessage = when (message) { - SeedPhraseError.InvalidChecksum -> { - context.getString(R.string.onboarding_seed_mnemonic_invalid_checksum) - } - is SeedPhraseError.InvalidWords -> { - context.getString(R.string.onboarding_seed_mnemonic_wrong_words) - } - else -> null - } - - return convertedMessage - } -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseMediator.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseMediator.kt deleted file mode 100644 index 5848187991..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseMediator.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.viewmodel - -import com.tangem.common.CompletionResult -import com.tangem.feature.onboarding.data.model.CreateWalletResponse -import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource - -/** -[REDACTED_AUTHOR] - */ -interface SeedPhraseMediator { - fun createWallet(callback: (CompletionResult) -> Unit) - fun importWallet( - mnemonicComponents: List, - passphrase: String?, - seedPhraseSource: SeedPhraseSource, - callback: (CompletionResult) -> Unit, - ) - - fun onWalletCreated(result: CompletionResult) - - fun allowScreenshots(allow: Boolean) -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseRouter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseRouter.kt deleted file mode 100644 index 4fd0dc3ee4..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseRouter.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.viewmodel - -import android.net.Uri -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow - -/** -[REDACTED_AUTHOR] - */ -class SeedPhraseRouter( - private val onBack: () -> Unit, - private val onOpenChat: () -> Unit, - private val onOpenUriClick: (Uri) -> Unit, -) { - - private val _currentScreen = MutableStateFlow(SeedPhraseScreen.Intro) - - val currentScreen: StateFlow - get() = _currentScreen - - fun navigateBack() { - _currentScreen.value = when (_currentScreen.value) { - SeedPhraseScreen.CheckSeedPhrase -> SeedPhraseScreen.YourSeedPhrase - SeedPhraseScreen.AboutSeedPhrase -> SeedPhraseScreen.Intro - SeedPhraseScreen.ImportSeedPhrase, - SeedPhraseScreen.YourSeedPhrase, - -> SeedPhraseScreen.AboutSeedPhrase - SeedPhraseScreen.Intro -> { - onBack.invoke() - return - } - } - } - - fun openScreen(screen: SeedPhraseScreen) { - _currentScreen.value = screen - } - - fun openChat() { - onOpenChat.invoke() - } - - fun openUri(uri: Uri) { - onOpenUriClick.invoke(uri) - } -} - -enum class SeedPhraseScreen { - Intro, AboutSeedPhrase, YourSeedPhrase, CheckSeedPhrase, ImportSeedPhrase -} \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt deleted file mode 100644 index 8e7e49d5c7..0000000000 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt +++ /dev/null @@ -1,484 +0,0 @@ -package com.tangem.feature.onboarding.presentation.wallet2.viewmodel - -import android.net.Uri -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.compose.ui.text.input.TextFieldValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.tangem.common.CompletionResult -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.data.common.locale.LocaleProvider -import com.tangem.feature.onboarding.data.model.CreateWalletResponse -import com.tangem.feature.onboarding.domain.SeedPhraseError -import com.tangem.feature.onboarding.domain.SeedPhraseInteractor -import com.tangem.feature.onboarding.domain.models.MnemonicType -import com.tangem.feature.onboarding.presentation.wallet2.analytics.CreateWalletEvents -import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseEvents -import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource -import com.tangem.feature.onboarding.presentation.wallet2.model.* -import com.tangem.feature.onboarding.presentation.wallet2.model.SegmentSeedType.Companion.fromMnemonicType -import com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders.StateBuilder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.Debouncer -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** -[REDACTED_AUTHOR] - */ -@Suppress("LargeClass") -@HiltViewModel -class SeedPhraseViewModel @Inject constructor( - private val interactor: SeedPhraseInteractor, - private val localeProvider: LocaleProvider, - private val dispatchers: CoroutineDispatcherProvider, - private val analyticsEventHandler: AnalyticsEventHandler, -) : ViewModel() { - - private var uiBuilder = StateBuilder(uiActions = createUiActions()) - - private var selectedMnemonicType = MnemonicType.Mnemonic12 - var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init(fromMnemonicType(selectedMnemonicType))) - private set - - private lateinit var router: SeedPhraseRouter - private lateinit var mediator: SeedPhraseMediator - - val currentScreen: StateFlow - get() = router.currentScreen - - val progress: Flow - get() = currentScreen.map { progressByScreen[it] ?: 0 } - - val maxProgress: Int by lazy { progressByScreen.maxOfOrNull { it.value } ?: 0 } - - var isFinished: Boolean = false - private set - - private val progressByScreen = mapOf( - SeedPhraseScreen.Intro to 1, - SeedPhraseScreen.AboutSeedPhrase to 2, - SeedPhraseScreen.YourSeedPhrase to 3, - SeedPhraseScreen.CheckSeedPhrase to 3, - SeedPhraseScreen.ImportSeedPhrase to 3, - ) - - private val textFieldsDebouncers = mutableMapOf() - - private var generatedMnemonicComponents: Map> = emptyMap() - private var importedMnemonicComponents: List? = null - private var importedPassphrase: String? = null - - override fun onCleared() { - textFieldsDebouncers.forEach { entry -> entry.value.release() } - textFieldsDebouncers.clear() - super.onCleared() - } - - fun setRouter(router: SeedPhraseRouter) { - this.router = router - subscribeToScreenChanges() - } - - private fun subscribeToScreenChanges() { - router.currentScreen.onEach { screen -> - when (screen) { - SeedPhraseScreen.Intro -> { - /* no-op */ - } - - SeedPhraseScreen.AboutSeedPhrase -> { - analyticsEventHandler.send(SeedPhraseEvents.IntroScreenOpened) - mediator.allowScreenshots(true) - } - - SeedPhraseScreen.YourSeedPhrase -> { - analyticsEventHandler.send(SeedPhraseEvents.GenerationScreenOpened) - mediator.allowScreenshots(false) - } - - SeedPhraseScreen.CheckSeedPhrase -> { - analyticsEventHandler.send(SeedPhraseEvents.CheckingScreenOpened) - mediator.allowScreenshots(true) - } - - SeedPhraseScreen.ImportSeedPhrase -> { - analyticsEventHandler.send(SeedPhraseEvents.ImportScreenOpened) - } - } - }.launchIn(viewModelScope) - } - - fun setMediator(mediator: SeedPhraseMediator) { - this.mediator = mediator - } - - fun setCardArtworkUri(cardArtworkUri: String) { - if (uiState.introState.cardImageUrl != cardArtworkUri) { - uiState = uiBuilder.setCardArtwork(uiState, cardArtworkUri) - } - } - - private fun createUiActions(): UiActions = UiActions( - introActions = IntroUiAction( - buttonCreateWalletClick = ::buttonCreateWalletClick, - buttonOtherOptionsClick = ::buttonOtherOptionsClick, - ), - aboutActions = AboutUiAction( - buttonReadMoreAboutSeedPhraseClick = ::buttonReadMoreAboutSeedPhraseClick, - buttonGenerateSeedPhraseClick = ::buttonGenerateSeedPhraseClick, - buttonImportSeedPhraseClick = ::buttonImportSeedPhraseClick, - ), - yourSeedPhraseActions = YourSeedPhraseUiAction( - buttonContinueClick = ::buttonContinueClick, - onSelectType = ::onSelectType, - ), - checkSeedPhraseActions = CheckSeedPhraseUiAction( - buttonCreateWalletClick = { - val mnemonicComponents = - generatedMnemonicComponents[selectedMnemonicType] ?: return@CheckSeedPhraseUiAction - buttonImportWalletClick(mnemonicComponents, null, SeedPhraseSource.GENERATED) - }, - secondTextFieldAction = TextFieldUiAction( - onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) }, - ), - seventhTextFieldAction = TextFieldUiAction( - onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Seventh, value) }, - ), - eleventhTextFieldAction = TextFieldUiAction( - onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Eleventh, value) }, - ), - ), - importSeedPhraseActions = ImportSeedPhraseUiAction( - phraseTextFieldAction = TextFieldUiAction( - onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) }, - ), - passTextFieldAction = TextFieldUiAction( - onTextFieldChanged = { value -> onPassPhraseTextFieldChanged(value) }, - ), - suggestedPhraseClick = ::buttonSuggestedPhraseClick, - onPassphraseInfoClick = ::buttonPassphraseInfoClick, - buttonCreateWalletClick = { - analyticsEventHandler.send(SeedPhraseEvents.ButtonImport) - buttonImportWalletClick(importedMnemonicComponents, importedPassphrase, SeedPhraseSource.IMPORTED) - }, - ), - menuChatClick = ::menuChatClick, - menuNavigateBackClick = ::menuNavigateBackClick, - ) - - // region CheckSeedPhrase - private fun onTextFieldChanged(field: SeedPhraseField, textFieldValue: TextFieldValue) { - launchSingle { - updateUi { uiBuilder.checkSeedPhrase.updateTextField(uiState, field, textFieldValue) } - - val fieldState = field.getState(uiState) - if (fieldState.textFieldValue.text.isEmpty()) { - updateUi { - val mediate = uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError = false) - uiBuilder.checkSeedPhrase.updateCreateWalletButton(mediate, enabled = false) - } - return@launchSingle - } - - createOrGetDebouncer(field.name).debounce(viewModelScope, context = dispatchers.io) { - val mnemonicComponents = generatedMnemonicComponents[selectedMnemonicType] ?: return@debounce - val hasError = !interactor.isWordMatch(mnemonicComponents, field, textFieldValue.text) - if (fieldState.isError != hasError) { - updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) } - } - val isCreateWalletButtonEnabled = SeedPhraseField.entries - .map { field -> field.getState(uiState) } - .all { fieldState -> fieldState.textFieldValue.text.isNotEmpty() && !fieldState.isError } - - if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != isCreateWalletButtonEnabled) { - updateUi { - uiBuilder.checkSeedPhrase.updateCreateWalletButton( - uiState = uiState, - enabled = isCreateWalletButtonEnabled, - ) - } - } - } - } - } - // endregion CheckSeedPhrase - - // region ImportSeedPhrase - private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) { - val oldTextFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue - val isSameText = textFieldValue.text == oldTextFieldValue.text - val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection - - uiState = uiBuilder.importSeedPhrase.updateSeedPhraseTextField(uiState, textFieldValue) - - val fieldState = uiState.importSeedPhraseState.fieldSeedPhrase - val inputMnemonic = fieldState.textFieldValue.text - - val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER) - when { - isSameText && isCursorMoved -> { - debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) { - updateSuggestions(fieldState) - } - } - isSameText && !isCursorMoved -> { - // do nothing - } - else -> { - debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) { - updateSuggestions(fieldState) - validateMnemonic(inputMnemonic) - } - } - } - } - - private fun onPassPhraseTextFieldChanged(textFieldValue: TextFieldValue) { - importedPassphrase = textFieldValue.text - uiState = uiBuilder.importSeedPhrase.updatePassPhraseTextField(uiState, textFieldValue) - } - - private suspend fun validateMnemonic(inputMnemonic: String) { - if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) { - val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false) - updateUi { - uiBuilder.importSeedPhrase.updateError(mediateState, null) - } - return - } - - interactor.validateMnemonicString(inputMnemonic) - .onSuccess { mnemonicComponents -> - importedMnemonicComponents = mnemonicComponents - val mediateState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = true) - updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, null) } - } - .onFailure { - uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(uiState, enabled = false) - val error = it as? SeedPhraseError ?: return - - importedMnemonicComponents = null - val mediateState = when (error) { - is SeedPhraseError.InvalidWords -> { - uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words) - } - else -> uiState - } - updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) } - } - } - - private suspend fun updateSuggestions(fieldState: TextFieldState) { - val suggestions = interactor.getSuggestions( - text = fieldState.textFieldValue.text, - hasSelection = !fieldState.textFieldValue.selection.collapsed, - cursorPosition = fieldState.textFieldValue.selection.end, - ) - updateUi { uiBuilder.importSeedPhrase.updateSuggestions(uiState, suggestions) } - } - // endregion ImportSeedPhrase - - // region ButtonClickHandlers - private fun buttonCreateWalletClick() { - viewModelScope.launch(dispatchers.io) { - analyticsEventHandler.send(CreateWalletEvents.OnboardingSeedButtonCreateWallet) - mediator.createWallet(::handleWalletCreationResult) - } - } - - private fun buttonImportWalletClick( - mnemonicComponents: List?, - passphrase: String?, - seedPhraseSource: SeedPhraseSource, - ) { - mnemonicComponents ?: return - - viewModelScope.launch(dispatchers.io) { - mediator.importWallet(mnemonicComponents, passphrase, seedPhraseSource, ::handleWalletCreationResult) - } - } - - private fun handleWalletCreationResult(result: CompletionResult) { - when (result) { - is CompletionResult.Success -> { - isFinished = true - } - is CompletionResult.Failure -> { - // errors shows on the TangemSdk bottom sheet dialog - } - } - mediator.onWalletCreated(result) - } - - private fun menuNavigateBackClick() { - router.navigateBack() - } - - private fun menuChatClick() { - router.openChat() - } - - private fun buttonOtherOptionsClick() { - analyticsEventHandler.send(CreateWalletEvents.OnboardingSeedButtonOtherOptions) - router.openScreen(SeedPhraseScreen.AboutSeedPhrase) - } - - private fun buttonReadMoreAboutSeedPhraseClick() { - analyticsEventHandler.send(SeedPhraseEvents.ButtonReadMore) - val webUri = Uri.Builder() - .scheme("https") - .authority("tangem.com") - .appendPath(localeProvider.getWebUriLocaleLanguage()) - .appendPath("blog/post/seed-phrase-a-risky-solution") - .build() - - router.openUri(webUri) - } - - private fun buttonGenerateSeedPhraseClick() { - val mnemonicTypes = listOf(MnemonicType.Mnemonic12, MnemonicType.Mnemonic24) - analyticsEventHandler.send(SeedPhraseEvents.ButtonGenerateSeedPhrase) - launchSingle { - updateUi { uiBuilder.generateMnemonicComponents(uiState) } - delay(DELAY_GENERATE_SEED_PHRASE) - interactor.generateMnemonics(mnemonicTypes) - .onSuccess { mnemonics -> - generatedMnemonicComponents = mnemonics.mapValues { it.value.mnemonicComponents } - val defaultMnemonic = mnemonics[selectedMnemonicType]?.mnemonicComponents ?: return@launchSingle - val mnemonicGridItems = toMnemonicGridList(defaultMnemonic) - updateUi { - router.openScreen(SeedPhraseScreen.YourSeedPhrase) - uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toPersistentList()) - } - } - .onFailure { - generatedMnemonicComponents = emptyMap() - // TODO: show error - } - } - } - - private fun toMnemonicGridList(mnemonicComponents: List): PersistentList { - return mnemonicComponents.mapIndexed { index, word -> - MnemonicGridItem( - index = index.inc(), - mnemonic = word, - ) - }.toPersistentList() - } - - private fun buttonImportSeedPhraseClick() { - analyticsEventHandler.send(SeedPhraseEvents.ButtonImportWallet) - router.openScreen(SeedPhraseScreen.ImportSeedPhrase) - } - - private fun buttonContinueClick() { - launchSingle { - updateUi { - uiBuilder.clearCheckSeedPhraseState(uiState) - } - router.openScreen(SeedPhraseScreen.CheckSeedPhrase) - } - } - - private fun onSelectType(selectedSeedType: SegmentSeedType) { - launchSingle { - selectedMnemonicType = selectedSeedType.toMnemonicType() - val selectedMnemonic = generatedMnemonicComponents[selectedMnemonicType] ?: return@launchSingle - updateUi { - uiBuilder.selectSeedType( - uiState = uiState, - mnemonicGridItems = toMnemonicGridList(selectedMnemonic), - selectedSeedType = selectedSeedType, - ) - } - } - } - - private fun buttonSuggestedPhraseClick(suggestionIndex: Int) { - launchSingle { - val textFieldValue = uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue - val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex] - val cursorPosition = textFieldValue.selection.end - - val insertResult = interactor.insertSuggestionWord( - text = textFieldValue.text, - suggestion = word, - cursorPosition = cursorPosition, - ) - updateUi { - val mediateState = uiBuilder.importSeedPhrase.insertSuggestionWord(uiState, insertResult) - uiBuilder.importSeedPhrase.updateSuggestions(mediateState, persistentListOf()) - // copy of TextFieldValue has no effect for textChange callback and seed validation doesn't work - // call validation force - } - validateMnemonic(uiState.importSeedPhraseState.fieldSeedPhrase.textFieldValue.text) - } - } - - private fun buttonPassphraseInfoClick() { - launchSingle { - updateUi { - uiBuilder.importSeedPhrase.showPassphraseInfoBottomSheet(uiState) { - dismissPassphraseBottomSheet() - } - } - } - } - - private fun dismissPassphraseBottomSheet() { - launchSingle { - updateUi { uiBuilder.importSeedPhrase.dismissPassphraseBottomSheet(uiState) } - } - } - // endregion ButtonClickHandlers - - // region Utils - /** - * Updating the UI with a contract where all copying of objects are called in the Single thread context and - * updating the UiState in the main context. - */ - private suspend fun updateUi(updateBlock: suspend () -> OnboardingSeedPhraseState) { - withSingleContext { - val newState = updateBlock.invoke() - withMainContext { uiState = newState } - } - } - - private fun createOrGetDebouncer(name: String): Debouncer { - return textFieldsDebouncers[name] ?: Debouncer().apply { textFieldsDebouncers[name] = this } - } - - private fun SeedPhraseField.getState(uiState: OnboardingSeedPhraseState): TextFieldState = when (this) { - SeedPhraseField.Second -> uiState.checkSeedPhraseState.tvSecondPhrase - SeedPhraseField.Seventh -> uiState.checkSeedPhraseState.tvSeventhPhrase - SeedPhraseField.Eleventh -> uiState.checkSeedPhraseState.tvEleventhPhrase - } - - private fun launchSingle(block: suspend CoroutineScope.() -> Unit): Job { - return viewModelScope.launch(dispatchers.single, block = block) - } - - private suspend fun withMainContext(block: suspend CoroutineScope.() -> T): T { - return withContext(dispatchers.main, block) - } - - private suspend fun withSingleContext(block: suspend CoroutineScope.() -> T): T { - return withContext(dispatchers.single, block) - } - // endregion Utils - - private companion object { - const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer" - const val MNEMONIC_DEBOUNCE_DELAY = 700L - const val DELAY_GENERATE_SEED_PHRASE = 300L - } -} \ No newline at end of file diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 33f0abba3e..3ff00087f0 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -19,8 +19,6 @@ import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse -import com.tangem.domain.redux.LegacyAction -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.DeleteWalletUseCase @@ -34,7 +32,6 @@ import com.tangem.feature.walletsettings.entity.WalletSettingsUM import com.tangem.feature.walletsettings.impl.R import com.tangem.feature.walletsettings.utils.ItemsBuilder import com.tangem.features.nft.NFTFeatureToggles -import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -55,12 +52,10 @@ internal class WalletSettingsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val analyticsContextProxy: AnalyticsContextProxy, - private val reduxStateHolder: ReduxStateHolder, private val getShouldSaveUserWalletsSyncUseCase: ShouldSaveUserWalletsSyncUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val walletsRepository: WalletsRepository, private val nftFeatureToggles: NFTFeatureToggles, - private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, ) : Model() { val params: WalletSettingsComponent.Params = paramsContainer.require() @@ -163,23 +158,12 @@ internal class WalletSettingsModel @Inject constructor( analyticsEventHandler.send(Settings.ButtonCreateBackup) analyticsContextProxy.addContext(scanResponse) - if (onboardingV2FeatureToggles.isOnboardingV2Enabled) { - router.push( - AppRoute.Onboarding( - scanResponse = scanResponse, - mode = AppRoute.Onboarding.Mode.AddBackupWallet1, - ), - ) - } else { - reduxStateHolder.dispatch( - LegacyAction.StartOnboardingProcess( - scanResponse = scanResponse, - canSkipBackup = false, - ), - ) - - router.push(AppRoute.OnboardingWallet()) - } + router.push( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = AppRoute.Onboarding.Mode.AddBackupWallet1, + ), + ) } private fun onCheckedNFTChange(isChecked: Boolean) { diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index b2a3af1b04..fc42f54104 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -96,9 +96,6 @@ dependencies { implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) - //TODO: Create api/impl modules for onboarding [REDACTED_JIRA] - implementation(projects.features.onboarding) - /** Feature Apis */ implementation(projects.features.details.api) implementation(projects.features.manageTokens.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 419db01ebc..cb41f9c60a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -14,8 +14,6 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.ShouldShowSwapPromoWalletUseCase -import com.tangem.domain.redux.LegacyAction -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.settings.NeverToSuggestRateAppUseCase import com.tangem.domain.settings.RemindToRateAppLaterUseCase import com.tangem.domain.tokens.FetchTokenListUseCase @@ -96,7 +94,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val scanCardToUnlockWalletClickHandler: ScanCardToUnlockWalletClickHandler, private val unlockWalletsUseCase: UnlockWalletsUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - private val reduxStateHolder: ReduxStateHolder, private val dispatchers: CoroutineDispatcherProvider, private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, private val getCardInfoUseCase: GetCardInfoUseCase, @@ -116,14 +113,6 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private fun prepareAndStartOnboardingProcess() { modelScope.launch(dispatchers.main) { getSelectedUserWallet()?.let { - reduxStateHolder.dispatch( - LegacyAction.StartOnboardingProcess( - scanResponse = it.scanResponse, - canSkipBackup = false, - ), - ) - - // navigation action shouldn't be out of coroutine to avoid race router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, 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 2b865d1f61..048f9af182 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 @@ -15,7 +15,6 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.features.biometry.BiometryFeatureToggles -import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import javax.inject.Inject @@ -26,7 +25,6 @@ internal class DefaultWalletRouter @Inject constructor( private val router: AppRouter, private val urlOpener: UrlOpener, private val reduxStateHolder: ReduxStateHolder, - private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles, private val biometryFeatureToggles: BiometryFeatureToggles, ) : InnerWalletRouter { @@ -50,22 +48,16 @@ internal class DefaultWalletRouter @Inject constructor( } override fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean) { - if (onboardingV2FeatureToggles.isOnboardingV2Enabled) { - router.push( - AppRoute.Onboarding( - scanResponse = scanResponse, - mode = if (continueBackup) { - AppRoute.Onboarding.Mode.AddBackupWallet1 - } else { - AppRoute.Onboarding.Mode.Onboarding - }, - ), - ) - } else { - router.push( - AppRoute.OnboardingWallet(canSkipBackup = false), - ) - } + router.push( + AppRoute.Onboarding( + scanResponse = scanResponse, + mode = if (continueBackup) { + AppRoute.Onboarding.Mode.AddBackupWallet1 + } else { + AppRoute.Onboarding.Mode.Onboarding + }, + ), + ) } override fun openOnrampSuccessScreen(txId: String) { diff --git a/settings.gradle.kts b/settings.gradle.kts index 5aa3d2ba61..385d8b2dc8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -170,7 +170,6 @@ include(":libs:tangem-sdk-api") // endregion Libs modules // region Feature modules -include(":features:onboarding") include(":features:onboarding-v2:api") include(":features:onboarding-v2:impl") From f97a017f3fa6874dcd7cdedbe86d35bea3427d5f Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 13:54:11 +0300 Subject: [PATCH 123/151] Updated on 2026-08-14 --- .../com/tangem/tap/common/redux/AppReducer.kt | 2 - .../com/tangem/tap/common/redux/AppState.kt | 4 - .../saveWallet/redux/SaveWalletAction.kt | 31 --- .../saveWallet/redux/SaveWalletMiddleware.kt | 188 ----------------- .../saveWallet/redux/SaveWalletReducer.kt | 53 ----- .../saveWallet/redux/SaveWalletState.kt | 18 -- .../ui/SaveWalletBottomSheetFragment.kt | 81 -------- .../saveWallet/ui/SaveWalletScreenState.kt | 12 -- .../saveWallet/ui/SaveWalletViewModel.kt | 92 --------- .../EnrollBiometricsDialogConent.kt | 52 ----- .../ui/components/SaveWalletScreenContent.kt | 193 ------------------ .../ui/models/EnrollBiometricsDialog.kt | 9 - .../tangem/tap/routing/utils/ChildFactory.kt | 10 +- .../com/tangem/common/routing/AppRoute.kt | 4 - .../configs/feature_toggles_config.json | 4 - .../biometry/BiometryFeatureToggles.kt | 5 - .../impl/DefaultBiometryFeatureToggles.kt | 12 -- .../biometry/impl/di/FeatureModule.kt | 5 - .../entry/impl/model/OnboardingEntryModel.kt | 84 +++----- .../wallet/child/wallet/model/WalletModel.kt | 12 +- .../router/DefaultWalletRouter.kt | 8 - .../presentation/router/InnerWalletRouter.kt | 3 - 22 files changed, 28 insertions(+), 854 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/saveWallet/ui/models/EnrollBiometricsDialog.kt delete mode 100644 features/biometry/api/src/main/kotlin/com/tangem/features/biometry/BiometryFeatureToggles.kt delete mode 100644 features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultBiometryFeatureToggles.kt diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index 7aa2d7a120..111bad630c 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -4,7 +4,6 @@ import com.tangem.tap.common.redux.global.globalReducer import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.features.details.redux.walletconnect.WalletConnectReducer import com.tangem.tap.features.home.redux.HomeReducer -import com.tangem.tap.features.saveWallet.redux.SaveWalletReducer import com.tangem.tap.features.welcome.redux.WelcomeReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer import org.rekotlin.Action @@ -19,7 +18,6 @@ fun appReducer(action: Action, state: AppState?): AppState { detailsState = DetailsReducer.reduce(action, state), walletConnectState = WalletConnectReducer.reduce(action, state.walletConnectState), welcomeState = WelcomeReducer.reduce(action, state), - saveWalletState = SaveWalletReducer.reduce(action, state), daggerGraphState = DaggerGraphReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index 99946cc324..e1814f72c0 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -10,8 +10,6 @@ import com.tangem.tap.features.details.redux.walletconnect.WalletConnectState import com.tangem.tap.features.home.redux.HomeMiddleware import com.tangem.tap.features.home.redux.HomeState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware -import com.tangem.tap.features.saveWallet.redux.SaveWalletMiddleware -import com.tangem.tap.features.saveWallet.redux.SaveWalletState import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.features.welcome.redux.WelcomeMiddleware import com.tangem.tap.features.welcome.redux.WelcomeState @@ -26,7 +24,6 @@ data class AppState( val detailsState: DetailsState = DetailsState(), val walletConnectState: WalletConnectState = WalletConnectState(), val welcomeState: WelcomeState = WelcomeState(), - val saveWalletState: SaveWalletState = SaveWalletState(), val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { @@ -41,7 +38,6 @@ data class AppState( WalletConnectMiddleware().walletConnectMiddleware, BackupMiddleware().backupMiddleware, WelcomeMiddleware().middleware, - SaveWalletMiddleware().middleware, LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt deleted file mode 100644 index f4981a8c5c..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.tap.features.saveWallet.redux - -import com.tangem.common.core.TangemError -import com.tangem.domain.models.scan.ScanResponse -import org.rekotlin.Action - -internal sealed interface SaveWalletAction : Action { - data class ProvideBackupInfo( - val scanResponse: ScanResponse, - val accessCode: String?, - val backupCardsIds: Set?, - ) : SaveWalletAction - - data object AllowToUseBiometrics : SaveWalletAction { - data object Success : SaveWalletAction - data class Error(val error: TangemError) : SaveWalletAction - } - - data object Dismiss : SaveWalletAction - - data object CloseError : SaveWalletAction - data object EnrollBiometrics : SaveWalletAction { - data object Enroll : SaveWalletAction - data object Cancel : SaveWalletAction - } - - data class SaveWalletAfterBackup( - val hasBackupError: Boolean, - val shouldNavigateToWallet: Boolean, - ) : SaveWalletAction -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt deleted file mode 100644 index d8f15fd3d7..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletMiddleware.kt +++ /dev/null @@ -1,188 +0,0 @@ -package com.tangem.tap.features.saveWallet.redux - -import com.tangem.common.* -import com.tangem.common.core.TangemSdkError -import com.tangem.common.extensions.guard -import com.tangem.common.routing.AppRoute -import com.tangem.core.analytics.Analytics -import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent -import com.tangem.domain.wallets.models.UserWallet -import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.Onboarding -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.extensions.onUserWalletSelected -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.launch -import org.rekotlin.Middleware -import timber.log.Timber - -internal class SaveWalletMiddleware { - - private val saveWalletJobHolder = JobHolder() - - val middleware: Middleware = { _, stateProvider -> - { next -> - { action -> - val state = stateProvider() - if (action is SaveWalletAction && state != null) { - handleAction(action, state.saveWalletState) - } - next(action) - } - } - } - - private fun handleAction(action: SaveWalletAction, state: SaveWalletState) { - when (action) { - is SaveWalletAction.AllowToUseBiometrics -> allowToUseBiometrics(state) - is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics() - is SaveWalletAction.Dismiss -> dismiss(state) - is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup( - state = state, - hasBackupError = action.hasBackupError, - shouldNavigateToWallet = action.shouldNavigateToWallet, - ) - is SaveWalletAction.AllowToUseBiometrics.Success, - is SaveWalletAction.AllowToUseBiometrics.Error, - is SaveWalletAction.ProvideBackupInfo, - is SaveWalletAction.CloseError, - is SaveWalletAction.EnrollBiometrics, - is SaveWalletAction.EnrollBiometrics.Cancel, - -> Unit - } - } - - private fun saveWalletAfterBackup( - state: SaveWalletState, - hasBackupError: Boolean, - shouldNavigateToWallet: Boolean, - ) { - scope.launch { - val backupInfo = state.backupInfo ?: error("Backup info is null") - - val userWalletBuilder = store.inject(DaggerGraphState::userWalletBuilderFactory) - .create(scanResponse = backupInfo.scanResponse) - - val userWallet = userWalletBuilder - .backupCardsIds(state.backupInfo.backupCardsIds) - .hasBackupError(hasBackupError) - .build() - .guard { - Timber.e("User wallet not created") - return@launch - } - - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - userWalletsListManager.save(userWallet, canOverride = true) - .flatMap { - saveAccessCodeIfNeeded(accessCode = backupInfo.accessCode, cardsInWallet = userWallet.cardsInWallet) - } - .doOnFailure { error -> - Timber.e(error, "Unable to save user wallet") - } - .doOnSuccess { - store.onUserWalletSelected(userWallet) - if (!shouldNavigateToWallet) { - store.dispatchWithMain(OnboardingWalletAction.WalletSaved(userWallet.walletId)) - } - } - .doOnResult { if (shouldNavigateToWallet) navigateToWallet() } - } - } - - private fun enrollBiometrics() { - store.inject(DaggerGraphState::settingsManager).openBiometricSettings() - } - - private fun allowToUseBiometrics(state: SaveWalletState) = scope.launch { - if (tangemSdkManager.checkNeedEnrollBiometrics()) { - store.dispatchWithMain(SaveWalletAction.EnrollBiometrics) - return@launch - } - - if (state.backupInfo != null) { - // TODO: Remove after onboarding refactoring - Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.On)) - } else { - Analytics.send( - MainScreenAnalyticsEvent.EnableBiometrics( - state = com.tangem.core.analytics.models.AnalyticsParam.OnOffState.On, - ), - ) - } - - /* - - * because it will be automatically saved on UserWalletsListManager switch - */ - val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager) - val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard { - val error = IllegalStateException("No selected user wallet") - Timber.e(error, "Unable to save user wallet") - store.dispatchWithMain( - SaveWalletAction.AllowToUseBiometrics.Error(TangemSdkError.ExceptionError(error)), - ) - return@launch - } - - handleSuccessAllowing(selectedUserWallet) - }.saveIn(saveWalletJobHolder) - - private suspend fun handleSuccessAllowing(userWallet: UserWallet) { - store.inject(DaggerGraphState::walletsRepository).saveShouldSaveUserWallets(item = true) - - store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = true) - - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = userWallet.hasAccessCode, - ) - - store.dispatchWithMain(SaveWalletAction.AllowToUseBiometrics.Success) - store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) } - } - - private fun dismiss(state: SaveWalletState) { - if (state.backupInfo != null) { - // TODO: Remove after onboarding refactoring - Analytics.send(Onboarding.EnableBiometrics(AnalyticsParam.OnOffState.Off)) - } else { - Analytics.send( - MainScreenAnalyticsEvent.EnableBiometrics( - com.tangem.core.analytics.models.AnalyticsParam.OnOffState.Off, - ), - ) - } - } - - private suspend fun saveAccessCodeIfNeeded( - accessCode: String?, - cardsInWallet: Set, - ): CompletionResult { - return when { - accessCode.isNullOrBlank() -> { - CompletionResult.Success(Unit) - } - else -> { - tangemSdkManager.saveAccessCode( - accessCode = accessCode, - cardsIds = cardsInWallet, - ) - } - } - } - - private fun navigateToWallet() { - store.dispatchNavigationAction { - replaceAll(AppRoute.Wallet) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt deleted file mode 100644 index 370f7cd935..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletReducer.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.tangem.tap.features.saveWallet.redux - -import com.tangem.tap.common.redux.AppState -import org.rekotlin.Action - -internal object SaveWalletReducer { - fun reduce(action: Action, state: AppState): SaveWalletState { - return if (action is SaveWalletAction) { - internalReduce(action, state.saveWalletState) - } else { - state.saveWalletState - } - } - - private fun internalReduce(action: SaveWalletAction, state: SaveWalletState): SaveWalletState { - return when (action) { - is SaveWalletAction.ProvideBackupInfo -> state.copy( - backupInfo = SaveWalletState.WalletBackupInfo( - scanResponse = action.scanResponse, - accessCode = action.accessCode, - backupCardsIds = action.backupCardsIds, - ), - ) - is SaveWalletAction.AllowToUseBiometrics, - -> state.copy(isSaveInProgress = true) - is SaveWalletAction.AllowToUseBiometrics.Error -> state.copy( - error = action.error, - isSaveInProgress = false, - ) - is SaveWalletAction.AllowToUseBiometrics.Success -> state.copy( - backupInfo = null, - isSaveInProgress = false, - ) - is SaveWalletAction.Dismiss -> state.copy( - backupInfo = null, - ) - is SaveWalletAction.CloseError -> state.copy( - error = null, - ) - is SaveWalletAction.EnrollBiometrics -> state.copy( - needEnrollBiometrics = true, - ) - is SaveWalletAction.EnrollBiometrics.Enroll, - is SaveWalletAction.EnrollBiometrics.Cancel, - -> state.copy( - needEnrollBiometrics = false, - isSaveInProgress = false, - ) - is SaveWalletAction.SaveWalletAfterBackup, - -> state - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt deleted file mode 100644 index b57528ceaa..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.tap.features.saveWallet.redux - -import com.tangem.common.core.TangemError -import com.tangem.domain.models.scan.ScanResponse -import org.rekotlin.StateType - -data class SaveWalletState( - val backupInfo: WalletBackupInfo? = null, - val isSaveInProgress: Boolean = false, - val needEnrollBiometrics: Boolean = false, - val error: TangemError? = null, -) : StateType { - data class WalletBackupInfo( - val scanResponse: ScanResponse, - val accessCode: String?, - val backupCardsIds: Set?, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt deleted file mode 100644 index 7062719124..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletBottomSheetFragment.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.tangem.tap.features.saveWallet.ui - -import android.content.DialogInterface -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.fragment.app.viewModels -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.screen.ComposeBottomSheetFragment -import com.tangem.tap.features.details.ui.cardsettings.resolveReference -import com.tangem.tap.features.saveWallet.ui.components.EnrollBiometricsDialogContent -import com.tangem.tap.features.saveWallet.ui.components.SaveWalletScreenContent -import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint -internal class SaveWalletBottomSheetFragment : ComposeBottomSheetFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - override val expandedHeightFraction: Float = .98f - - private val viewModel by viewModels() - - override fun onCancel(dialog: DialogInterface) { - viewModel.cancelOrClose() - super.onCancel(dialog) - } - - @Composable - override fun ScreenContent(modifier: Modifier) { - val state by viewModel.state.collectAsStateWithLifecycle() - val snackbarHostState = remember { SnackbarHostState() } - val errorMessage by rememberUpdatedState(newValue = state.error?.resolveReference()) - val enrollBiometricsDialog by rememberUpdatedState(newValue = state.enrollBiometricsDialog) - - Box(modifier = modifier) { - SaveWalletScreenContent( - showProgress = state.showProgress, - onSaveWalletClick = viewModel::saveWallet, - onCloseClick = { - viewModel.cancelOrClose() - dismissAllowingStateLoss() - }, - ) - - SnackbarHost( - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(vertical = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - hostState = snackbarHostState, - ) - } - - EnrollBiometricsDialog(dialog = enrollBiometricsDialog) - - LaunchedEffect(key1 = errorMessage) { - errorMessage?.let { - snackbarHostState.showSnackbar(it) - viewModel.closeError() - } - } - } - - @Suppress("TopLevelComposableFunctions") - @Composable - private fun EnrollBiometricsDialog(dialog: EnrollBiometricsDialog?) { - if (dialog == null) return - EnrollBiometricsDialogContent(dialog) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt deleted file mode 100644 index 51dbdcadc0..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletScreenState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.tap.features.saveWallet.ui - -import androidx.compose.runtime.Immutable -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog - -@Immutable -internal data class SaveWalletScreenState( - val showProgress: Boolean = false, - val enrollBiometricsDialog: EnrollBiometricsDialog? = null, - val error: TextReference? = null, -) \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt deleted file mode 100644 index 40164976d3..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/SaveWalletViewModel.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.tangem.tap.features.saveWallet.ui - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.features.details.ui.cardsettings.TextReference -import com.tangem.tap.features.saveWallet.redux.SaveWalletAction -import com.tangem.tap.features.saveWallet.redux.SaveWalletState -import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog -import com.tangem.tap.store -import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.rekotlin.StoreSubscriber -import javax.inject.Inject - -@HiltViewModel -internal class SaveWalletViewModel @Inject constructor( - private val analyticsEventHandler: AnalyticsEventHandler, - private val setSaveWalletScreenShownUseCase: SetSaveWalletScreenShownUseCase, - dispatchers: AppCoroutineDispatcherProvider, -) : ViewModel(), StoreSubscriber { - - private val stateInternal = MutableStateFlow(SaveWalletScreenState()) - val state: StateFlow = stateInternal - - init { - subscribeToStoreChanges() - - viewModelScope.launch(dispatchers.main) { - setSaveWalletScreenShownUseCase() - } - } - - fun saveWallet() { - analyticsEventHandler.send(WalletScreenAnalyticsEvent.MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.On)) - store.dispatch(SaveWalletAction.AllowToUseBiometrics) - } - - fun cancelOrClose() { - analyticsEventHandler.send( - WalletScreenAnalyticsEvent.MainScreen.EnableBiometrics(AnalyticsParam.OnOffState.Off), - ) - store.dispatch(SaveWalletAction.Dismiss) - } - - fun closeError() { - store.dispatch(SaveWalletAction.CloseError) - } - - override fun newState(state: SaveWalletState) { - stateInternal.update { prevState -> - prevState.copy( - showProgress = state.isSaveInProgress, - enrollBiometricsDialog = if (state.needEnrollBiometrics) createEnrollBiometricsDialog() else null, - error = state.error - ?.takeUnless { it.silent } - ?.let { error -> - error.messageResId?.let { TextReference.Res(it) } - ?: TextReference.Str(error.customMessage) - }, - ) - } - } - - override fun onCleared() { - store.unsubscribe(this) - } - - private fun subscribeToStoreChanges() { - store.subscribe(this) { appState -> - appState.skip { old, new -> old.saveWalletState == new.saveWalletState } - .select { it.saveWalletState } - } - } - - private fun createEnrollBiometricsDialog() = EnrollBiometricsDialog( - onEnroll = { - store.dispatchOnMain(SaveWalletAction.EnrollBiometrics.Enroll) - }, - onCancel = { - store.dispatchOnMain(SaveWalletAction.EnrollBiometrics.Cancel) - }, - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt deleted file mode 100644 index 54da091df6..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/EnrollBiometricsDialogConent.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.tap.features.saveWallet.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.R -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.saveWallet.ui.models.EnrollBiometricsDialog - -@Composable -fun EnrollBiometricsDialogContent(dialog: EnrollBiometricsDialog) { - BasicDialog( - title = stringResourceSafe(R.string.save_user_wallet_agreement_enroll_biometrics_title), - message = stringResourceSafe(R.string.save_user_wallet_agreement_enroll_biometrics_description), - confirmButton = DialogButtonUM( - title = stringResourceSafe(R.string.common_enable), - onClick = dialog.onEnroll, - ), - dismissButton = DialogButtonUM( - onClick = dialog.onCancel, - ), - onDismissDialog = dialog.onCancel, - ) -} - -// region Preview -@Composable -private fun EnrollBiometricDialogContentSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - EnrollBiometricsDialogContent(dialog = EnrollBiometricsDialog({}, {})) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun EnrollBiometricDialogContentPreview() { - TangemThemePreview { - EnrollBiometricDialogContentSample() - } -} -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt deleted file mode 100644 index 403c914016..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/components/SaveWalletScreenContent.kt +++ /dev/null @@ -1,193 +0,0 @@ -package com.tangem.tap.features.saveWallet.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.atoms.Hand -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.wallet.R - -@Composable -internal fun SaveWalletScreenContent(showProgress: Boolean, onSaveWalletClick: () -> Unit, onCloseClick: () -> Unit) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Header(onCloseClick = onCloseClick) - SpacerHHalf() - Title(modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing22)) - SpacerH32() - Description( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing34, - end = TangemTheme.dimens.spacing56, - ) - .fillMaxWidth(), - ) - SpacerHHalf() - Footer( - showProgress = showProgress, - onSaveWalletClick = onSaveWalletClick, - ) - SpacerH16() - } -} - -@Composable -private fun Header(onCloseClick: () -> Unit) { - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Hand() - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - IconButton( - modifier = Modifier.size(TangemTheme.dimens.size32), - onClick = onCloseClick, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = painterResource(id = R.drawable.ic_close_24), - tint = TangemTheme.colors.icon.secondary, - contentDescription = stringResourceSafe(id = R.string.common_cancel), - ) - } - SpacerW8() - } - } -} - -@Composable -private fun Title(modifier: Modifier = Modifier) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing32), - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size56), - painter = painterResource(id = R.drawable.ic_fingerprint_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Text( - text = stringResourceSafe(id = R.string.save_user_wallet_agreement_header_biometrics), - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -private fun Description(modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), - horizontalAlignment = Alignment.Start, - ) { - DescriptionItem( - iconPainter = painterResource(id = R.drawable.ic_face_recognition_24), - title = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_title), - description = stringResourceSafe(id = R.string.save_user_wallet_agreement_access_description), - ) - DescriptionItem( - iconPainter = painterResource(id = R.drawable.ic_lock_24), - title = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_title), - description = stringResourceSafe(id = R.string.save_user_wallet_agreement_code_description_biometrics), - ) - } -} - -@Composable -private fun Footer(showProgress: Boolean, onSaveWalletClick: () -> Unit) { - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), - ) { - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - showProgress = showProgress, - text = stringResourceSafe(id = R.string.save_user_wallet_agreement_allow_biometrics), - onClick = onSaveWalletClick, - ) - Text( - modifier = Modifier.fillMaxWidth(fraction = .7f), - text = stringResourceSafe(R.string.save_user_wallet_agreement_notice), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -private fun DescriptionItem(iconPainter: Painter, title: String, description: String) { - Row( - verticalAlignment = Alignment.Top, - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - Icon( - modifier = Modifier.size(TangemTheme.dimens.size24), - painter = iconPainter, - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - SpacerW24() - Column { - Text( - text = title, - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - SpacerH4() - Text( - text = description, - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.secondary, - ) - } - } -} - -// region Preview -@Composable -private fun SaveWalletScreenContentSample(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .background(TangemTheme.colors.background.primary), - ) { - SaveWalletScreenContent( - showProgress = false, - onSaveWalletClick = { /* no-op */ }, - onCloseClick = { /* no-op */ }, - ) - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SaveWalletScreenContentPreview() { - TangemThemePreview { - SaveWalletScreenContentSample() - } -} -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/models/EnrollBiometricsDialog.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/ui/models/EnrollBiometricsDialog.kt deleted file mode 100644 index 7f785a95cf..0000000000 --- a/app/src/main/java/com/tangem/tap/features/saveWallet/ui/models/EnrollBiometricsDialog.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.tap.features.saveWallet.ui.models - -import androidx.compose.runtime.Immutable - -@Immutable -data class EnrollBiometricsDialog( - val onEnroll: () -> Unit, - val onCancel: () -> Unit, -) \ No newline at end of file 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 d7a44e57fd..f5feb80a77 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 @@ -33,7 +33,6 @@ import com.tangem.tap.features.details.ui.resetcard.api.ResetCardComponent import com.tangem.tap.features.details.ui.securitymode.api.SecurityModeComponent import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectComponent import com.tangem.tap.features.home.api.HomeComponent -import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child import com.tangem.tap.routing.toggle.RoutingFeatureToggles @@ -41,7 +40,7 @@ import com.tangem.utils.Provider import dagger.hilt.android.scopes.ActivityScoped import java.util.WeakHashMap import javax.inject.Inject -import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedisegnedWalletConnectComponent +import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent @ActivityScoped @Suppress("LongParameterList", "LargeClass") @@ -77,7 +76,7 @@ internal class ChildFactory @Inject constructor( private val walletComponentFactory: WalletEntryComponent.Factory, private val sendComponentFactoryV2: com.tangem.features.send.v2.api.SendComponent.Factory, private val sendFeatureToggles: SendFeatureToggles, - private val redesignedWalletConnectComponentFactory: RedisegnedWalletConnectComponent.Factory, + private val redesignedWalletConnectComponentFactory: RedesignedWalletConnectComponent.Factory, private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val testerRouter: TesterRouter, @@ -419,8 +418,6 @@ internal class ChildFactory @Inject constructor( componentFactory = nftSendComponentFactory, ) } - is AppRoute.SaveWallet, - -> error("Unsupported route: $route") } // endregion } @@ -448,9 +445,6 @@ internal class ChildFactory @Inject constructor( componentFactory = appCurrencySelectorComponentFactory, ) } - is AppRoute.SaveWallet -> { - route.asFragmentChild(Provider { SaveWalletBottomSheetFragment() }) - } is AppRoute.Send -> { if (sendFeatureToggles.isSendV2Enabled) { 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 24ed3c80f7..0a280474be 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 @@ -164,10 +164,6 @@ sealed class AppRoute(val path: String) : Route { @Serializable data object TesterMenu : AppRoute(path = "/tester_menu") - @Deprecated("Do not use! Should be replaced by an implementation in wallet route component") - @Serializable - data object SaveWallet : AppRoute(path = "/save_wallet") - @Serializable data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") 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 57f18c60fc..5077c92b05 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 @@ -39,10 +39,6 @@ "name": "TX_HISTORY_REFACTORING_ENABLED", "version": "5.22.0" }, - { - "name": "ASK_BIOMETRY_REFACTORING_ENABLED", - "version": "5.22.0" - }, { "name": "NEW_ARTWORK_LOADING", "version": "5.24.0" diff --git a/features/biometry/api/src/main/kotlin/com/tangem/features/biometry/BiometryFeatureToggles.kt b/features/biometry/api/src/main/kotlin/com/tangem/features/biometry/BiometryFeatureToggles.kt deleted file mode 100644 index 8d9da24f06..0000000000 --- a/features/biometry/api/src/main/kotlin/com/tangem/features/biometry/BiometryFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.biometry - -interface BiometryFeatureToggles { - val isAskForBiometryEnabled: Boolean -} \ No newline at end of file diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultBiometryFeatureToggles.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultBiometryFeatureToggles.kt deleted file mode 100644 index 4cbc7b25ac..0000000000 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/DefaultBiometryFeatureToggles.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.biometry.impl - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.biometry.BiometryFeatureToggles -import javax.inject.Inject - -internal class DefaultBiometryFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) : BiometryFeatureToggles { - override val isAskForBiometryEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("ASK_BIOMETRY_REFACTORING_ENABLED") -} \ No newline at end of file diff --git a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/di/FeatureModule.kt b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/di/FeatureModule.kt index 3e33b8ce67..01f1c819ce 100644 --- a/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/di/FeatureModule.kt +++ b/features/biometry/impl/src/main/kotlin/com/tangem/features/biometry/impl/di/FeatureModule.kt @@ -3,9 +3,7 @@ package com.tangem.features.biometry.impl.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.biometry.AskBiometryComponent -import com.tangem.features.biometry.BiometryFeatureToggles import com.tangem.features.biometry.impl.DefaultAskBiometryComponent -import com.tangem.features.biometry.impl.DefaultBiometryFeatureToggles import com.tangem.features.biometry.impl.model.AskBiometryModel import dagger.Binds import dagger.Module @@ -18,9 +16,6 @@ import dagger.multibindings.IntoMap @InstallIn(SingletonComponent::class) internal interface FeatureModule { - @Binds - fun bindFeatureToggles(impl: DefaultBiometryFeatureToggles): BiometryFeatureToggles - @Binds fun bindComponentFactory(impl: DefaultAskBiometryComponent.Factory): AskBiometryComponent.Factory } 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 b6a675a820..2b9e393147 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 @@ -19,7 +19,6 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.legacy.asLockable import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.biometry.AskBiometryComponent -import com.tangem.features.biometry.BiometryFeatureToggles import com.tangem.features.onboarding.v2.TitleProvider import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent @@ -32,8 +31,6 @@ 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 -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import javax.inject.Inject @@ -46,7 +43,6 @@ internal class OnboardingEntryModel @Inject constructor( private val router: Router, private val tangemSdkManager: TangemSdkManager, private val settingsRepository: SettingsRepository, - private val askBiometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, private val uiMessageSender: UiMessageSender, private val userWalletsListManager: UserWalletsListManager, @@ -148,37 +144,25 @@ internal class OnboardingEntryModel @Inject constructor( private fun navigateToFinalScreenFlow( doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated, ) { - 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, - onDone = ::exitComponentScreen, - ), - ) + 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, + onDone = ::exitComponentScreen, + ), + ) } - } else { - doIfVisa { - analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.SuccessScreenOpened) - } - stackNavigation.replaceAll( - OnboardingRoute.Done( - mode = doneMode, - onDone = ::exitComponentScreen, - ), - ) } } @@ -213,38 +197,16 @@ internal class OnboardingEntryModel @Inject constructor( } private fun exitComponentScreen() { - if (askBiometryFeatureToggles.isAskForBiometryEnabled) { - if (userWalletsListManager.hasUserWallets) { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } + if (userWalletsListManager.hasUserWallets) { + val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } - if (isLocked) { - router.replaceAll(AppRoute.Welcome()) - } else { - router.replaceAll(AppRoute.Wallet) - } + if (isLocked) { + router.replaceAll(AppRoute.Welcome()) } else { - router.replaceAll(AppRoute.Home) + router.replaceAll(AppRoute.Wallet) } } else { - if (userWalletsListManager.hasUserWallets) { - val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false } - - if (isLocked) { - router.replaceAll(AppRoute.Welcome()) - } else { - modelScope.launch(NonCancellable) { - router.replaceAll(AppRoute.Wallet) - if (tangemSdkManager.checkCanUseBiometry() && - settingsRepository.shouldShowSaveUserWalletScreen() - ) { - delay(timeMillis = 1_800) - router.push(AppRoute.SaveWallet) - } - } - } - } else { - router.replaceAll(AppRoute.Home) - } + router.replaceAll(AppRoute.Home) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 2ae1a8de10..808dd9238b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -38,7 +38,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent -import com.tangem.features.biometry.BiometryFeatureToggles import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION import com.tangem.features.pushnotifications.api.utils.getPushPermissionOrNull import com.tangem.utils.Provider @@ -75,7 +74,6 @@ internal class WalletModel @Inject constructor( private val walletImageResolver: WalletImageResolver, private val tokenListStore: MultiWalletTokenListStore, private val onrampStatusFactory: OnrampStatusFactory, - private val biometryFeatureToggles: BiometryFeatureToggles, private val analyticsEventsHandler: AnalyticsEventHandler, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, @@ -126,13 +124,9 @@ internal class WalletModel @Inject constructor( withContext(dispatchers.io) { delay(timeMillis = 1_800) } if (isShowSaveWalletScreenEnabled()) { - if (biometryFeatureToggles.isAskForBiometryEnabled) { - innerWalletRouter.dialogNavigation.activate( - configuration = WalletDialogConfig.AskForBiometry, - ) - } else { - innerWalletRouter.openSaveUserWalletScreen() - } + innerWalletRouter.dialogNavigation.activate( + configuration = WalletDialogConfig.AskForBiometry, + ) } } } 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 048f9af182..c188ec0104 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 @@ -14,7 +14,6 @@ import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig -import com.tangem.features.biometry.BiometryFeatureToggles import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import javax.inject.Inject @@ -25,7 +24,6 @@ internal class DefaultWalletRouter @Inject constructor( private val router: AppRouter, private val urlOpener: UrlOpener, private val reduxStateHolder: ReduxStateHolder, - private val biometryFeatureToggles: BiometryFeatureToggles, ) : InnerWalletRouter { override val dialogNavigation: SlotNavigation = SlotNavigation() @@ -91,12 +89,6 @@ internal class DefaultWalletRouter @Inject constructor( router.push(AppRoute.Home) } - override fun openSaveUserWalletScreen() { - if (biometryFeatureToggles.isAskForBiometryEnabled.not()) { - router.push(AppRoute.SaveWallet) - } - } - override fun isWalletLastScreen(): Boolean { return router.stack.lastOrNull() is AppRoute.Wallet } 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 393c930e9c..d839063142 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 @@ -46,9 +46,6 @@ internal interface InnerWalletRouter { /** Open stories screen */ fun openStoriesScreen() - /** Open save user wallet screen */ - fun openSaveUserWalletScreen() - /** Is wallet last screen */ fun isWalletLastScreen(): Boolean From 8cbaf872cf3eab885257c503bba436c9bd8b114e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 14:50:19 +0500 Subject: [PATCH 124/151] Updated on 2026-08-14 --- .../DefaultLegacyWalletConnectRepository.kt | 15 +++-- .../di/WalletConnectInteractorModule.kt | 3 + .../domain/LegacyWalletConnectRepository.kt | 2 +- core/res/src/main/res/values/strings.xml | 2 + .../components/notifications/Notification.kt | 5 +- .../core/ui/extensions/CryptoCurrency.kt | 11 +++- .../walletconnect/pair/WcPairSdkDelegate.kt | 2 - features/walletconnect/impl/build.gradle.kts | 1 + .../connections/entity/WcAppInfoUM.kt | 20 +++++- .../connections/model/WcAppInfoModel.kt | 60 +++++++++++++++++- .../transformers/WcAppInfoTransformer.kt | 62 +++++++++++++++++++ .../WcConnectButtonProgressTransformer.kt | 13 ++++ .../connections/ui/WcAppInfoContent.kt | 47 ++++++-------- 13 files changed, 203 insertions(+), 40 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcConnectButtonProgressTransformer.kt diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 25d7dffa0a..12c73bd711 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -10,6 +10,7 @@ import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.pair.unsupportedDApps import com.tangem.domain.walletconnect.model.legacy.Account +import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles import com.tangem.tap.common.analytics.events.WalletConnect import com.tangem.tap.domain.walletconnect2.app.TangemWcBlockchainHelper @@ -29,6 +30,7 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor( private val stub: LegacyWalletConnectRepositoryStub, private val legacy: DefaultLegacyWalletConnectRepository, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, + private val wcInitializeUseCase: WcInitializeUseCase, ) : LegacyWalletConnectRepository { private val isNewWc by lazy { walletConnectFeatureToggles.isRedesignedWalletConnectEnabled @@ -43,8 +45,13 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor( if (isNewWc) stub.currentSessions else legacy.currentSessions } - override fun init(projectId: String) { - if (isNewWc) stub.init(projectId) else legacy.init(projectId) + override suspend fun init(projectId: String) { + if (isNewWc) { + stub.init(projectId) + wcInitializeUseCase.init() // TODO(wc) Nikolai: Delete after RoutingComponent finished. + } else { + legacy.init(projectId) + } } override fun setUserNamespaces(userNamespaces: Map>) { @@ -89,7 +96,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository override val activeSessions: Flow> = emptyFlow() override val currentSessions: List = listOf() - override fun init(projectId: String) = Unit + override suspend fun init(projectId: String) = Unit override fun setUserNamespaces(userNamespaces: Map>) = Unit @@ -134,7 +141,7 @@ internal class DefaultLegacyWalletConnectRepository( /** * @param projectId Project ID at https://cloud.walletconnect.com/ */ - override fun init(projectId: String) { + override suspend fun init(projectId: String) { val relayUrl = "relay.walletconnect.com" val serverUrl = "wss://$relayUrl?projectId=$projectId" val connectionType = ConnectionType.AUTOMATIC diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt index 265fe31e47..8d95e1f664 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/di/WalletConnectInteractorModule.kt @@ -8,6 +8,7 @@ import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.files.FileReader import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletconnect.model.legacy.WalletConnectSessionsRepository +import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.features.walletconnect.components.WalletConnectFeatureToggles @@ -74,6 +75,7 @@ internal object WalletConnectModule { wcRequestDeserializer: WcJrpcRequestsDeserializer, analyticsHandler: AnalyticsEventHandler, walletConnectFeatureToggles: WalletConnectFeatureToggles, + wcInitializeUseCase: WcInitializeUseCase, ): LegacyWalletConnectRepository { val legacy = DefaultLegacyWalletConnectRepository( application = application, @@ -85,6 +87,7 @@ internal object WalletConnectModule { stub, legacy, walletConnectFeatureToggles, + wcInitializeUseCase, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 59d5954e00..10092dc183 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -13,7 +13,7 @@ interface LegacyWalletConnectRepository { val currentSessions: List - fun init(projectId: String) + suspend fun init(projectId: String) fun setUserNamespaces(userNamespaces: Map>) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 94ac2266d6..90a9ac6c99 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1307,6 +1307,8 @@ New connection Connect your wallet to a different dApps No sessions + This domain is flagged as unsafe by multiple security providers. Leave immediately to protect your assets + Known security risk Wallet connect Discard You have an interrupted backup. Do you want to resume? diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt index 7bd5322685..cf66eead2c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/Notification.kt @@ -52,6 +52,7 @@ import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsSta fun Notification( config: NotificationConfig, modifier: Modifier = Modifier, + titleColor: Color = TangemTheme.colors.text.primary1, subtitleColor: Color = TangemTheme.colors.text.tertiary, containerColor: Color? = null, iconTint: Color? = null, @@ -69,6 +70,7 @@ fun Notification( iconResId = config.iconResId, iconTint = iconTint, title = config.title, + titleColor = titleColor, subtitle = config.subtitle, subtitleColor = subtitleColor, showArrowIcon = isEnabled && config.showArrowIcon, @@ -129,6 +131,7 @@ private fun MainContent( iconTint: Color?, title: TextReference?, subtitle: TextReference, + titleColor: Color, subtitleColor: Color, showArrowIcon: Boolean, ) { @@ -143,7 +146,7 @@ private fun MainContent( SpacerW(width = TangemTheme.dimens.spacing10) - TextsBlock(title = title, subtitle = subtitle, subtitleColor = subtitleColor) + TextsBlock(title = title, titleColor = titleColor, subtitle = subtitle, subtitleColor = subtitleColor) if (showArrowIcon) { SpacerWMax() diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt index 277d3f4a52..a1cace1217 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/CryptoCurrency.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.graphics.luminance import androidx.core.graphics.toColorInt import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network private const val LIGHT_LUMINANCE = 0.5f private const val COLOR_HEX_START_INDEX = 2 @@ -18,7 +19,15 @@ private const val COLOR_HEX_END_INDEX = 7 */ @get:DrawableRes val CryptoCurrency.networkIconResId: Int - get() = getActiveIconRes(network.id.value) + get() = network.iconResId + +/** + * Retrieves the resource ID. + * + * @return Drawable resource ID for the network. + */ +val Network.iconResId: Int + get() = getActiveIconRes(id.value) /** * Tries to extract a background color from the contract address of a token. 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 1c11137607..54f931d604 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 @@ -10,7 +10,6 @@ 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 @@ -24,7 +23,6 @@ internal class WcPairSdkDelegate : WcSdkObserver { suspend fun pair(url: String): Either = coroutineScope { suspend fun proposalCallback() = onSessionProposal .receiveAsFlow() - .filter { proposal -> proposal.url == url } .first() val pairCall = async { sdkPair(url) } diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 841499063e..88f123d6cd 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { implementation(projects.common.ui) /** Domain models */ + implementation(projects.domain.blockaid.models) implementation(projects.domain.qrScanning.models) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt index 401b13e42d..413724be60 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -3,6 +3,9 @@ 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 com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.walletconnect.impl.R import kotlinx.collections.immutable.ImmutableList @Immutable @@ -21,7 +24,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { val appIcon: String, val isVerified: Boolean, val appSubtitle: String, - val notificationUM: NotificationUM?, + val notification: WcAppInfoSecurityNotification?, val walletName: String, val networksInfo: WcNetworksInfo, override val connectButtonConfig: WcPrimaryButtonConfig, @@ -29,6 +32,21 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { ) : WcAppInfoUM() } +sealed class WcAppInfoSecurityNotification( + title: TextReference, + subtitle: TextReference, +) : NotificationUM.Info(title = title, subtitle = subtitle) { + data object UnknownDomain : WcAppInfoSecurityNotification( + title = resourceReference(R.string.wc_alert_audit_unknown_domain), + subtitle = resourceReference(R.string.wc_alert_domain_issues_description), + ) + + data object SecurityRisk : WcAppInfoSecurityNotification( + title = resourceReference(R.string.wc_notification_security_risk_title), + subtitle = resourceReference(R.string.wc_notification_security_risk_subtitle), + ) +} + @Immutable internal sealed class WcNetworksInfo { data class MissingRequiredNetworkInfo(val networks: String) : WcNetworksInfo() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt index cb7aa69d4a..d2c9722fbc 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt @@ -4,31 +4,85 @@ import androidx.compose.runtime.Stable import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.walletconnect.model.WcPairRequest +import com.tangem.domain.walletconnect.model.WcSessionApprove +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.walletconnect.usecase.pair.WcPairState +import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase +import com.tangem.domain.wallets.models.UserWallet import com.tangem.features.walletconnect.connections.components.WcAppInfoContainerComponent import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig +import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.* import javax.inject.Inject +import kotlin.properties.Delegates +import com.tangem.utils.transformer.update as transformerUpdate @Stable @ModelScoped internal class WcAppInfoModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, + wcPairUseCaseFactory: WcPairUseCase.Factory, paramsContainer: ParamsContainer, ) : Model() { private val params: WcAppInfoContainerComponent.Params = paramsContainer.require() + private val wcPairUseCase = wcPairUseCaseFactory.create(WcPairRequest(uri = params.wcUrl, source = params.source)) val appInfoUiState: StateFlow field = MutableStateFlow(createLoadingState()) + // TODO(wc) Doston: Temp solution, will be fixed in next PR`s + private var userWallet by Delegates.notNull() + private var proposalNetwork by Delegates.notNull() + + init { + loadDAppInfo() + } + + private fun loadDAppInfo() { + wcPairUseCase() + .onEach { state -> + when (state) { + is WcPairState.Approving.Loading -> appInfoUiState.transformerUpdate( + WcConnectButtonProgressTransformer(showProgress = true), + ) + is WcPairState.Approving.Result -> { + appInfoUiState.transformerUpdate( + WcConnectButtonProgressTransformer(showProgress = false), + ) + // TODO: wc dismiss and reload sessions + } + is WcPairState.Error -> { + // TODO: wc show toast/snackbar/alert? + } + is WcPairState.Loading -> appInfoUiState.update { createLoadingState() } + is WcPairState.Proposal -> { + userWallet = state.dAppSession.proposalNetwork.keys.first { !it.isLocked } + proposalNetwork = state.dAppSession.proposalNetwork.getValue(userWallet) + appInfoUiState.transformerUpdate( + WcAppInfoTransformer( + dAppSession = state.dAppSession, + onDismiss = ::dismiss, + onConnect = ::onConnect, + userWallet = userWallet, + proposalNetwork = proposalNetwork, + ), + ) + } + } + } + .launchIn(modelScope) + } + fun dismiss() { params.onDismiss() } private fun onConnect() { - // TODO: [REDACTED_JIRA] + wcPairUseCase.approve(WcSessionApprove(wallet = userWallet, network = proposalNetwork.required.toList())) } private fun createLoadingState(): WcAppInfoUM.Loading { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt new file mode 100644 index 0000000000..d64ddbd43b --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -0,0 +1,62 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.tangem.core.ui.extensions.iconResId +import com.tangem.domain.walletconnect.model.WcSessionProposal +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo +import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.toImmutableList + +internal class WcAppInfoTransformer( + private val dAppSession: WcSessionProposal, + private val onDismiss: () -> Unit, + private val onConnect: () -> Unit, + private val userWallet: UserWallet, + private val proposalNetwork: WcSessionProposal.ProposalNetwork, +) : Transformer { + + override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { + return WcAppInfoUM.Content( + appName = dAppSession.dAppMetaData.name, + appIcon = dAppSession.dAppMetaData.icons.firstOrNull().orEmpty(), + isVerified = dAppSession.securityStatus == CheckDAppResult.SAFE, + appSubtitle = dAppSession.dAppMetaData.description, + notification = createNotification(dAppSession.securityStatus), + walletName = userWallet.name, + networksInfo = convertNetworksInfo(proposalNetwork), + connectButtonConfig = WcPrimaryButtonConfig( + showProgress = false, + enabled = proposalNetwork.missingRequired.isEmpty(), + onClick = onConnect, + ), + onDismiss = onDismiss, + ) + } + + private fun createNotification(securityStatus: CheckDAppResult): WcAppInfoSecurityNotification? { + return when (securityStatus) { + CheckDAppResult.SAFE -> null + CheckDAppResult.UNSAFE -> WcAppInfoSecurityNotification.SecurityRisk + CheckDAppResult.FAILED_TO_VERIFY -> WcAppInfoSecurityNotification.UnknownDomain + } + } + + private fun convertNetworksInfo(proposalNetwork: WcSessionProposal.ProposalNetwork): WcNetworksInfo { + return if (proposalNetwork.missingRequired.isNotEmpty()) { + WcNetworksInfo.MissingRequiredNetworkInfo( + networks = proposalNetwork.missingRequired + .joinToString { it.name }, + ) + } else { + WcNetworksInfo.ContainsAllRequiredNetworks( + icons = (proposalNetwork.required + proposalNetwork.available) + .map { it.iconResId } + .toImmutableList(), + ) + } + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcConnectButtonProgressTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcConnectButtonProgressTransformer.kt new file mode 100644 index 0000000000..0a2fe643d7 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcConnectButtonProgressTransformer.kt @@ -0,0 +1,13 @@ +package com.tangem.features.walletconnect.connections.model.transformers + +import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM +import com.tangem.utils.transformer.Transformer + +internal class WcConnectButtonProgressTransformer(private val showProgress: Boolean) : Transformer { + override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { + val contentState = prevState as? WcAppInfoUM.Content ?: return prevState + return contentState.copy( + connectButtonConfig = contentState.connectButtonConfig.copy(showProgress = showProgress), + ) + } +} \ 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 index 540be9a66f..eef9435ca0 100644 --- 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 @@ -28,15 +28,16 @@ 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.* 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.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig @@ -61,11 +62,23 @@ private fun WcAppInfoModalBottomSheetContent(state: WcAppInfoUM.Content, modifie .background(color = TangemTheme.colors.background.action) .fillMaxWidth() WcAppInfoFirstBlock(modifier = blocksModifier, state = state) - state.notificationUM?.config?.let { config -> + state.notification?.let { notification -> Notification( modifier = Modifier.padding(top = TangemTheme.dimens.spacing14), - config = config, - iconTint = TangemTheme.colors.icon.attention, + config = notification.config, + containerColor = when (notification) { + WcAppInfoSecurityNotification.SecurityRisk -> TangemColorPalette.Amaranth.copy(alpha = 0.1F) + WcAppInfoSecurityNotification.UnknownDomain -> null + }, + titleColor = when (notification) { + WcAppInfoSecurityNotification.SecurityRisk -> TangemTheme.colors.text.warning + WcAppInfoSecurityNotification.UnknownDomain -> TangemTheme.colors.text.primary1 + }, + subtitleColor = TangemTheme.colors.text.primary1, + iconTint = when (notification) { + WcAppInfoSecurityNotification.SecurityRisk -> TangemTheme.colors.icon.warning + WcAppInfoSecurityNotification.UnknownDomain -> TangemTheme.colors.icon.attention + }, ) } WcAppInfoSecondBlock( @@ -531,23 +544,6 @@ private fun WcLoadingAppInfoItem(modifier: Modifier = Modifier) { } // endregion -@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() { - TangemThemePreview { - WcAppInfoModalBottomSheetLoading( - state = WcAppInfoUM.Loading( - onDismiss = {}, - WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {}), - ), - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) - } -} - @Composable @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -564,13 +560,13 @@ private fun WcAppInfoBottomSheetPreview(@PreviewParameter(WcAppInfoStateProvider private class WcAppInfoStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WcAppInfoUM.Loading(onDismiss = {}, WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {})), + // WcAppInfoUM.Loading(onDismiss = {}, WcPrimaryButtonConfig(showProgress = false, enabled = false, onClick = {})), WcAppInfoUM.Content( appName = "React App", appIcon = "", isVerified = true, appSubtitle = "react-app.walletconnect.com", - notificationUM = null, + notification = WcAppInfoSecurityNotification.SecurityRisk, walletName = "Tangem 2.0", networksInfo = WcNetworksInfo.ContainsAllRequiredNetworks( icons = persistentListOf( @@ -589,10 +585,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider Date: Fri, 25 Apr 2025 15:45:38 +0300 Subject: [PATCH 125/151] 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 7a607593e3..a20134ee4b 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 7a607593e34f0977474613f077532215eaf0ffe6 +Subproject commit a20134ee4be4d7e1e34c6f44089b3cb0713eacc3 From fea48e3064cf0f063302cfe13c6af8a4ff75686d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 16:45:34 +0300 Subject: [PATCH 126/151] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/TangemApplication.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index bb4aa08278..c4e6516129 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -291,10 +291,10 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat excludedBlockchainsManager.init() }, ) + initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) } appScope.launch { - initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) launch(Dispatchers.IO) { loadNativeLibraries() walletConnect2Repository.init( From 4aa74c4b7017889db2b89a9709dac83bad9bed09 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 20:04:51 +0500 Subject: [PATCH 127/151] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 8 ++ .../api/common/adapter/BigIntegerAdapter.kt | 13 ++ .../com/tangem/datasource/di/MoshiModule.kt | 8 +- .../nft/converter/NFTSdkAssetConverter.kt | 55 ++++++-- .../NFTSdkAssetIdentifierConverter.kt | 2 + .../NFTSdkAssetSalePriceConverter.kt | 25 ++++ .../DefaultTransactionRepository.kt | 49 ++++++++ .../serialization/BigIntegerSerializer.kt | 21 ++++ .../serialization/SerializedBigInteger.kt | 6 + .../com/tangem/domain/nft/models/NFTAsset.kt | 14 ++- domain/transaction/build.gradle.kts | 6 +- .../transaction/TransactionRepository.kt | 12 ++ .../CreateApprovalTransactionUseCase.kt | 19 +++ .../CreateNFTTransferTransactionUseCase.kt | 59 +++++++++ .../entity/factory/NFTDetailsUMFactory.kt | 2 +- features/send-v2/impl/build.gradle.kts | 1 + .../v2/send/confirm/model/SendConfirmModel.kt | 5 +- .../confirm/model/NFTSendConfirmModel.kt | 118 ++++++++++++------ .../send/v2/sendnft/model/NFTSendModel.kt | 41 ++++-- .../helpers/StakingFeeTransactionLoader.kt | 1 - gradle/tangem_dependencies.toml | 2 +- 21 files changed, 397 insertions(+), 70 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigIntegerAdapter.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt create mode 100644 domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateNFTTransferTransactionUseCase.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 45734e3796..77c2518104 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 @@ -188,4 +188,12 @@ internal object TransactionDomainModule { ): SignUseCase { return SignUseCase(cardSdkConfigRepository, walletManagersFacade) } + + @Provides + @Singleton + fun provideCreateNFTTransferTransactionUseCase( + transactionRepository: TransactionRepository, + ): CreateNFTTransferTransactionUseCase { + return CreateNFTTransferTransactionUseCase(transactionRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigIntegerAdapter.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigIntegerAdapter.kt new file mode 100644 index 0000000000..00fca862ea --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/adapter/BigIntegerAdapter.kt @@ -0,0 +1,13 @@ +package com.tangem.datasource.api.common.adapter + +import com.squareup.moshi.FromJson +import com.squareup.moshi.ToJson +import java.math.BigInteger + +internal class BigIntegerAdapter { + @FromJson + fun fromJson(value: String) = BigInteger(value) + + @ToJson + fun toJson(value: BigInteger) = value.toString() +} \ No newline at end of file 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 fad4677392..37ea5507c5 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 @@ -6,10 +6,7 @@ import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import com.tangem.blockchain.nft.models.NFTAsset import com.tangem.blockchain.nft.models.NFTCollection import com.tangem.common.json.MoshiJsonConverter -import com.tangem.datasource.api.common.adapter.BigDecimalAdapter -import com.tangem.datasource.api.common.adapter.DateTimeAdapter -import com.tangem.datasource.api.common.adapter.LocalDateAdapter -import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters +import com.tangem.datasource.api.common.adapter.* import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.models.scan.serialization.* @@ -38,6 +35,7 @@ class MoshiModule { .withDefaultValue(ProviderModel.UnsupportedType), ) .add(BigDecimalAdapter()) + .add(BigIntegerAdapter()) .add(LocalDateAdapter()) .add(DateTimeAdapter()) .add(VisaActivationRemoteState.jsonAdapter) @@ -73,6 +71,7 @@ class MoshiModule { val adapters = MoshiJsonConverter.getTangemSdkAdapters() + listOf( BigDecimalAdapter(), + BigIntegerAdapter(), WalletDerivedKeysMapAdapter(), ScanResponseDerivedKeysMapAdapter(), ByteArrayKeyAdapter(), @@ -100,6 +99,7 @@ class MoshiModule { adapters = MoshiJsonConverter.getTangemSdkAdapters() + listOf( BigDecimalAdapter(), + BigIntegerAdapter(), WalletDerivedKeysMapAdapter(), ScanResponseDerivedKeysMapAdapter(), ByteArrayKeyAdapter(), diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt index db95b8ce76..be1c8f0aeb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetConverter.kt @@ -4,14 +4,17 @@ import com.tangem.domain.models.StatusSource import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.nft.models.NFTSalePrice import com.tangem.domain.tokens.model.Network -import com.tangem.utils.converter.Converter +import com.tangem.utils.converter.TwoWayConverter import com.tangem.blockchain.nft.models.NFTAsset as SdkNFTAsset -object NFTSdkAssetConverter : Converter, NFTAsset> { +object NFTSdkAssetConverter : TwoWayConverter, NFTAsset> { override fun convert(value: Pair): NFTAsset { val (network, asset) = value val assetId = NFTSdkAssetIdentifierConverter.convert(asset.identifier) val collectionId = NFTSdkCollectionIdentifierConverter.convert(asset.collectionIdentifier) + val salePrice = asset.salePrice?.let { NFTSdkAssetSalePriceConverter(assetId).convert(it) } + ?: NFTSalePrice.Empty(assetId) + return NFTAsset( id = assetId, collectionId = collectionId, @@ -20,13 +23,9 @@ object NFTSdkAssetConverter : Converter, NFTAsset> { owner = asset.owner, name = asset.name, description = asset.description, - salePrice = asset.salePrice?.let { - NFTSalePrice.Value( - assetId = assetId, - value = it.value, - symbol = it.symbol, - ) - } ?: NFTSalePrice.Empty(assetId = assetId), + amount = asset.amount, + decimals = asset.decimals, + salePrice = salePrice, rarity = asset.rarity?.let { NFTAsset.Rarity( rank = it.rank, @@ -48,4 +47,42 @@ object NFTSdkAssetConverter : Converter, NFTAsset> { source = StatusSource.CACHE, ) } + + override fun convertBack(value: NFTAsset): Pair { + val assetId = NFTSdkAssetIdentifierConverter.convertBack(value.id) + val collectionId = NFTSdkCollectionIdentifierConverter.convertBack(value.collectionId) + val salePrice = (value.salePrice as? NFTSalePrice.Value)?.let { + NFTSdkAssetSalePriceConverter(value.id).convertBack(it) + } + return value.network to SdkNFTAsset( + identifier = assetId, + collectionIdentifier = collectionId, + blockchainId = value.network.id.value, + contractType = value.contractType, + owner = value.owner, + name = value.name, + description = value.description, + amount = value.amount, + decimals = value.decimals, + salePrice = salePrice, + rarity = value.rarity?.let { + SdkNFTAsset.Rarity( + rank = it.rank, + label = it.label, + ) + }, + media = value.media?.let { + SdkNFTAsset.Media( + url = it.url, + mimetype = it.mimetype, + ) + }, + traits = value.traits.map { + SdkNFTAsset.Trait( + name = it.name, + value = it.value, + ) + }, + ) + } } \ No newline at end of file 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 090e711d37..bd93af8f19 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 @@ -9,6 +9,7 @@ object NFTSdkAssetIdentifierConverter : TwoWayConverter NFTAsset.Identifier.EVM( tokenId = value.tokenId, tokenAddress = value.tokenAddress, + contractType = NFTAsset.Identifier.EVM.ContractType.valueOf(value.contractType.name), ) is SdkNFTAsset.Identifier.TON -> NFTAsset.Identifier.TON( tokenAddress = value.tokenAddress, @@ -24,6 +25,7 @@ object NFTSdkAssetIdentifierConverter : TwoWayConverter SdkNFTAsset.Identifier.EVM( tokenId = value.tokenId, tokenAddress = value.tokenAddress, + contractType = SdkNFTAsset.Identifier.EVM.ContractType.valueOf(value.contractType.name), ) is NFTAsset.Identifier.TON -> SdkNFTAsset.Identifier.TON( tokenAddress = value.tokenAddress, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt new file mode 100644 index 0000000000..252753005a --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/nft/converter/NFTSdkAssetSalePriceConverter.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.local.nft.converter + +import com.tangem.domain.nft.models.NFTAsset +import com.tangem.domain.nft.models.NFTSalePrice +import com.tangem.utils.converter.TwoWayConverter +import com.tangem.blockchain.nft.models.NFTAsset.SalePrice as SDKSalePrice + +internal class NFTSdkAssetSalePriceConverter( + private val assetId: NFTAsset.Identifier, +) : TwoWayConverter { + override fun convert(value: SDKSalePrice): NFTSalePrice.Value { + return NFTSalePrice.Value( + assetId = assetId, + value = value.value, + symbol = value.symbol, + ) + } + + override fun convertBack(value: NFTSalePrice.Value): SDKSalePrice { + return SDKSalePrice( + symbol = value.symbol, + value = value.value, + ) + } +} \ 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 38c3cb363b..e76e34479a 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 @@ -17,6 +17,7 @@ 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.blockchain.nft.models.NFTAsset import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.tokens.model.CryptoCurrency @@ -30,6 +31,7 @@ import timber.log.Timber import java.math.BigDecimal import java.math.BigInteger +@Suppress("LargeClass") internal class DefaultTransactionRepository( private val walletManagersFacade: WalletManagersFacade, private val walletManagersStore: WalletManagersStore, @@ -144,6 +146,53 @@ internal class DefaultTransactionRepository( ) } + override suspend fun createNFTTransferTransaction( + ownerAddress: String, + nftAsset: NFTAsset, + fee: Fee?, + memo: String?, + destinationAddress: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData.Uncompiled = withContext(coroutineDispatcherProvider.io) { + val blockchain = Blockchain.fromId(network.id.value) + + val nftTransferCallData = SmartContractCallDataProviderFactory.getNFTTransferCallData( + destinationAddress = destinationAddress, + ownerAddress = ownerAddress, + nftAsset = nftAsset, + blockchain = blockchain, + ) + + val extras = if (nftTransferCallData != null) { + createTransactionDataExtras( + callData = nftTransferCallData, + network = network, + nonce = null, + gasLimit = null, + ) + } else { + null + } + + return@withContext createTransaction( + amount = Amount( + value = nftAsset.amount?.toBigDecimal() ?: error("Invalid amount"), + token = Token( + symbol = blockchain.currency, + contractAddress = "", + decimals = nftAsset.decimals ?: error("Invalid decimals"), + ), + ), + fee = fee, + memo = memo, + destination = destinationAddress, + userWalletId = userWalletId, + network = network, + txExtras = extras, + ) + } + override suspend fun validateTransaction( amount: Amount, fee: Fee?, diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt new file mode 100644 index 0000000000..93394b1e0c --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/BigIntegerSerializer.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.core.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.math.BigInteger + +object BigIntegerSerializer : KSerializer { + + override val descriptor = PrimitiveSerialDescriptor(serialName = "BigInteger", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: BigInteger) { + encoder.encodeString(value.toString()) + } + + override fun deserialize(decoder: Decoder): BigInteger { + return BigInteger(decoder.decodeString()) + } +} \ No newline at end of file diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt new file mode 100644 index 0000000000..8fbbda7eea --- /dev/null +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/serialization/SerializedBigInteger.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.core.serialization + +import kotlinx.serialization.Serializable +import java.math.BigInteger + +typealias SerializedBigInteger = @Serializable(with = BigIntegerSerializer::class) BigInteger \ 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 2c75ead2f4..2658873319 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 @@ -1,5 +1,6 @@ package com.tangem.domain.nft.models +import com.tangem.domain.core.serialization.SerializedBigInteger import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.Network import kotlinx.serialization.Serializable @@ -13,6 +14,8 @@ data class NFTAsset( val owner: String?, val name: String?, val description: String?, + val amount: SerializedBigInteger?, + val decimals: Int?, val salePrice: NFTSalePrice, val rarity: Rarity?, val media: Media?, @@ -44,10 +47,17 @@ data class NFTAsset( @Serializable data class EVM( - val tokenId: String, val tokenAddress: String, + val tokenId: SerializedBigInteger, + val contractType: ContractType, ) : Identifier() { - override val stringValue: String = "${tokenAddress}_$tokenId" + override val stringValue: String = "${contractType}_$tokenId" + + enum class ContractType { + ERC1155, + ERC721, + Unknown, + } } @Serializable diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index dbae9c5373..30cedbb7c9 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(projects.core.utils) implementation(projects.core.ui) + implementation(projects.core.datasource) /** Tangem SDKs */ implementation(tangemDeps.card.core) @@ -22,10 +23,11 @@ dependencies { } implementation(tangemDeps.blockchain) - implementation(projects.domain.models) - implementation(projects.domain.legacy) implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) + + implementation(projects.domain.models) + implementation(projects.domain.legacy) implementation(projects.domain.wallets.models) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) 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 5b7499c85a..2149911b09 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 @@ -5,6 +5,7 @@ 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.blockchain.nft.models.NFTAsset import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId @@ -34,6 +35,17 @@ interface TransactionRepository { network: Network, ): TransactionData.Uncompiled + @Suppress("LongParameterList") + suspend fun createNFTTransferTransaction( + ownerAddress: String, + nftAsset: NFTAsset, + fee: Fee?, + memo: String?, + destinationAddress: String, + userWalletId: UserWalletId, + network: Network, + ): TransactionData.Uncompiled + @Suppress("LongParameterList") suspend fun createApprovalTransaction( amount: Amount, 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 903025cb16..d15b407307 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 @@ -34,4 +34,23 @@ class CreateApprovalTransactionUseCase( fee = fee, ) } + + @Suppress("LongParameterList") + suspend operator fun invoke( + cryptoCurrency: CryptoCurrency.Token, + userWalletId: UserWalletId, + amount: BigDecimal?, + contractAddress: String, + spenderAddress: String, + ) = Either.catch { + transactionRepository.createApprovalTransaction( + amount = BigDecimal.ZERO.convertToSdkAmount(cryptoCurrency), + approvalAmount = amount?.convertToSdkAmount(cryptoCurrency), + contractAddress = contractAddress, + spenderAddress = spenderAddress, + userWalletId = userWalletId, + network = cryptoCurrency.network, + fee = null, + ) + } } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateNFTTransferTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateNFTTransferTransactionUseCase.kt new file mode 100644 index 0000000000..9cf4aafd1c --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/CreateNFTTransferTransactionUseCase.kt @@ -0,0 +1,59 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.nft.models.NFTAsset +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.transaction.TransactionRepository +import com.tangem.domain.wallets.models.UserWalletId + +class CreateNFTTransferTransactionUseCase( + private val transactionRepository: TransactionRepository, +) { + /** + * [REDACTED_TODO_COMMENT] + */ + @Suppress("LongParameterList") + suspend operator fun invoke( + ownerAddress: String, + nftAsset: NFTAsset, + fee: Fee, + memo: String?, + destinationAddress: String, + userWalletId: UserWalletId, + network: Network, + ) = Either.catch { + transactionRepository.createNFTTransferTransaction( + ownerAddress = ownerAddress, + nftAsset = nftAsset, + destinationAddress = destinationAddress, + fee = fee, + memo = memo, + userWalletId = userWalletId, + network = network, + ) + } + + /** + * [REDACTED_TODO_COMMENT] + */ + @Suppress("LongParameterList") + suspend operator fun invoke( + ownerAddress: String, + nftAsset: NFTAsset, + memo: String?, + destinationAddress: String, + userWalletId: UserWalletId, + network: Network, + ) = Either.catch { + transactionRepository.createNFTTransferTransaction( + ownerAddress = ownerAddress, + nftAsset = nftAsset, + destinationAddress = destinationAddress, + fee = null, + memo = memo, + userWalletId = userWalletId, + network = network, + ) + } +} \ 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 index afe6f46072..9a8e41a51f 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 @@ -89,7 +89,7 @@ internal class NFTDetailsUMFactory( ), NFTAssetUM.BlockItem( title = resourceReference(R.string.nft_details_token_id), - value = id.tokenId, + value = id.tokenId.toString(), ), NFTAssetUM.BlockItem( title = resourceReference(R.string.nft_details_chain), diff --git a/features/send-v2/impl/build.gradle.kts b/features/send-v2/impl/build.gradle.kts index 9629c4825b..dcd5503302 100644 --- a/features/send-v2/impl/build.gradle.kts +++ b/features/send-v2/impl/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { implementation(projects.core.analytics) implementation(projects.core.configToggles) implementation(projects.core.navigation) + implementation(projects.core.datasource) api(projects.core.pagination) /** Tangem SDK */ 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 8b0a2cad19..0cdba71372 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 @@ -433,6 +433,7 @@ internal class SendConfirmModel @Inject constructor( ).filter { it.second is CommonSendRoute.Confirm }.onEach { (state, _) -> val amountUM = state.amountUM as? AmountState.Data val confirmUM = state.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( @@ -462,9 +463,9 @@ internal class SendConfirmModel @Inject constructor( } else -> resourceReference(R.string.common_send) }, - iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content }, + iconResId = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, isEnabled = confirmUM.isPrimaryButtonEnabled, - isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending, + isHapticClick = isReadyToSend, onClick = { when (confirmUM) { is ConfirmUM.Success -> appRouter.pop() 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 1adf375c6e..19424360b9 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 @@ -2,6 +2,7 @@ package com.tangem.features.send.v2.sendnft.confirm.model import android.os.SystemClock import arrow.core.getOrElse +import com.tangem.blockchain.common.TransactionData import com.tangem.common.routing.AppRouter import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped @@ -12,9 +13,14 @@ 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.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase +import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase 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.NavigationUM import com.tangem.features.send.v2.impl.R @@ -37,6 +43,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject @@ -49,21 +56,32 @@ internal class NFTSendConfirmModel @Inject constructor( private val appRouter: AppRouter, private val isSendTapHelpEnabledUseCase: IsSendTapHelpEnabledUseCase, private val neverShowTapHelpUseCase: NeverShowTapHelpUseCase, + private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val notificationsUpdateTrigger: NotificationsUpdateTrigger, private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, private val sendFeeCheckReloadListener: SendFeeCheckReloadListener, private val urlOpener: UrlOpener, private val shareManager: ShareManager, private val analyticsEventHandler: AnalyticsEventHandler, + sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, ) : Model(), NFTSendConfirmClickIntents { private val params: NFTSendConfirmComponent.Params = paramsContainer.require() + private val userWallet = params.userWallet private val cryptoCurrencyStatus = params.cryptoCurrencyStatus + private val cryptoCurrency = cryptoCurrencyStatus.currency private val _uiState = MutableStateFlow(params.state) val uiState = _uiState.asStateFlow() + private val sendBalanceUpdater = sendBalanceUpdaterFactory.create( + cryptoCurrency = cryptoCurrency, + userWallet = userWallet, + ) + private val destinationUM get() = uiState.value.destinationUM as? DestinationUM.Content private val feeUM @@ -191,34 +209,63 @@ internal class NFTSendConfirmModel @Inject constructor( } private fun verifyAndSendTransaction() { - // TODO: + val destination = destinationUM?.addressTextField?.value ?: return + val memo = destinationUM?.memoTextField?.value + val fee = feeSelectorUM?.selectedFee ?: return + val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value ?: return + + val sdkNFTAsset = NFTSdkAssetConverter.convertBack(params.nftAsset) + + modelScope.launch { + createNFTTransferTransactionUseCase( + ownerAddress = ownerAddress, + nftAsset = sdkNFTAsset.second, + fee = fee, + memo = memo, + destinationAddress = destination, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ).fold( + ifLeft = { error -> + Timber.e(error) + _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) + // alertFactory.getGenericErrorState { + // onFailedTxEmailClick(error.localizedMessage.orEmpty()) + // } + }, + ifRight = { txData -> + sendTransaction(txData) + }, + ) + } } - // 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 suspend fun sendTransaction(txData: TransactionData.Uncompiled) { + val result = sendTransactionUseCase( + txData = txData, + userWallet = params.userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ) + + _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) + + result.fold( + ifLeft = { error -> + Timber.e(error.toString()) + // 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() { sendFeeCheckReloadListener.checkReloadResultFlow.onEach { isFeeResultSuccess -> @@ -232,13 +279,13 @@ internal class NFTSendConfirmModel @Inject constructor( }.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 updateTransactionStatus(txData: TransactionData.Uncompiled) { + val txUrl = getExplorerTransactionUrlUseCase( + txHash = txData.hash.orEmpty(), + networkId = cryptoCurrency.network.id, + ).getOrElse { "" } + _uiState.update(NFTSendConfirmSentStateTransformer(txData, txUrl)) + } private fun updateConfirmNotifications() { modelScope.launch { @@ -272,6 +319,7 @@ internal class NFTSendConfirmModel @Inject constructor( transform = { state, route -> state to route }, ).onEach { (state, _) -> val confirmUM = state.confirmUM + val isReadyToSend = confirmUM is ConfirmUM.Content && !confirmUM.isSending params.callback.onResult( state.copy( navigationUM = NavigationUM.Content( @@ -301,9 +349,9 @@ internal class NFTSendConfirmModel @Inject constructor( } else -> resourceReference(R.string.common_send) }, - iconResId = R.drawable.ic_tangem_24.takeIf { confirmUM is ConfirmUM.Content }, + iconResId = R.drawable.ic_tangem_24.takeIf { isReadyToSend }, isEnabled = confirmUM.isPrimaryButtonEnabled, - isHapticClick = confirmUM is ConfirmUM.Content && !confirmUM.isSending, + isHapticClick = isReadyToSend, onClick = { when (confirmUM) { is ConfirmUM.Success -> appRouter.pop() 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 65d322cf3f..71fb3be509 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 @@ -3,11 +3,13 @@ package com.tangem.features.send.v2.sendnft.model import androidx.compose.runtime.Stable import arrow.core.Either import arrow.core.getOrElse +import arrow.core.left import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router +import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.cardTypesResolver @@ -17,6 +19,8 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.NFTSendComponent @@ -52,6 +56,8 @@ internal class NFTSendModel @Inject constructor( private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val getCurrencyStatusUpdatesUseCase: GetCurrencyStatusUpdatesUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, + private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, + private val getFeeUseCase: GetFeeUseCase, ) : Model(), SendNFTComponentCallback { val params: NFTSendComponent.Params = paramsContainer.require() @@ -92,20 +98,29 @@ internal class NFTSendModel @Inject constructor( } suspend fun loadFee(): Either { - TODO() - // val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") - // val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value - // ?: error("Invalid owner address") - // val enteredDestinationAddress = destinationUM.addressTextField.value - // val nftAsset = null // NFTSdkAssetConverter.convertBack(params.nftAsset).second + val destinationUM = uiState.value.destinationUM as? DestinationUM.Content ?: error("Invalid destination") + val ownerAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ?: error("Invalid owner address") + val enteredDestinationAddress = destinationUM.addressTextField.value + val enteredMemo = destinationUM.memoTextField?.value + val nftAsset = NFTSdkAssetConverter.convertBack(params.nftAsset).second - // getNFTTransferFeeUseCase( - // ownerAddress = ownerAddress, - // destinationAddress = enteredDestinationAddress, - // nftAsset = nftAsset, - // userWallet = userWallet, - // cryptoCurrency = cryptoCurrency, - // ) + val nftSendTransaction = createNFTTransferTransactionUseCase( + ownerAddress = ownerAddress, + nftAsset = nftAsset, + memo = enteredMemo, + destinationAddress = enteredDestinationAddress, + userWalletId = userWallet.walletId, + network = cryptoCurrency.network, + ).getOrElse { + return GetFeeError.DataError(it).left() + } + + return getFeeUseCase( + transactionData = nftSendTransaction, + network = cryptoCurrency.network, + userWallet = userWallet, + ) } private fun initAppCurrency() { 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 3bbd7b2f6e..0acd1468a2 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 @@ -192,7 +192,6 @@ internal class StakingFeeTransactionLoader @AssistedInject constructor( cryptoCurrency = tokenCurrency, userWalletId = userWallet.walletId, amount = amount, - fee = null, contractAddress = tokenCurrency.contractAddress, spenderAddress = validatorAddress, ).getOrElse { diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a0bc01da42..ac75068c2a 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-1042" +tangemBlockchainSdk = "develop-1046" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 3d16de3f4f54464e39db6141c459df8f56c74061 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 24 Apr 2025 11:23:21 +0300 Subject: [PATCH 128/151] Updated on 2026-08-14 --- .../CommonYieldBalanceFetcherDelegate.kt | 31 ++++++- .../multi/DefaultMultiYieldBalanceFetcher.kt | 4 + .../DefaultSingleYieldBalanceFetcher.kt | 4 + .../DefaultMultiYieldBalanceFetcherTest.kt | 77 ++++++++++++++++++ .../DefaultSingleYieldBalanceFetcherTest.kt | 81 +++++++++++++++++++ .../BaseCurrencyStatusOperations.kt | 34 ++++++-- 6 files changed, 221 insertions(+), 10 deletions(-) diff --git a/data/staking/src/main/java/com/tangem/data/staking/fetcher/CommonYieldBalanceFetcherDelegate.kt b/data/staking/src/main/java/com/tangem/data/staking/fetcher/CommonYieldBalanceFetcherDelegate.kt index 7204495961..413cf7046d 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/fetcher/CommonYieldBalanceFetcherDelegate.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/fetcher/CommonYieldBalanceFetcherDelegate.kt @@ -6,27 +6,32 @@ import arrow.core.left import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.raise.ensure +import arrow.core.toOption import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.core.utils.catchOn import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.model.StakingID +import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import timber.log.Timber internal fun commonFetcher( implementor: YieldBalanceFetcherImplementor, + userWalletsStore: UserWalletsStore, stakingYieldsStore: StakingYieldsStore, yieldsBalancesStore: YieldsBalancesStore, dispatchers: CoroutineDispatcherProvider, ): FlowFetcher { return CommonYieldBalanceFetcher( implementor = implementor, + userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, yieldsBalancesStore = yieldsBalancesStore, dispatchers = dispatchers, @@ -36,19 +41,25 @@ internal fun commonFetcher( /** * Common implementation of YieldBalanceFetcher * - * @param implementor fetcher implementor + * @property implementor fetcher implementor + * @property userWalletsStore user wallets store * @property stakingYieldsStore staking yields store * @property yieldsBalancesStore yields balances store * @property dispatchers dispatchers */ private class CommonYieldBalanceFetcher( private val implementor: YieldBalanceFetcherImplementor, + private val userWalletsStore: UserWalletsStore, private val stakingYieldsStore: StakingYieldsStore, private val yieldsBalancesStore: YieldsBalancesStore, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { override suspend fun invoke(params: Params): Either { + checkIsSupportedByWalletOrElse(userWalletId = params.userWalletId) { + return it.left() + } + val stakingIds = getStakingIds(params).getOrElse { return it.left() } @@ -58,7 +69,23 @@ private class CommonYieldBalanceFetcher( implementor.fetch(params = params, stakingIds = stakingIds, requests) } - .onLeft { yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds) } + .onLeft { + Timber.e(it, "Unable to fetch yield balances $params") + yieldsBalancesStore.storeError(userWalletId = params.userWalletId, stakingIds = stakingIds) + } + } + + private inline fun checkIsSupportedByWalletOrElse(userWalletId: UserWalletId, ifNotSupported: (Throwable) -> Unit) { + val maybeUserWallet = userWalletsStore.getSyncOrNull(key = userWalletId).toOption() + + val isSupportedByWallet = maybeUserWallet.isSome(UserWallet::isMultiCurrency) + + if (!isSupportedByWallet) { + val exception = IllegalStateException("Wallet $userWalletId is not supported: $maybeUserWallet") + Timber.e(exception) + + ifNotSupported(exception) + } } private suspend fun getStakingIds(params: Params): Either> = either { diff --git a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt index 7a5e721099..f804e9cdd7 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcher.kt @@ -6,6 +6,7 @@ import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher @@ -15,6 +16,7 @@ import javax.inject.Inject /** * Default implementation of [MultiYieldBalanceFetcher] * + * @property userWalletsStore user wallets store * @property stakingYieldsStore staking yields store * @property yieldsBalancesStore yields balances store * @property stakingIdFactory factory for creating StakingID @@ -24,6 +26,7 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( + private val userWalletsStore: UserWalletsStore, private val stakingYieldsStore: StakingYieldsStore, private val yieldsBalancesStore: YieldsBalancesStore, private val stakingIdFactory: StakingIdFactory, @@ -32,6 +35,7 @@ internal class DefaultMultiYieldBalanceFetcher @Inject constructor( ) : MultiYieldBalanceFetcher, FlowFetcher by commonFetcher( implementor = createMultiFetcherImplementor(yieldsBalancesStore, stakingIdFactory, stakeKitApi, dispatchers), + userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, yieldsBalancesStore = yieldsBalancesStore, dispatchers = dispatchers, diff --git a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt index a59151f771..6640397ffb 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcher.kt @@ -6,6 +6,7 @@ import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.multi.MultiYieldBalanceFetcher @@ -16,6 +17,7 @@ import javax.inject.Inject /** * Default implementation of [MultiYieldBalanceFetcher] * + * @property userWalletsStore user wallets store * @property stakingYieldsStore staking yields store * @property yieldsBalancesStore yields balances store * @property stakingIdFactory factory for creating StakingID @@ -25,6 +27,7 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class DefaultSingleYieldBalanceFetcher @Inject constructor( + private val userWalletsStore: UserWalletsStore, private val stakingYieldsStore: StakingYieldsStore, private val yieldsBalancesStore: YieldsBalancesStore, private val stakingIdFactory: StakingIdFactory, @@ -33,6 +36,7 @@ internal class DefaultSingleYieldBalanceFetcher @Inject constructor( ) : SingleYieldBalanceFetcher, FlowFetcher by commonFetcher( implementor = createSingleFetcherImplementor(yieldsBalancesStore, stakingIdFactory, stakeKitApi, dispatchers), + userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, yieldsBalancesStore = yieldsBalancesStore, dispatchers = dispatchers, diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt index 5f57b9d20d..789b151646 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/multi/DefaultMultiYieldBalanceFetcherTest.kt @@ -1,10 +1,12 @@ package com.tangem.data.staking.multi +import arrow.core.toOption import com.google.common.truth.Truth import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.data.staking.MockYieldDTOFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory @@ -13,6 +15,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.stakekit.StakeKitApi import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.model.StakingID import com.tangem.domain.wallets.models.UserWalletId @@ -26,12 +29,14 @@ import org.junit.Test */ internal class DefaultMultiYieldBalanceFetcherTest { + private val userWalletsStore: UserWalletsStore = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() private val yieldsBalancesStore: YieldsBalancesStore = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val stakeKitApi: StakeKitApi = mockk() private val fetcher = DefaultMultiYieldBalanceFetcher( + userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, yieldsBalancesStore = yieldsBalancesStore, stakingIdFactory = stakingIdFactory, @@ -45,6 +50,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs @@ -63,6 +69,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.create(params.userWalletId, ton.id, ton.network) stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) @@ -76,18 +83,77 @@ internal class DefaultMultiYieldBalanceFetcherTest { Truth.assertThat(actual.isRight()).isTrue() } + @Test + fun `fetch yields balances failure if user wallet is not supported`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + + val actual = fetcher(params) + + coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + coVerify(inverse = true) { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + } + + val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if userWalletsStore returns null`() = runTest { + val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) + + val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null + + val actual = fetcher(params) + + coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + coVerify(inverse = true) { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + } + + val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + @Test fun `fetch yields balances failure if stakingIdFactory returns empty list`() = runTest { val currencyIdWithNetworkMap = mapOf(ton.id to ton.network, solana.id to solana.network) val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns emptySet() coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns emptySet() val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.create(params.userWalletId, ton.id, ton.network) stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } @@ -113,6 +179,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs @@ -122,6 +189,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.create(params.userWalletId, ton.id, ton.network) stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) @@ -147,6 +215,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs @@ -156,6 +225,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.create(params.userWalletId, ton.id, ton.network) stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) @@ -181,6 +251,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs @@ -195,6 +266,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.create(params.userWalletId, ton.id, ton.network) stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) @@ -226,6 +298,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs @@ -237,6 +310,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.create(params.userWalletId, ton.id, ton.network) stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) @@ -268,6 +342,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val params = YieldBalanceFetcherParams.Multi(userWalletId, currencyIdWithNetworkMap) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.create(params.userWalletId, ton.id, ton.network) } returns setOf(tonId) coEvery { stakingIdFactory.create(params.userWalletId, solana.id, solana.network) } returns setOf(solanaId) coEvery { yieldsBalancesStore.refresh(params.userWalletId, tonAndSolanaIds) } just Runs @@ -287,6 +362,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.create(params.userWalletId, ton.id, ton.network) stakingIdFactory.create(params.userWalletId, solana.id, solana.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = tonAndSolanaIds) @@ -306,6 +382,7 @@ internal class DefaultMultiYieldBalanceFetcherTest { private companion object { val userWalletId = UserWalletId("011") + val userWallet = MockUserWalletFactory.create() val mocks = MockCryptoCurrencyFactory() diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt index 16e2049744..476fc620d8 100644 --- a/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/single/DefaultSingleYieldBalanceFetcherTest.kt @@ -1,10 +1,12 @@ package com.tangem.data.staking.single +import arrow.core.toOption import com.google.common.truth.Truth import com.tangem.blockchain.common.Blockchain import com.tangem.common.test.data.staking.MockYieldBalanceWrapperDTOFactory import com.tangem.common.test.data.staking.MockYieldDTOFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.data.staking.store.YieldsBalancesStore import com.tangem.data.staking.utils.StakingIdFactory import com.tangem.data.staking.utils.YieldBalanceRequestBodyFactory @@ -17,6 +19,7 @@ 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 com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.staking.fetcher.YieldBalanceFetcherParams import com.tangem.domain.staking.model.StakingID import com.tangem.domain.wallets.models.UserWalletId @@ -31,12 +34,14 @@ import java.math.BigDecimal */ internal class DefaultSingleYieldBalanceFetcherTest { + private val userWalletsStore: UserWalletsStore = mockk() private val stakingYieldsStore: StakingYieldsStore = mockk() private val yieldsBalancesStore: YieldsBalancesStore = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val stakeKitApi: StakeKitApi = mockk() private val fetcher = DefaultSingleYieldBalanceFetcher( + userWalletsStore = userWalletsStore, stakingYieldsStore = stakingYieldsStore, yieldsBalancesStore = yieldsBalancesStore, stakingIdFactory = stakingIdFactory, @@ -52,6 +57,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { network = ton.network, ) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId coEvery { yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) @@ -70,6 +76,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) stakingYieldsStore.getSyncWithTimeout() @@ -82,6 +89,67 @@ internal class DefaultSingleYieldBalanceFetcherTest { Truth.assertThat(actual.isRight()).isTrue() } + @Test + fun `fetch yields balances failure if user wallet is not supported`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet + + val actual = fetcher(params) + + coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + coVerify(inverse = true) { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + } + + val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${userWallet.toOption()}") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + + @Test + fun `fetch yields balances failure if userWalletsStore returns null`() = runTest { + val params = YieldBalanceFetcherParams.Single( + userWalletId = userWalletId, + currencyId = ton.id, + network = ton.network, + ) + + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns null + + val actual = fetcher(params) + + coVerify { userWalletsStore.getSyncOrNull(params.userWalletId) } + + coVerify(inverse = true) { + stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) + yieldsBalancesStore.refresh(userWalletId = any(), stakingIds = any()) + stakingYieldsStore.getSyncWithTimeout() + stakeKitApi.getSingleYieldBalance(integrationId = any(), body = any()) + yieldsBalancesStore.storeActual(userWalletId = any(), values = any()) + yieldsBalancesStore.storeError(userWalletId = any(), stakingIds = any()) + } + + val expected = IllegalStateException("Wallet ${params.userWalletId} is not supported: ${null.toOption()}") + + Truth.assertThat(actual.isLeft()).isTrue() + Truth.assertThat(actual.leftOrNull()).isInstanceOf(expected::class.java) + Truth.assertThat(actual.leftOrNull()).hasMessageThat().isEqualTo(expected.message) + } + @Test fun `fetch yields balances failure if stakingIdFactory createForDefault returns null`() = runTest { val params = YieldBalanceFetcherParams.Single( @@ -90,11 +158,13 @@ internal class DefaultSingleYieldBalanceFetcherTest { network = ton.network, ) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns null val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.createForDefault(userWalletId = userWalletId, currencyId = ton.id, network = ton.network) } @@ -121,6 +191,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { network = ton.network, ) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId coEvery { yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) @@ -132,6 +203,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) stakingYieldsStore.getSyncWithTimeout() @@ -158,6 +230,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { network = ton.network, ) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId coEvery { yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) @@ -169,6 +242,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) stakingYieldsStore.getSyncWithTimeout() @@ -195,6 +269,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { network = ton.network, ) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId coEvery { yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) @@ -207,6 +282,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) stakingYieldsStore.getSyncWithTimeout() @@ -239,6 +315,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { network = ton.network, ) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId coEvery { yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) @@ -251,6 +328,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) stakingYieldsStore.getSyncWithTimeout() @@ -283,6 +361,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { network = ton.network, ) + coEvery { userWalletsStore.getSyncOrNull(params.userWalletId) } returns userWallet coEvery { stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) } returns tonId coEvery { yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) @@ -302,6 +381,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { val actual = fetcher(params) coVerify { + userWalletsStore.getSyncOrNull(params.userWalletId) stakingIdFactory.createForDefault(params.userWalletId, ton.id, ton.network) yieldsBalancesStore.refresh(userWalletId = params.userWalletId, stakingIds = setOf(tonId)) stakingYieldsStore.getSyncWithTimeout() @@ -353,6 +433,7 @@ internal class DefaultSingleYieldBalanceFetcherTest { private companion object { val userWalletId = UserWalletId("011") + val userWallet = MockUserWalletFactory.create() val mocks = MockCryptoCurrencyFactory() 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 fed0750cf9..c2d41790de 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 @@ -91,6 +91,7 @@ abstract class BaseCurrencyStatusOperations( userWalletId: UserWalletId, currency: CryptoCurrency, includeQuotes: Boolean = true, + subscribeOnYieldBalance: Boolean = true, ): Flow> { val rawCurrencyId = currency.id.rawCurrencyId @@ -117,13 +118,24 @@ abstract class BaseCurrencyStatusOperations( val yieldBalanceFlow = getYieldBalance(userWalletId = userWalletId, cryptoCurrency = currency) - return combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> - currencyStatusProxyCreator.createCurrencyStatus( - currency = currency, - maybeQuote = maybeQuote, - maybeNetworkStatus = maybeNetworkStatus, - maybeYieldBalance = maybeYieldBalance, - ) + return if (subscribeOnYieldBalance) { + combine(quoteFlow, statusFlow, yieldBalanceFlow) { maybeQuote, maybeNetworkStatus, maybeYieldBalance -> + currencyStatusProxyCreator.createCurrencyStatus( + currency = currency, + maybeQuote = maybeQuote, + maybeNetworkStatus = maybeNetworkStatus, + maybeYieldBalance = maybeYieldBalance, + ) + } + } else { + combine(quoteFlow, statusFlow) { maybeQuote, maybeNetworkStatus -> + currencyStatusProxyCreator.createCurrencyStatus( + currency = currency, + maybeQuote = maybeQuote, + maybeNetworkStatus = maybeNetworkStatus, + maybeYieldBalance = null, + ) + } } } @@ -242,7 +254,13 @@ abstract class BaseCurrencyStatusOperations( recover = { return flowOf(it.left()) }, ) - return getCurrencyStatusFlow(userWalletId, currency, includeQuotes) + return getCurrencyStatusFlow( + userWalletId = userWalletId, + currency = currency, + includeQuotes = includeQuotes, + // If toggle is off, then subscribe on yield balance. If toggle is on, then don't + subscribeOnYieldBalance = !tokensFeatureToggles.isStakingLoadingRefactoringEnabled, + ) } suspend fun getCurrenciesStatusesSync(userWalletId: UserWalletId): Either> { From ed25735b31576d48bea55f6ad40da9f1fb273e05 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 19:50:53 +0500 Subject: [PATCH 129/151] Updated on 2026-08-14 --- .../SendConfirmAlertFactory.kt | 2 +- .../analytics/CommonSendAnalyticEvents.kt | 109 ++++++++++++++++++ .../send/v2/send/DefaultSendComponent.kt | 35 ++++-- .../v2/send/analytics/SendAnalyticEvents.kt | 67 +---------- .../v2/send/confirm/model/SendConfirmModel.kt | 42 +++++-- ...endConfirmationNotificationsTransformer.kt | 10 +- .../features/send/v2/send/model/SendModel.kt | 2 +- .../SendDestinationComponentParams.kt | 2 + .../destination/model/SendDestinationModel.kt | 9 +- 9 files changed, 184 insertions(+), 94 deletions(-) rename features/send-v2/impl/src/main/java/com/tangem/features/send/v2/{send/confirm/model => common}/SendConfirmAlertFactory.kt (97%) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt similarity index 97% rename from features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmAlertFactory.kt rename to features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt index 7eb09a280d..147ce18f67 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.features.send.v2.send.confirm.model +package com.tangem.features.send.v2.common import com.tangem.common.ui.alerts.TransactionErrorAlertConverter import com.tangem.common.ui.alerts.models.AlertDemoModeUM diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt new file mode 100644 index 0000000000..a392ca4da3 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/analytics/CommonSendAnalyticEvents.kt @@ -0,0 +1,109 @@ +package com.tangem.features.send.v2.common.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +/** + * Send screen analytics + */ +internal sealed class CommonSendAnalyticEvents( + category: String, + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = category, event = event, params = params) { + + /** Recipient address screen opened */ + data class AddressScreenOpened( + val categoryName: String, + ) : CommonSendAnalyticEvents(category = categoryName, event = "Address Screen Opened") + + /** Amount screen opened */ + data class AmountScreenOpened( + val categoryName: String, + ) : CommonSendAnalyticEvents(category = categoryName, event = "Amount Screen Opened") + + /** Fee screen opened */ + data class FeeScreenOpened( + val categoryName: String, + ) : CommonSendAnalyticEvents(category = categoryName, event = "Fee Screen Opened") + + /** Confirmation screen opened */ + data class ConfirmationScreenOpened( + val categoryName: String, + ) : CommonSendAnalyticEvents(category = categoryName, event = "Confirm Screen Opened") + + /** If transaction delays notification is present */ + data class NoticeTransactionDelays( + val categoryName: String, + val token: String, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Notice - Transaction Delays Are Possible", + params = mapOf(TOKEN_PARAM to token), + ) + + /** If error occurs during send transactions */ + data class TransactionError( + val categoryName: String, + val token: String, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Error - Transaction Rejected", + params = mapOf(TOKEN_PARAM to token), + ) + + /** Close button clicked */ + data class CloseButtonClicked( + val categoryName: String, + val source: SendScreenSource, + val isFromSummary: Boolean, + val isValid: Boolean, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Button - Close", + params = mapOf( + SOURCE to source.name, + "FromSummary" to if (isFromSummary) "Yes" else "No", + "isValid" to if (isValid) "Yes" else "No", + ), + ) + + /** Share button clicked */ + data class ShareButtonClicked( + val categoryName: String, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Button - Share", + ) + + /** Expore button clicked */ + data class ExploreButtonClicked( + val categoryName: String, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Button - Explore", + ) + + /** Screen reopened from confirmation screen */ + data class ScreenReopened( + val categoryName: String, + val source: SendScreenSource, + ) : CommonSendAnalyticEvents( + category = categoryName, + event = "Screen Reopened", + params = mapOf(SOURCE to source.name), + ) + + companion object { + const val SEND_CATEGORY = "Token / Send" + const val NFT_SEND_CATEGORY = "NFT" + } + + internal enum class SendScreenSource { + Address, + Amount, + Fee, + Confirm, + } +} \ No newline at end of file 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 d94ae93e49..af8eb70adc 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 @@ -18,13 +18,15 @@ 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.core.ui.extensions.resourceReference 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.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource 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.impl.R 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 @@ -48,6 +50,7 @@ internal class DefaultSendComponent @AssistedInject constructor( ) : SendComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() + private val analyticCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY private val innerRouter = InnerRouter( stackNavigation = stackNavigation, @@ -88,19 +91,27 @@ internal class DefaultSendComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is SendConfirmComponent -> if (currentRoute.value.isEditMode) { - analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ConfirmationScreenOpened(categoryName = analyticCategoryName), + ) activeComponent.updateState(model.uiState.value) } is SendAmountComponent -> { - analyticsEventHandler.send(SendAnalyticEvents.AmountScreenOpened) + analyticsEventHandler.send( + CommonSendAnalyticEvents.AmountScreenOpened(categoryName = analyticCategoryName), + ) activeComponent.updateState(model.uiState.value.amountUM) } is SendDestinationComponent -> { - analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened) + analyticsEventHandler.send( + CommonSendAnalyticEvents.AddressScreenOpened(categoryName = analyticCategoryName), + ) activeComponent.updateState(model.uiState.value.destinationUM) } is SendFeeComponent -> { - analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened) + analyticsEventHandler.send( + CommonSendAnalyticEvents.FeeScreenOpened(categoryName = analyticCategoryName), + ) } } currentRoute.emit(stack.active.configuration) @@ -135,7 +146,8 @@ internal class DefaultSendComponent @AssistedInject constructor( state = model.uiState.value.destinationUM, currentRoute = currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + analyticsCategoryName = analyticCategoryName, + title = resourceReference(R.string.send_recipient_label), userWalletId = params.userWalletId, cryptoCurrency = params.currency, callback = model, @@ -156,7 +168,7 @@ internal class DefaultSendComponent @AssistedInject constructor( state = model.uiState.value.amountUM, currentRoute = currentRoute.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + analyticsCategoryName = analyticCategoryName, userWallet = model.userWallet, appCurrency = model.appCurrency, cryptoCurrencyStatus = model.cryptoCurrencyStatus, @@ -167,7 +179,8 @@ internal class DefaultSendComponent @AssistedInject constructor( onChildBack() } else { analyticsEventHandler.send( - SendAnalyticEvents.CloseButtonClicked( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = analyticCategoryName, source = SendScreenSource.Amount, isFromSummary = false, isValid = model.uiState.value.amountUM.isPrimaryButtonEnabled, @@ -196,7 +209,7 @@ internal class DefaultSendComponent @AssistedInject constructor( params = SendFeeComponentParams.FeeParams( state = model.uiState.value.feeUM, currentRoute = currentRoute.filterIsInstance(), - analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + analyticsCategoryName = analyticCategoryName, userWallet = model.userWallet, cryptoCurrencyStatus = model.cryptoCurrencyStatus, feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, @@ -235,7 +248,7 @@ internal class DefaultSendComponent @AssistedInject constructor( userWallet = model.userWallet, currentRoute = currentRoute, isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = SendAnalyticEvents.SEND_CATEGORY, + analyticsCategoryName = analyticCategoryName, cryptoCurrencyStatus = model.cryptoCurrencyStatus, feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, appCurrency = model.appCurrency, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt index aca97719f1..90e33cdec8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/analytics/SendAnalyticEvents.kt @@ -3,8 +3,8 @@ package com.tangem.features.send.v2.send.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE -import com.tangem.core.analytics.models.AnalyticsParam.Key.SOURCE import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents /** * Send screen analytics @@ -12,19 +12,7 @@ import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM internal sealed class SendAnalyticEvents( event: String, params: Map = mapOf(), -) : AnalyticsEvent(category = SEND_CATEGORY, event = event, params = params) { - - /** Recipient address screen opened */ - data object AddressScreenOpened : SendAnalyticEvents(event = "Address Screen Opened") - - /** Amount screen opened */ - data object AmountScreenOpened : SendAnalyticEvents(event = "Amount Screen Opened") - - /** Fee screen opened */ - data object FeeScreenOpened : SendAnalyticEvents(event = "Fee Screen Opened") - - /** Confirmation screen opened */ - data object ConfirmationScreenOpened : SendAnalyticEvents(event = "Confirm Screen Opened") +) : AnalyticsEvent(category = CommonSendAnalyticEvents.SEND_CATEGORY, event = event, params = params) { /** Transaction send screen opened */ data class TransactionScreenOpened( @@ -37,55 +25,4 @@ internal sealed class SendAnalyticEvents( FEE_TYPE to feeType.value, ), ) - - /** If transaction delays notification is present */ - data class NoticeTransactionDelays( - val token: String, - ) : SendAnalyticEvents( - event = "Notice - Transaction Delays Are Possible", - params = mapOf(TOKEN_PARAM to token), - ) - - /** If error occurs during send transactions */ - data class TransactionError(val token: String) : SendAnalyticEvents( - event = "Error - Transaction Rejected", - params = mapOf(TOKEN_PARAM to token), - ) - - /** Close button clicked */ - data class CloseButtonClicked( - val source: SendScreenSource, - val isFromSummary: Boolean, - val isValid: Boolean, - ) : SendAnalyticEvents( - event = "Button - Close", - params = mapOf( - SOURCE to source.name, - "FromSummary" to if (isFromSummary) "Yes" else "No", - "isValid" to if (isValid) "Yes" else "No", - ), - ) - - /** Share button clicked */ - data object ShareButtonClicked : SendAnalyticEvents(event = "Button - Share") - - /** Expore button clicked */ - data object ExploreButtonClicked : SendAnalyticEvents(event = "Button - Explore") - - /** Screen reopened from confirmation screen */ - data class ScreenReopened(val source: SendScreenSource) : SendAnalyticEvents( - event = "Screen Reopened", - params = mapOf(SOURCE to source.name), - ) - - companion object { - const val SEND_CATEGORY = "Token / Send" - } - - internal enum class SendScreenSource { - Address, - Amount, - Fee, - Confirm, - } } \ 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 0cdba71372..51a5163719 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,11 +32,12 @@ 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.SendConfirmAlertFactory +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource 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.analytics.SendAnalyticHelper import com.tangem.features.send.v2.send.confirm.SendConfirmComponent import com.tangem.features.send.v2.send.confirm.model.transformers.SendConfirmInitialStateTransformer @@ -93,6 +94,7 @@ internal class SendConfirmModel @Inject constructor( private val params: SendConfirmComponent.Params = paramsContainer.require() + private val analyticsCategoryName = params.analyticsCategoryName private val userWallet = params.userWallet private val appCurrency = params.appCurrency private val cryptoCurrencyStatus = params.cryptoCurrencyStatus @@ -165,7 +167,12 @@ internal class SendConfirmModel @Inject constructor( val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) } - analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = analyticsCategoryName, + source = SendScreenSource.Address, + ), + ) router.push(CommonSendRoute.Destination(isEditMode = true)) } } @@ -177,7 +184,12 @@ internal class SendConfirmModel @Inject constructor( val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) } - analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Amount)) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = analyticsCategoryName, + source = SendScreenSource.Amount, + ), + ) router.push(CommonSendRoute.Amount(isEditMode = true)) } } @@ -189,7 +201,12 @@ internal class SendConfirmModel @Inject constructor( val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) } - analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = analyticsCategoryName, + source = SendScreenSource.Fee, + ), + ) router.push(CommonSendRoute.Fee) } } @@ -207,13 +224,13 @@ internal class SendConfirmModel @Inject constructor( override fun onExploreClick() { val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return - analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) + analyticsEventHandler.send(CommonSendAnalyticEvents.ExploreButtonClicked(analyticsCategoryName)) urlOpener.openUrl(confirmUM.txUrl) } override fun onShareClick() { val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return - analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) + analyticsEventHandler.send(CommonSendAnalyticEvents.ShareButtonClicked(analyticsCategoryName)) shareManager.shareText(confirmUM.txUrl) } @@ -347,7 +364,12 @@ internal class SendConfirmModel @Inject constructor( popBack = appRouter::pop, onFailedTxEmailClick = ::onFailedTxEmailClick, ) - analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) + analyticsEventHandler.send( + CommonSendAnalyticEvents.TransactionError( + categoryName = analyticsCategoryName, + token = cryptoCurrency.symbol, + ), + ) }, ifRight = { updateTransactionStatus(txData) @@ -419,6 +441,7 @@ internal class SendConfirmModel @Inject constructor( analyticsEventHandler = analyticsEventHandler, cryptoCurrency = cryptoCurrencyStatus.currency, appCurrency = appCurrency, + analyticsCategoryName = params.analyticsCategoryName, ).transform(uiState.value.confirmUM), ) } @@ -445,7 +468,8 @@ internal class SendConfirmModel @Inject constructor( backIconRes = R.drawable.ic_close_24, backIconClick = { analyticsEventHandler.send( - SendAnalyticEvents.CloseButtonClicked( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = analyticsCategoryName, source = SendScreenSource.Confirm, isFromSummary = true, isValid = confirmUM.isPrimaryButtonEnabled, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt index 853e5bcdb5..dc73d36994 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformer.kt @@ -14,9 +14,9 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency -import com.tangem.features.send.v2.impl.R -import com.tangem.features.send.v2.send.analytics.SendAnalyticEvents +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.common.ui.state.ConfirmUM +import com.tangem.features.send.v2.impl.R 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 @@ -30,6 +30,7 @@ internal class SendConfirmationNotificationsTransformer( private val analyticsEventHandler: AnalyticsEventHandler, private val cryptoCurrency: CryptoCurrency, private val appCurrency: AppCurrency, + private val analyticsCategoryName: String, ) : Transformer { override fun transform(prevState: ConfirmUM): ConfirmUM { val state = prevState as? ConfirmUM.Content ?: return prevState @@ -52,7 +53,10 @@ internal class SendConfirmationNotificationsTransformer( if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) { add(NotificationUM.Warning.FeeTooLow) analyticsEventHandler.send( - SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol), + CommonSendAnalyticEvents.NoticeTransactionDelays( + categoryName = analyticsCategoryName, + token = cryptoCurrency.symbol, + ), ) } } 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 c153cbe90e..ea1ca3146c 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -37,7 +37,7 @@ 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.confirm.SendConfirmComponent -import com.tangem.features.send.v2.send.confirm.model.SendConfirmAlertFactory +import com.tangem.features.send.v2.common.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.amount.SendAmountUpdateQRTrigger 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 ecd05eba6d..f22aacd83c 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 @@ -1,5 +1,6 @@ package com.tangem.features.send.v2.subcomponents.destination +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.features.send.v2.common.CommonSendRoute @@ -21,6 +22,7 @@ internal sealed class SendDestinationComponentParams { override val analyticsCategoryName: String, override val cryptoCurrency: CryptoCurrency, override val userWalletId: UserWalletId, + val title: TextReference, val isBalanceHidingFlow: StateFlow, val currentRoute: Flow, val callback: ModelCallback, 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 b2e71ae80a..16dbe408d7 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,10 +22,10 @@ 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.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource 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.subcomponents.destination.SendDestinationComponentParams import com.tangem.features.send.v2.subcomponents.destination.SendDestinationComponentParams.DestinationBlockParams @@ -289,7 +289,7 @@ internal class SendDestinationModel @Inject constructor( ).onEach { (state, route) -> params.callback.onNavigationResult( NavigationUM.Content( - title = resourceReference(R.string.send_recipient_label), + title = params.title, subtitle = null, backIconRes = if (route.isEditMode) { R.drawable.ic_back_24 @@ -299,7 +299,8 @@ internal class SendDestinationModel @Inject constructor( backIconClick = { if (!route.isEditMode) { analyticsEventHandler.send( - SendAnalyticEvents.CloseButtonClicked( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = params.analyticsCategoryName, source = SendScreenSource.Address, isFromSummary = false, isValid = state.isPrimaryButtonEnabled, From 5a5cfc0facf83355624cd155ffb60e21ae567b95 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 25 Apr 2025 19:51:03 +0500 Subject: [PATCH 130/151] Updated on 2026-08-14 --- .../core/analytics/models/AnalyticsParam.kt | 6 ++ core/res/src/main/res/values-de/strings.xml | 2 +- core/res/src/main/res/values-es/strings.xml | 2 +- core/res/src/main/res/values-fr/strings.xml | 2 +- core/res/src/main/res/values-it/strings.xml | 6 -- core/res/src/main/res/values-ja/strings.xml | 8 +- core/res/src/main/res/values-ru/strings.xml | 3 + .../src/main/res/values-uk-rUA/strings.xml | 2 +- .../src/main/res/values-zh-rTW/strings.xml | 3 - core/res/src/main/res/values/strings.xml | 4 + .../v2/sendnft/DefaultNFTSendComponent.kt | 25 ++++-- .../analytics/NFTSendAnalyticEvents.kt | 28 ++++++ .../analytics/NFTSendAnalyticHelper.kt | 53 +++++++++++ .../confirm/model/NFTSendConfirmModel.kt | 89 +++++++++++++++---- ...endConfirmationNotificationsTransformer.kt | 8 +- .../send/v2/sendnft/model/NFTSendModel.kt | 38 +++++++- 16 files changed, 232 insertions(+), 47 deletions(-) create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt create mode 100644 features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt index 31e79f809c..26da2b1161 100644 --- a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/AnalyticsParam.kt @@ -113,6 +113,12 @@ sealed class AnalyticsParam { data object WalletConnect : TxSentFrom("WalletConnect") data object Sell : TxSentFrom("Sell") + + data class NFT( + override val blockchain: String, + override val token: String, + override val feeType: FeeType, + ) : TxSentFrom("NFT"), TxData } sealed interface TxData { diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 67aa4da4df..cecd9a6bbb 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -379,7 +379,7 @@ Du hast deine biometrischen Daten aktualisiert, scanne deine Karte oder Ring, um einzutreten Dein Guthaben sollte höher sein als der Gebührenwert, um eine Überweisung zu tätigen Unzureichende Mittel - Du hast nicht genug Mana für diese Transaktion. Bitte warte, bis das Mana wieder aufgefüllt ist. Dein Mana-Guthaben beträgt %1$s/%2$s + Du hast nicht genug Mana für diese Transaktion. Bitte warte, bis das Mana wieder aufgefüllt ist. Dein Managuthaben beträgt %1$s/%2$s Nicht genug Mana Du kannst nur %s übertragen, da das Koinos-Netzwerk ein Mana-Limit vorgibt. Mana-Limit diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 8d8cd1ccfe..128949fd13 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -375,7 +375,7 @@ Ha actualizado la biometría, escanee su tarjeta o anillo para entrar Su saldo debe ser mayor que el valor de la tarifa para hacer una transferencia Saldo insuficiente - No tiene suficiente Mana para esta transacción. Por favor, espere hasta que el Mana se recargue. Su saldo de Mana es %1$s/%2$s + No tiene suficiente Mana para esta transacción. Espere hasta que se reponga el maná. Su saldo de maná es %1$s/%2$s Mana insuficiente Solo puede transferir %s debido al límite de Mana impuesto por la red Koinos Límite de Mana diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index e7209c6305..9d695df7c1 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -375,7 +375,7 @@ Vous avez mis à jour vos données biométriques, scannez votre carte ou bague pour entrer Votre solde doit être supérieur à la valeur des frais pour effectuer un transfert Solde insuffisant - Vous n\'avez pas assez de Mana pour cette transaction. Veuillez attendre que le Mana soit reconstitué. Votre solde de Mana est de %1$s/%2$s + Vous n\'avez pas assez de mana pour cette transaction. Veuillez patienter jusqu\'à ce que le mana soit réapprovisionné. Votre solde de mana est %1$s/%2$s Mana insuffisant Vous ne pouvez transférer que %s en raison de la limite de Mana imposée par le réseau Koinos Limite de Mana diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml index 001ae1f84d..7132029cfe 100644 --- a/core/res/src/main/res/values-it/strings.xml +++ b/core/res/src/main/res/values-it/strings.xml @@ -19,7 +19,6 @@ Invia Impossibile inviare la transazione Con successo - Due to %1$s limitations only %2$d UTXOs can fit in a single transaction. This means you can only send %3$s or less. You need to reduce the amount. Codice di accesso Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto Mantenimento della carta @@ -36,7 +35,6 @@ Termini del servizio Not enough funds for the transaction. Please top up your account. An error occurred - An error occurred. Code: %s. Notifica richiesta Scansiona carta Avvicina per modificare il codice di accesso @@ -44,9 +42,6 @@ Avvicina per scansionare Avvicina per firmare Avvicina la carta - You don\'t have enough Mana for this transaction. Please wait until the Mana is refilled. Your Mana balance is %1$s/%2$s - To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely - Destination account is not active. Send %s or more to activate the account. Per creare un account, invia fondi a questo indirizzo The destination account does not have a trustline for the asset being sent. Crea portafoglio @@ -55,7 +50,6 @@ L\'importo minimo è di %s Il resto minimo è di %s Commissione non valida - Minimum balance is %s Il conto totale non è stato creato L\'importo da inviare deve essere %s + commissione o maggiore Errore sconosciuto Memo diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index ec498830f5..e41ea53407 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -377,7 +377,7 @@ 生体認証を更新しました。カードまたはリングをスキャンして入ってください。 送金するには、残高が手数料額より高くなければなりません。 残高不足 - この取引に必要なManaが不足しています。Manaが補充されるまでお待ちください。あなたのMana残高は%1$s/%2$sです。 + この取引に必要なManaが足りません。Manaが補充されるまでお待ちください。Manaの残高は%1$s / %2$sです。 Manaが不足しています KoinosネットワークによるMana制限のため、%s のみ送金できます。 Mana上限 @@ -548,6 +548,8 @@ 送信先アカウントが有効ではありません。%s 以上を送信してアカウントを有効にしてください。 アカウントを作成するには、このアドレスに資金を送金してください 送信先アカウントには、送金されるアセットのトラストラインがありません。 + コードを共有すると、販売ごとに5 USDTを獲得できます。お友達は10%割引になります。 + 友達への紹介で報酬を獲得しよう! すべてのデバイスを保護するには、単一のアクセスコードを設定してください。 保護する 後で各カードおよびリングに個別のアクセスコードを設定できます。 @@ -892,9 +894,9 @@ 再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。 残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。 TONでステーキングを開始するには、まず任意の金額の送金を行ってください。これにより、ウォレットがアクティブになります。 - TONをステーキングまたは出金する場合、入金と出金の両方でステーキング額から0.2 TONの手数料が差し引かれます。 + ネットワーク手数料に加え、取引の全ステップを完了するには最大0.2 TONが必要です。未使用分は取引完了後に返金されます。 この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。 - ステーキング手数料 + 0.2 TON保留済み このアクションは、ネットワークのルールに従って、他のポジションをクローズするか、引き出しステータスに切り替えます。 ポジション状況 資金をステーキングから引き出すには、ロックを解除してください。ロック解除には%sかかります。 diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 401b2fea2a..c599f5bead 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -525,6 +525,9 @@ Аккаунт получателя не активирован. Отправьте %s или более для активации аккаунта. Для создания аккаунта отправьте средства на этот адрес Аккаунт получателя не содержит трастлайна для отправляемого актива. + Присоединиться + Поделись промокодом — заработай 5 USDT с каждой покупки. Твои друзья получат скидку 10% на карту Tangem! + Получай бонусы за каждого друга! Установите единый код доступа для защиты всех ваших карт или колец Защита Установите индивидуальный код доступа для каждой карты или кольца позже. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 1a942a7a26..130fc7559d 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -381,7 +381,7 @@ Ви оновили дані біометрії, відскануйте свою картку або кільце для входу Ваш баланс повинен перевищувати суму комісії для здійснення переказу Недостатньо балансу - У вас недостатньо Mana для цієї транзакції. Будь ласка, зачекайте, поки Mana буде поповнена. Ваш баланс Mana становить %1$s/%2$s + У вас недостатньо Мани для цієї транзакції. Будь ласка, зачекайте, поки Мана поповниться. Ваш баланс мани дорівнює %1$s/%2$s Недостатньо Mana Ви можете переказати тільки %s через обмеження Mana, встановлене мережею Koinos Ліміт Mana diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 78849a3497..e1f37d9840 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -140,7 +140,6 @@ Tangem反饋 無法發送交易 An error occurred - An error occurred. Code: %s. 需要memo 數量 %s 要繼續,您需要允許 %1s 智能合約使用您的 %2s @@ -153,12 +152,10 @@ 點擊簽名 點按卡片 您已更新生物識別登入,掃描您的卡進入 - You don\'t have enough Mana for this transaction. Please wait until the Mana is refilled. Your Mana balance is %1$s/%2$s 管理代幣 要訪問所有的網路您需要掃描卡片 掃描卡片 代幣 - To use the %1$s network, you must pay the account reserve (%2$s %3$s), which locks up and hides that amount indefinitely 目標帳戶未激活。發送 %s 或更多以激活帳戶 要創建帳戶,請將資金發送到此地址 目標帳戶沒有用於發送資產的 trustline diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 90a9ac6c99..9e12a259c0 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -546,6 +546,7 @@ You haven\'t added this network yet. To start receiving NFTs, add it to the main screen! Network not added Not Added + Send NFT Traits %1$d NFTs in %2$d collection Tap here to receive first NFT @@ -555,6 +556,9 @@ Destination account is not active. Send %s or more to activate the account. To create account send funds to this address The destination account does not have a trustline for the asset being sent. + Join Now + Share your code - earn 5 USDT per sale. Your friend gets 10% OFF. + Get REWARDS for every friend! 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/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 4b65d6d5ae..7fc4f402c2 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 @@ -11,16 +11,20 @@ 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.analytics.api.AnalyticsEventHandler 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.core.ui.extensions.resourceReference 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.analytics.CommonSendAnalyticEvents 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.impl.R 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 @@ -40,9 +44,11 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: NFTSendComponent.Params, private val nftDetailsBlockComponentFactory: NFTDetailsBlockComponent.Factory, + private val analyticsEventHandler: AnalyticsEventHandler, ) : NFTSendComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() + private val analyticsCategoryName = CommonSendAnalyticEvents.NFT_SEND_CATEGORY private val innerRouter = InnerRouter( stackNavigation = stackNavigation, @@ -79,15 +85,21 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( componentScope.launch { when (val activeComponent = stack.active.instance) { is NFTSendConfirmComponent -> if (currentRouteFlow.value.isEditMode) { - // analyticsEventHandler.send(SendAnalyticEvents.ConfirmationScreenOpened) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ConfirmationScreenOpened(categoryName = analyticsCategoryName), + ) activeComponent.updateState(model.uiState.value) } is SendDestinationComponent -> { - // analyticsEventHandler.send(SendAnalyticEvents.AddressScreenOpened) + analyticsEventHandler.send( + CommonSendAnalyticEvents.AddressScreenOpened(categoryName = analyticsCategoryName), + ) activeComponent.updateState(model.uiState.value.destinationUM) } is SendFeeComponent -> { - // analyticsEventHandler.send(SendAnalyticEvents.FeeScreenOpened) + analyticsEventHandler.send( + CommonSendAnalyticEvents.FeeScreenOpened(categoryName = analyticsCategoryName), + ) } } currentRouteFlow.emit(stack.active.configuration) @@ -121,7 +133,8 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( state = model.uiState.value.destinationUM, currentRoute = currentRouteFlow.filterIsInstance(), isBalanceHidingFlow = model.isBalanceHiddenFlow, - analyticsCategoryName = "", // todo + title = resourceReference(R.string.nft_send), + analyticsCategoryName = analyticsCategoryName, userWalletId = params.userWalletId, cryptoCurrency = model.cryptoCurrency, callback = model, @@ -145,7 +158,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( params = SendFeeComponentParams.FeeParams( state = model.uiState.value.feeUM, currentRoute = currentRouteFlow.filterIsInstance(), - analyticsCategoryName = "", // todo + analyticsCategoryName = analyticsCategoryName, userWallet = model.userWallet, cryptoCurrencyStatus = model.cryptoCurrencyStatus, feeCryptoCurrencyStatus = model.feeCryptoCurrencyStatus, @@ -167,7 +180,7 @@ internal class DefaultNFTSendComponent @AssistedInject constructor( nftDetailsBlockComponentFactory = nftDetailsBlockComponentFactory, params = NFTSendConfirmComponent.Params( state = model.uiState.value, - analyticsCategoryName = "", // todo + analyticsCategoryName = analyticsCategoryName, userWallet = model.userWallet, nftAsset = params.nftAsset, nftCollectionName = params.nftCollectionName, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt new file mode 100644 index 0000000000..ec215b0478 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticEvents.kt @@ -0,0 +1,28 @@ +package com.tangem.features.send.v2.sendnft.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.AnalyticsParam.Key.FEE_TYPE +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents + +/** + * Send screen analytics + */ +internal sealed class NFTSendAnalyticEvents( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent(category = CommonSendAnalyticEvents.NFT_SEND_CATEGORY, event = event, params = params) { + + /** Transaction send screen opened */ + data class TransactionScreenOpened( + val token: String, + val feeType: AnalyticsParam.FeeType, + ) : NFTSendAnalyticEvents( + event = "NFT Sent Screen Opened", + params = mapOf( + TOKEN_PARAM to token, + FEE_TYPE to feeType.value, + ), + ) +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt new file mode 100644 index 0000000000..da36ab5006 --- /dev/null +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/analytics/NFTSendAnalyticHelper.kt @@ -0,0 +1,53 @@ +package com.tangem.features.send.v2.sendnft.analytics + +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationTextFieldUM +import com.tangem.features.send.v2.subcomponents.destination.ui.state.DestinationUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeSelectorUM +import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM +import javax.inject.Inject + +@ModelScoped +internal class NFTSendAnalyticHelper @Inject constructor( + private val analyticsEventHandler: AnalyticsEventHandler, +) { + + fun nftSendSuccessAnalytics(cryptoCurrency: CryptoCurrency, nftSendUM: NFTSendUM) { + val destinationUM = nftSendUM.destinationUM as? DestinationUM.Content + val feeUM = nftSendUM.feeUM as? FeeUM.Content + val feeSelectorUM = feeUM?.feeSelectorUM as? FeeSelectorUM.Content ?: return + val feeType = feeSelectorUM.selectedType.toAnalyticType(feeSelectorUM) + analyticsEventHandler.send( + NFTSendAnalyticEvents.TransactionScreenOpened( + token = cryptoCurrency.symbol, + feeType = feeType, + ), + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = AnalyticsParam.TxSentFrom.NFT( + blockchain = cryptoCurrency.network.name, + token = cryptoCurrency.symbol, + feeType = feeType, + ), + memoType = getSendTransactionMemoType(destinationUM?.memoTextField), + ), + ) + } + + private fun getSendTransactionMemoType( + recipientMemo: DestinationTextFieldUM.RecipientMemo?, + ): Basic.TransactionSent.MemoType { + val memo = recipientMemo?.value + return when { + memo?.isBlank() == true -> Basic.TransactionSent.MemoType.Empty + memo?.isNotBlank() == true -> Basic.TransactionSent.MemoType.Full + else -> Basic.TransactionSent.MemoType.Null + } + } +} \ 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 index 19424360b9..0e89448a8f 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 @@ -14,6 +14,11 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase @@ -21,12 +26,14 @@ import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.SendBalanceUpdater +import com.tangem.features.send.v2.common.SendConfirmAlertFactory +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.common.analytics.CommonSendAnalyticEvents.SendScreenSource 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.analytics.NFTSendAnalyticHelper 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 @@ -40,6 +47,7 @@ import com.tangem.features.send.v2.subcomponents.fee.ui.state.FeeUM import com.tangem.features.send.v2.subcomponents.notifications.NotificationsUpdateTrigger import com.tangem.features.send.v2.subcomponents.notifications.model.NotificationData import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -47,7 +55,7 @@ import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class NFTSendConfirmModel @Inject constructor( paramsContainer: ParamsContainer, @@ -59,17 +67,23 @@ internal class NFTSendConfirmModel @Inject constructor( private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val notificationsUpdateTrigger: NotificationsUpdateTrigger, private val sendFeeCheckReloadTrigger: SendFeeCheckReloadTrigger, private val sendFeeCheckReloadListener: SendFeeCheckReloadListener, + private val alertFactory: SendConfirmAlertFactory, private val urlOpener: UrlOpener, private val shareManager: ShareManager, private val analyticsEventHandler: AnalyticsEventHandler, + private val nftSendAnalyticHelper: NFTSendAnalyticHelper, sendBalanceUpdaterFactory: SendBalanceUpdater.Factory, ) : Model(), NFTSendConfirmClickIntents { private val params: NFTSendConfirmComponent.Params = paramsContainer.require() + private val analyticsCategoryName = params.analyticsCategoryName private val userWallet = params.userWallet private val cryptoCurrencyStatus = params.cryptoCurrencyStatus private val cryptoCurrency = cryptoCurrencyStatus.currency @@ -129,7 +143,12 @@ internal class NFTSendConfirmModel @Inject constructor( val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) } - // analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Address)) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = analyticsCategoryName, + source = SendScreenSource.Address, + ), + ) router.push(CommonSendRoute.Destination(isEditMode = true)) } } @@ -141,7 +160,12 @@ internal class NFTSendConfirmModel @Inject constructor( val confirmUM = it.confirmUM as? ConfirmUM.Content it.copy(confirmUM = confirmUM?.copy(showTapHelp = false) ?: it.confirmUM) } - // analyticsEventHandler.send(SendAnalyticEvents.ScreenReopened(SendScreenSource.Fee)) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ScreenReopened( + categoryName = analyticsCategoryName, + source = SendScreenSource.Fee, + ), + ) router.push(CommonSendRoute.Fee) } } @@ -159,18 +183,38 @@ internal class NFTSendConfirmModel @Inject constructor( override fun onExploreClick() { val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return - // analyticsEventHandler.send(SendAnalyticEvents.ExploreButtonClicked) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ExploreButtonClicked(analyticsCategoryName), + ) urlOpener.openUrl(confirmUM.txUrl) } override fun onShareClick() { val confirmUM = uiState.value.confirmUM as? ConfirmUM.Success ?: return - // analyticsEventHandler.send(SendAnalyticEvents.ShareButtonClicked) + analyticsEventHandler.send( + CommonSendAnalyticEvents.ShareButtonClicked(analyticsCategoryName), + ) shareManager.shareText(confirmUM.txUrl) } override fun onFailedTxEmailClick(errorMessage: String) { - // TODO() + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage, + blockchainId = cryptoCurrency.network.id.value, + derivationPath = cryptoCurrency.network.derivationPath.value, + destinationAddress = confirmData.enteredDestination.orEmpty(), + tokenSymbol = null, + amount = params.nftAsset.amount.toString(), + fee = confirmData.fee?.amount?.value?.stripZeroPlainString(), + ), + ) + + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return + + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + } } private fun initialState() { @@ -229,9 +273,9 @@ internal class NFTSendConfirmModel @Inject constructor( ifLeft = { error -> Timber.e(error) _uiState.update(NFTSendConfirmSendingStateTransformer(isSending = false)) - // alertFactory.getGenericErrorState { - // onFailedTxEmailClick(error.localizedMessage.orEmpty()) - // } + alertFactory.getGenericErrorState { + onFailedTxEmailClick(error.localizedMessage.orEmpty()) + } }, ifRight = { txData -> sendTransaction(txData) @@ -252,17 +296,22 @@ internal class NFTSendConfirmModel @Inject constructor( result.fold( ifLeft = { error -> Timber.e(error.toString()) - // alertFactory.getSendTransactionErrorState( - // error = error, - // popBack = appRouter::pop, - // onFailedTxEmailClick = ::onFailedTxEmailClick, - // ) - // analyticsEventHandler.send(SendAnalyticEvents.TransactionError(cryptoCurrency.symbol)) + alertFactory.getSendTransactionErrorState( + error = error, + popBack = appRouter::pop, + onFailedTxEmailClick = ::onFailedTxEmailClick, + ) + analyticsEventHandler.send( + CommonSendAnalyticEvents.TransactionError( + categoryName = analyticsCategoryName, + token = cryptoCurrency.symbol, + ), + ) }, ifRight = { updateTransactionStatus(txData) sendBalanceUpdater.scheduleUpdates() - // sendAnalyticHelper.sendSuccessAnalytics(cryptoCurrency, uiState.value) + nftSendAnalyticHelper.nftSendSuccessAnalytics(cryptoCurrency, uiState.value) }, ) } @@ -307,6 +356,7 @@ internal class NFTSendConfirmModel @Inject constructor( feeUM = uiState.value.feeUM, analyticsEventHandler = analyticsEventHandler, cryptoCurrency = cryptoCurrencyStatus.currency, + analyticsCategoryName = analyticsCategoryName, ).transform(uiState.value.confirmUM), ) } @@ -331,7 +381,8 @@ internal class NFTSendConfirmModel @Inject constructor( backIconRes = R.drawable.ic_close_24, backIconClick = { analyticsEventHandler.send( - SendAnalyticEvents.CloseButtonClicked( + CommonSendAnalyticEvents.CloseButtonClicked( + categoryName = analyticsCategoryName, source = SendScreenSource.Confirm, isFromSummary = true, isValid = confirmUM.isPrimaryButtonEnabled, 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 index af93239501..f5b2ba2744 100644 --- 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 @@ -5,8 +5,8 @@ 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.analytics.CommonSendAnalyticEvents 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 @@ -18,6 +18,7 @@ internal class NFTSendConfirmationNotificationsTransformer( private val feeUM: FeeUM, private val analyticsEventHandler: AnalyticsEventHandler, private val cryptoCurrency: CryptoCurrency, + private val analyticsCategoryName: String, ) : Transformer { override fun transform(prevState: ConfirmUM): ConfirmUM { val state = prevState as? ConfirmUM.Content ?: return prevState @@ -39,7 +40,10 @@ internal class NFTSendConfirmationNotificationsTransformer( if (feeSelectorUM.selectedType == FeeType.Custom && minimumValue > customValue) { add(NotificationUM.Warning.FeeTooLow) analyticsEventHandler.send( - SendAnalyticEvents.NoticeTransactionDelays(cryptoCurrency.symbol), + CommonSendAnalyticEvents.NoticeTransactionDelays( + categoryName = analyticsCategoryName, + token = cryptoCurrency.symbol, + ), ) } } 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 71fb3be509..8146fab729 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 @@ -13,6 +13,11 @@ import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.feedback.GetCardInfoUseCase +import com.tangem.domain.feedback.SaveBlockchainErrorUseCase +import com.tangem.domain.feedback.SendFeedbackEmailUseCase +import com.tangem.domain.feedback.models.BlockchainErrorInfo +import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.tokens.GetCryptoCurrenciesUseCase import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase @@ -25,6 +30,7 @@ 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.SendConfirmAlertFactory 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.sendnft.confirm.NFTSendConfirmComponent @@ -58,6 +64,10 @@ internal class NFTSendModel @Inject constructor( private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, + private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, + private val getCardInfoUseCase: GetCardInfoUseCase, + private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val alertFactory: SendConfirmAlertFactory, ) : Model(), SendNFTComponentCallback { val params: NFTSendComponent.Params = paramsContainer.require() @@ -145,13 +155,33 @@ internal class NFTSendModel @Inject constructor( ) }, ifLeft = { - // sendConfirmAlertFactory.getGenericErrorState(::onFailedTxEmailClick) + alertFactory.getGenericErrorState(::onFailedTxEmailClick) return@launch }, ) } } + private fun onFailedTxEmailClick(errorMessage: String? = null) { + saveBlockchainErrorUseCase( + error = BlockchainErrorInfo( + errorMessage = errorMessage.orEmpty(), + blockchainId = cryptoCurrency.network.id.value, + derivationPath = cryptoCurrency.network.derivationPath.value, + destinationAddress = "", + tokenSymbol = null, + amount = "", + fee = "", + ), + ) + + val cardInfo = getCardInfoUseCase(userWallet.scanResponse).getOrNull() ?: return + + modelScope.launch { + sendFeedbackEmailUseCase(type = FeedbackEmailType.TransactionSendingProblem(cardInfo = cardInfo)) + } + } + private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) { getCurrencyStatusUpdatesUseCase( userWalletId = userWalletId, @@ -171,9 +201,9 @@ internal class NFTSendModel @Inject constructor( } }, ifLeft = { - // sendConfirmAlertFactory.getGenericErrorState { - // onFailedTxEmailClick(it.toString()) - // } + alertFactory.getGenericErrorState { + onFailedTxEmailClick(it.toString()) + } }, ) }.launchIn(modelScope) From d65b4039daf7987430e74e963265c2145ab6c755 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 09:56:00 +0300 Subject: [PATCH 131/151] Updated on 2026-08-14 --- .../java/com/tangem/tap/DecomposeFragment.kt | 65 ---- .../main/java/com/tangem/tap/MainActivity.kt | 201 +--------- .../tap/common/extensions/Navigation.kt | 60 --- .../com/tangem/tap/common/extensions/Store.kt | 12 +- .../com/tangem/tap/common/redux/AppState.kt | 1 - .../common/redux/NotificationsMiddleware.kt | 141 ------- .../tap/common/redux/global/GlobalAction.kt | 7 - .../com/tangem/tap/routing/ProxyAppRouter.kt | 7 +- .../tap/routing/component/RoutingComponent.kt | 9 - .../component/impl/DefaultRoutingComponent.kt | 37 +- .../routing/toggle/RoutingFeatureToggles.kt | 14 - .../tangem/tap/routing/utils/ChildFactory.kt | 364 +----------------- .../tangem/tap/routing/utils/RouteMappers.kt | 34 -- app/src/main/res/layout/activity_main.xml | 15 - .../configs/feature_toggles_config.json | 4 - 15 files changed, 26 insertions(+), 945 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/DecomposeFragment.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/routing/toggle/RoutingFeatureToggles.kt delete mode 100644 app/src/main/res/layout/activity_main.xml diff --git a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt b/app/src/main/java/com/tangem/tap/DecomposeFragment.kt deleted file mode 100644 index 3136ae77dd..0000000000 --- a/app/src/main/java/com/tangem/tap/DecomposeFragment.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.tangem.tap - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.screen.ComposeFragment -import com.tangem.utils.Provider -import dagger.hilt.android.AndroidEntryPoint -import java.util.WeakHashMap -import javax.inject.Inject - -@AndroidEntryPoint -internal class DecomposeFragment : ComposeFragment() { - - @Inject - override lateinit var uiDependencies: UiDependencies - - private val component by lazy(LazyThreadSafetyMode.NONE) { - val tag = requireArguments().getString(TAG_KEY) - val builder = componentsBuilders[tag] - - requireNotNull(builder?.build()) { - "Component builder is not set, call newInstance() for DecomposeFragment creation first." - } - } - - @Composable - override fun ScreenContent(modifier: Modifier) { - component.Content(modifier) - } - - private class ComponentBuilder>( - private val contextProvider: Provider, - private val params: P, - private val componentFactory: F, - ) { - - fun build(): C = componentFactory.create(contextProvider(), params) - } - - companion object { - - private const val TAG_KEY = "tag" - - private val componentsBuilders = WeakHashMap>() - - fun > newInstance( - tag: String, - contextProvider: Provider, - params: P, - componentFactory: F, - ): Fragment { - this@Companion.componentsBuilders[tag] = ComponentBuilder(contextProvider, params, componentFactory) - - return DecomposeFragment().apply { - this.arguments = bundleOf(TAG_KEY to tag) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 081728b21c..66df0f2d73 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -12,7 +12,6 @@ import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels -import androidx.annotation.StringRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate.setDefaultNightMode @@ -20,19 +19,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen -import androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import arrow.core.getOrElse -import by.kirich1409.viewbindingdelegate.viewBinding -import com.arkivanov.decompose.value.subscribe -import com.arkivanov.essenty.lifecycle.asEssentyLifecycle -import com.google.android.material.snackbar.BaseTransientBottomBar -import com.google.android.material.snackbar.Snackbar import com.tangem.common.routing.AppRoute import com.tangem.common.routing.entity.SerializableIntent import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -41,12 +32,6 @@ import com.tangem.core.decompose.di.RootAppComponentContext import com.tangem.core.deeplink.DeepLinksRegistry import com.tangem.core.navigation.email.EmailSender import com.tangem.core.ui.UiDependencies -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.message.EventMessageEffect -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.core.ui.res.TangemColorPalette -import com.tangem.core.ui.res.TangemTheme import com.tangem.data.balancehiding.DefaultDeviceFlipDetector import com.tangem.data.card.sdk.CardSdkOwner import com.tangem.domain.apptheme.model.AppThemeMode @@ -70,11 +55,8 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.ActivityResultCallbackHolder import com.tangem.tap.common.DialogManager import com.tangem.tap.common.OnActivityResultCallback -import com.tangem.tap.common.SnackbarHandler import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.showFragmentAllowingStateLoss -import com.tangem.tap.common.redux.NotificationsHandler import com.tangem.tap.domain.walletconnect2.domain.WalletConnectInteractor import com.tangem.tap.features.intentHandler.IntentProcessor import com.tangem.tap.features.intentHandler.handlers.BackgroundScanIntentHandler @@ -85,11 +67,8 @@ import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphAction import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.configurator.AppRouterConfig -import com.tangem.tap.routing.toggle.RoutingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.FeatureCoroutineExceptionHandler -import com.tangem.wallet.R -import com.tangem.wallet.databinding.ActivityMainBinding import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.* import kotlinx.coroutines.flow.* @@ -102,7 +81,6 @@ lateinit var tangemSdkManager: TangemSdkManager lateinit var backupService: BackupService internal var lockUserWalletsTimer: LockUserWalletsTimer? = null private set -var notificationsHandler: NotificationsHandler? = null private val coroutineContext: CoroutineContext get() = SupervisorJob() + Dispatchers.IO + FeatureCoroutineExceptionHandler.create("scope") @@ -114,7 +92,7 @@ val mainScope = CoroutineScope(mainCoroutineContext) @Suppress("LargeClass") @AndroidEntryPoint -class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbackHolder { +class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject lateinit var appStateHolder: AppStateHolder @@ -187,9 +165,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac @Inject lateinit var dispatchers: CoroutineDispatcherProvider - @Inject - internal lateinit var routingFeatureToggles: RoutingFeatureToggles - @Inject internal lateinit var uiDependencies: UiDependencies @@ -203,9 +178,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac // TODO: fixme: inject through DI private val intentProcessor: IntentProcessor = IntentProcessor() - private var snackbar: Snackbar? = null private val dialogManager = DialogManager() - private val binding: ActivityMainBinding by viewBinding(ActivityMainBinding::bind) private val onActivityResultCallbacks = mutableListOf() @@ -242,13 +215,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac installActivityDependencies() observeAppThemeModeUpdates() - if (routingFeatureToggles.isNavigationRefactoringEnabled) { - setRootContent() - } else { - setContentView(R.layout.activity_main) - installRouting() - installEventMessageEffect() - } + setRootContent() initContent() @@ -265,33 +232,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac lifecycle.addObserver(defaultDeviceFlipDetector) } - private fun installEventMessageEffect() { - binding.composeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) - binding.composeView.setContent { - TangemTheme( - activity = this, - uiDependencies = uiDependencies, - overrideSystemBarColors = false, - ) { - EventMessageEffect( - onShowSnackbar = { message, _ -> - showSnackbar( - text = message.message.resolveReference(resources), - length = when (message.duration) { - SnackbarMessage.Duration.Short -> Snackbar.LENGTH_SHORT - SnackbarMessage.Duration.Long -> Snackbar.LENGTH_LONG - SnackbarMessage.Duration.Indefinite -> Snackbar.LENGTH_INDEFINITE - }, - buttonTitle = message.actionLabel?.resolveReference(resources), - action = message.action, - onDismiss = message.onDismissRequest, - ) - }, - ) - } - } - } - private fun setRootContent() { // for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy // doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it @@ -306,54 +246,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - private fun installRouting() { - // for now activity is singleTop and after going to ChromeCustomTab it calls onCreate but onDestroy - // doesn't calls. It lead to issue that decompose nav stack is not saved in bundle and to restore it - // we try to init component with previous stack - val routingComponent = routingComponentFactory.create( - context = rootComponentContext, - initialStack = appRouterConfig.stack, - ) - - appRouterConfig.routerScope = mainScope - appRouterConfig.componentRouter = routingComponent.router - appRouterConfig.snackbarHandler = this - - routingComponent.stack.subscribe(lifecycle.asEssentyLifecycle()) { childStack -> - val stack = childStack.backStack - .plus(childStack.active) - .map { it.configuration } - - if (stack == appRouterConfig.stack) return@subscribe - - appRouterConfig.stack = stack - - if (stack.size == 1) { - supportFragmentManager.run { - val activeChildName = stack.first().path - - (0 until backStackEntryCount) - .mapNotNull { getBackStackEntryAt(it).name } - .filter { it != activeChildName } - .forEach { - popBackStackImmediate(it, POP_BACK_STACK_INCLUSIVE) - } - } - } - - when (val child = childStack.active.instance) { - is RoutingComponent.Child.Initial -> Unit - is RoutingComponent.Child.LegacyFragment -> { - supportFragmentManager.showFragmentAllowingStateLoss(child.name, child.fragmentProvider) - } - is RoutingComponent.Child.LegacyIntent -> { - startActivity(child.intent) - } - is RoutingComponent.Child.ComposableComponent -> error("Unsupported child: $child") - } - } - } - private fun installActivityDependencies() { cardSdkOwner.register(activity = this) tangemSdkManager = injectedTangemSdkManager @@ -429,17 +321,10 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onResume() { super.onResume() - - if (!routingFeatureToggles.isNavigationRefactoringEnabled) { - // TODO: RESEARCH! NotificationsHandler is created in onResume and destroyed in onStop - notificationsHandler = NotificationsHandler(binding.fragmentContainer) - } - navigateToInitialScreenIfNeeded(intent) } override fun onStop() { - notificationsHandler = null dialogManager.onStop() super.onStop() } @@ -472,31 +357,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac MutableAppThemeModeHolder.isDarkThemeActive = isDarkTheme() } - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - - /* - * We need to manually change the background color of the activity when the UI mode changes to prevent - * flickering when navigating between fragments. - * - - * `android:configChanges="uiMode"` is set in the manifest. - * */ - if (routingFeatureToggles.isNavigationRefactoringEnabled.not()) { - updateAppBackground() - } - } - - private fun updateAppBackground() { - val backgroundColor = if (isDarkTheme()) { - TangemColorPalette.Dark6 - } else { - TangemColorPalette.White - } - - findViewById(R.id.fragment_container).setBackgroundColor(backgroundColor.toArgb()) - } - private fun isDarkTheme(): Boolean { return when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) { Configuration.UI_MODE_NIGHT_YES -> true @@ -518,29 +378,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac } } - override fun showSnackbar(@StringRes text: Int, length: Int, @StringRes buttonTitle: Int?, action: (() -> Unit)?) { - showSnackbar( - getString(text), - length, - buttonTitle?.let(::getString), - action = { action?.invoke() }, - ) - } - - override fun showSnackbar(text: TextReference, length: Int, buttonTitle: TextReference?, action: (() -> Unit)?) { - showSnackbar( - text = text.resolveReference(resources), - length = length, - buttonTitle = buttonTitle?.resolveReference(resources), - action = { action?.invoke() }, - ) - } - - override fun dismissSnackbar() { - snackbar?.dismiss() - snackbar = null - } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) @@ -568,40 +405,6 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac return if (result) super.dispatchTouchEvent(event) else false } - private fun showSnackbar( - text: String, - length: Int, - buttonTitle: String?, - action: (() -> Unit)? = null, - onDismiss: () -> Unit = {}, - ) { - if (snackbar != null) return - - snackbar = Snackbar.make(binding.fragmentContainer, text, length).apply { - val textColor = getColor(R.color.text_primary_2) - - setBackgroundTint(getColor(R.color.button_primary)) - setActionTextColor(textColor) - setTextColor(textColor) - - if (buttonTitle != null && action != null) { - setAction(buttonTitle, { action() }) - } - - addCallback( - object : BaseTransientBottomBar.BaseCallback() { - override fun onDismissed(transientBottomBar: Snackbar?, event: Int) { - onDismiss() - snackbar = null - removeCallback(this) - } - }, - ) - } - - snackbar?.show() - } - private fun navigateToInitialScreenIfNeeded(intentWhichStartedActivity: Intent?) { val backStack = appRouterConfig.stack ?: emptyList() // TODO move inital navigation to navigation component ([REDACTED_JIRA]) diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt b/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt deleted file mode 100644 index 243a2f8770..0000000000 --- a/app/src/main/java/com/tangem/tap/common/extensions/Navigation.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.tap.common.extensions - -import androidx.fragment.app.DialogFragment -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager -import com.tangem.utils.Provider -import com.tangem.wallet.R -import timber.log.Timber - -fun FragmentManager.showFragmentAllowingStateLoss(name: String, fragmentProvider: Provider) { - if (backStackEntryCount > 0) { - val currentFragmentName = getBackStackEntryAt(backStackEntryCount - 1).name - - if (name == currentFragmentName) { - Timber.i("Fragment $name is already at the top of the stack") - return - } - } - - Timber.i("Showing $name route") - - val isPoppedBack = popBackStackImmediate(name, 0) - - if (!isPoppedBack) { - val fragment = fragmentProvider() - - if (fragment is DialogFragment) { - fragment.showDialog(fragmentManager = this, name) - } else { - fragment.showFragment(fragmentManager = this, name) - } - - Timber.i("Route $name is shown") - } else { - Timber.i("Route $name is found in backstack and shown") - } -} - -private fun DialogFragment.showDialog(fragmentManager: FragmentManager, name: String) { - val transaction = fragmentManager.beginTransaction() - - try { - transaction.addToBackStack(name) - show(transaction, name) - } catch (e: IllegalStateException) { - transaction.add(this, name) - transaction.addToBackStack(name) - - transaction.commitAllowingStateLoss() - } -} - -private fun Fragment.showFragment(fragmentManager: FragmentManager, name: String) { - val transaction = fragmentManager.beginTransaction() - - transaction.replace(R.id.fragment_container, this, name) - transaction.addToBackStack(name) - - transaction.commitAllowingStateLoss() -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt index 919c21972d..c865434607 100644 --- a/app/src/main/java/com/tangem/tap/common/extensions/Store.kt +++ b/app/src/main/java/com/tangem/tap/common/extensions/Store.kt @@ -1,6 +1,8 @@ package com.tangem.tap.common.extensions import com.tangem.common.routing.AppRouter +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.common.extensions.withMainContext import com.tangem.domain.redux.StateDialog import com.tangem.domain.wallets.models.UserWallet @@ -43,20 +45,16 @@ suspend fun Store.onUserWalletSelected(userWallet: UserWallet) { state.globalState.tapWalletManager.onWalletSelected(userWallet) } -fun Store<*>.dispatchErrorNotification(error: TapError) { - dispatchOnMain(GlobalAction.ShowErrorNotification(error)) -} - /** * @param fatal used to indicate errors that should not normally occur */ -fun Store<*>.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) { +fun Store.dispatchDebugErrorNotification(message: String, fatal: Boolean = false) { val prefix = if (fatal) "FATAL ERROR: " else "DEBUG ERROR: " dispatchDebugErrorNotification(TapError.CustomError("$prefix $message")) } -fun Store<*>.dispatchDebugErrorNotification(error: TapError) { - dispatchOnMain(GlobalAction.DebugShowErrorNotification(error)) +fun Store.dispatchDebugErrorNotification(error: TapError) { + inject(DaggerGraphState::uiMessageSender).send(SnackbarMessage(stringReference(error.message ?: "debug error"))) } fun Store<*>.dispatchDialogShow(dialog: StateDialog) { diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index e1814f72c0..3df1fae79b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -31,7 +31,6 @@ data class AppState( fun getMiddleware(): List> { return listOf( logMiddleware, - notificationsMiddleware, GlobalMiddleware.handler, HomeMiddleware.handler, DetailsMiddleware().detailsMiddleware, diff --git a/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt deleted file mode 100644 index 1b862e7ed4..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/NotificationsMiddleware.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.tangem.tap.common.redux - -import android.content.Context -import android.widget.Toast -import androidx.coordinatorlayout.widget.CoordinatorLayout -import com.google.android.material.snackbar.Snackbar -import com.tangem.tap.common.extensions.getColor -import com.tangem.tap.domain.ArgError -import com.tangem.tap.domain.MultiMessageError -import com.tangem.tap.domain.TapError -import com.tangem.tap.domain.assembleErrors -import com.tangem.tap.notificationsHandler -import com.tangem.wallet.BuildConfig -import com.tangem.wallet.R -import org.rekotlin.Action -import org.rekotlin.Middleware -import java.lang.ref.WeakReference - -class NotificationsHandler(coordinatorLayout: CoordinatorLayout) { - private val basicCoordinatorLayout = WeakReference(coordinatorLayout) - private var baseLayout = basicCoordinatorLayout - - private fun showNotification(message: String) { - baseLayout.get()?.let { layout -> - Snackbar.make(layout, message, Snackbar.LENGTH_LONG) - .also { snackbar -> snackbar.show() } - } - } - - private fun showDebugNotification(message: String) { - baseLayout.get()?.let { layout -> - Snackbar.make(layout, message, Snackbar.LENGTH_LONG) - .also { snackbar -> - snackbar.setBackgroundTint(layout.getColor(R.color.warning_warning)) - snackbar.show() - } - } - } - - fun showNotification(message: Int, args: List? = null) { - baseLayout.get()?.let { - showNotification(getMessageString(it.context, message, args)) - } - } - - fun showToastNotification(message: Int, args: List? = null) { - baseLayout.get()?.let { - Toast.makeText(it.context, getMessageString(it.context, message, args), Toast.LENGTH_LONG).show() - } - } - - fun showNotification(errorList: List?>>, builder: (List) -> String) { - val context = baseLayout.get()?.context ?: return - - val message = builder(errorList.map { getMessageString(context, it.first, it.second) }) - showNotification(message) - } - - fun showDebugErrorNotification(message: Int, args: List? = null) { - baseLayout.get()?.let { - showDebugNotification(getMessageString(it.context, message, args)) - } - } -} - -fun getMessageString(context: Context, message: Int, args: List?): String { - return if (args.isNullOrEmpty()) { - context.getString(message) - } else { - context.getString(message, *args.toTypedArray()) - } -} - -val notificationsMiddleware: Middleware = { dispatch, state -> - { next -> - { action -> - handleNotificationAction(action) - next(action) - } - } -} - -@Suppress("ComplexMethod") -private fun handleNotificationAction(action: Action) { - if (action is Debug && !BuildConfig.DEBUG) return - - when (action) { - is NotificationAction -> { - notificationsHandler?.showNotification(action.messageResource) - } - is ToastNotificationAction -> notificationsHandler?.showToastNotification(action.messageResource) - is ErrorAction -> { - when (action) { - is Debug -> { - val args = (action.error as? ArgError)?.args ?: listOf() - when (action) { - is DebugNotification -> { - notificationsHandler?.showNotification(action.error.messageResource, args) - } - is DebugToastNotification -> { - notificationsHandler?.showToastNotification(action.error.messageResource, args) - } - is DebugErrorAction -> { - notificationsHandler?.showDebugErrorNotification(action.error.messageResource, args) - } - } - } - else -> { - when (action.error) { - is MultiMessageError -> { - val multiError = action.error as MultiMessageError - notificationsHandler?.showNotification(multiError.assembleErrors(), multiError.builder) - } - else -> { - val args = (action.error as? ArgError)?.args ?: listOf() - notificationsHandler?.showNotification(action.error.messageResource, args) - } - } - } - } - } - } -} - -interface ToastNotificationAction : Action { - val messageResource: Int -} - -interface NotificationAction : Action { - val messageResource: Int -} - -interface ErrorAction : Action { - val error: TapError -} - -// Processed only in the debug builds -interface Debug -interface DebugNotification : Debug, NotificationAction -interface DebugToastNotification : Debug, ToastNotificationAction -interface DebugErrorAction : Debug, ErrorAction \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index 79947dc0ac..0a43a25887 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -5,17 +5,10 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.redux.StateDialog -import com.tangem.tap.common.redux.DebugErrorAction -import com.tangem.tap.common.redux.ErrorAction -import com.tangem.tap.domain.TapError import org.rekotlin.Action sealed class GlobalAction : Action { - // notifications - data class ShowErrorNotification(override val error: TapError) : GlobalAction(), ErrorAction - data class DebugShowErrorNotification(override val error: TapError) : GlobalAction(), DebugErrorAction - // dialogs data class ShowDialog(val stateDialog: StateDialog) : GlobalAction() object HideDialog : GlobalAction() diff --git a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt index bcebe2fe83..19f2b9b8a7 100644 --- a/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt +++ b/app/src/main/java/com/tangem/tap/routing/ProxyAppRouter.kt @@ -42,7 +42,11 @@ internal class ProxyAppRouter( override fun replaceAll(vararg routes: AppRoute, onComplete: (isSuccess: Boolean) -> Unit) { safeNavigate(onComplete, message = "Replace all routes with $routes") { - innerRouter.replaceAll(*routes, onComplete = onComplete) + runCatching { + innerRouter.replaceAll(*routes, onComplete = onComplete) + }.getOrElse { + Timber.tag("ASDASD").e(it) + } } } @@ -78,6 +82,7 @@ internal class ProxyAppRouter( override fun defaultCompletionHandler(isSuccess: Boolean, errorMessage: String) { if (!isSuccess) { + Timber.tag("ASDASD").d(errorMessage) analyticsExceptionHandler.sendException(ExceptionAnalyticsEvent(RuntimeException(errorMessage))) Timber.w(errorMessage) diff --git a/app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt index 9709e18553..6cd367b582 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/RoutingComponent.kt @@ -3,22 +3,13 @@ package com.tangem.tap.routing.component import android.content.Intent import androidx.compose.runtime.Immutable import androidx.fragment.app.Fragment -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.value.Value import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.utils.Provider internal interface RoutingComponent : ComposableContentComponent { - // TODO: Remove after full navigation refactoring - val router: Router - - // TODO: Remove after full navigation refactoring - val stack: Value> - @Immutable sealed class Child { diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 759951e25e..fe42e73ef0 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -1,14 +1,11 @@ package com.tangem.tap.routing.component.impl import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.router.stack.ChildStack import com.arkivanov.decompose.router.stack.childStack import com.arkivanov.decompose.value.Value import com.arkivanov.decompose.value.subscribe -import com.arkivanov.essenty.lifecycle.doOnDestroy import com.google.android.material.snackbar.Snackbar import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext @@ -23,7 +20,6 @@ import com.tangem.tap.routing.RootContent import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.component.RoutingComponent.Child import com.tangem.tap.routing.configurator.AppRouterConfig -import com.tangem.tap.routing.toggle.RoutingFeatureToggles import com.tangem.tap.routing.utils.ChildFactory import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -36,35 +32,30 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val childFactory: ChildFactory, private val appRouterConfig: AppRouterConfig, private val uiDependencies: UiDependencies, - routingFeatureToggles: RoutingFeatureToggles, ) : RoutingComponent, AppComponentContext by context, SnackbarHandler { - override val stack: Value> = childStack( + private val stack: Value> = childStack( source = navigationProvider.getOrCreateTyped(), initialStack = { getInitialStackOrInit() }, serializer = null, // AppRoute.serializer(), // Disabled until Nav refactoring completes - handleBackButton = routingFeatureToggles.isNavigationRefactoringEnabled, - childFactory = ::child, + handleBackButton = true, + childFactory = { route, childContext -> + childFactory.createChild(route, childByContext(childContext)) + }, ) init { - lifecycle.doOnDestroy { - childFactory.doOnDestroy() - } + appRouterConfig.routerScope = componentScope + appRouterConfig.componentRouter = router + appRouterConfig.snackbarHandler = this - if (routingFeatureToggles.isNavigationRefactoringEnabled) { - appRouterConfig.routerScope = componentScope - appRouterConfig.componentRouter = router - appRouterConfig.snackbarHandler = this + stack.subscribe(lifecycle) { stack -> + val stackItems = stack.items.map { it.configuration } - stack.subscribe(lifecycle) { stack -> - val stackItems = stack.items.map { it.configuration } - - if (appRouterConfig.stack != stackItems) { - appRouterConfig.stack = stackItems - } + if (appRouterConfig.stack != stackItems) { + appRouterConfig.stack = stackItems } } } @@ -114,10 +105,6 @@ internal class DefaultRoutingComponent @AssistedInject constructor( initialStack } - private fun child(route: AppRoute, context: ComponentContext): Child { - return childFactory.createChild(route) { childByContext(context) } - } - @AssistedFactory interface Factory : RoutingComponent.Factory { override fun create(context: AppComponentContext, initialStack: List?): DefaultRoutingComponent diff --git a/app/src/main/java/com/tangem/tap/routing/toggle/RoutingFeatureToggles.kt b/app/src/main/java/com/tangem/tap/routing/toggle/RoutingFeatureToggles.kt deleted file mode 100644 index 6554684732..0000000000 --- a/app/src/main/java/com/tangem/tap/routing/toggle/RoutingFeatureToggles.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.tap.routing.toggle - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -internal class RoutingFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) { - - val isNavigationRefactoringEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "NAVIGATION_REFACTORING") -} \ No newline at end of file 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 f5feb80a77..0f2b7d5303 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 @@ -35,10 +35,7 @@ import com.tangem.tap.features.details.ui.walletconnect.api.WalletConnectCompone import com.tangem.tap.features.home.api.HomeComponent import com.tangem.tap.features.welcome.component.WelcomeComponent import com.tangem.tap.routing.component.RoutingComponent.Child -import com.tangem.tap.routing.toggle.RoutingFeatureToggles -import com.tangem.utils.Provider import dagger.hilt.android.scopes.ActivityScoped -import java.util.WeakHashMap import javax.inject.Inject import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent @@ -80,20 +77,11 @@ internal class ChildFactory @Inject constructor( private val nftComponentFactory: NFTComponent.Factory, private val nftSendComponentFactory: NFTSendComponent.Factory, private val testerRouter: TesterRouter, - private val routingFeatureToggles: RoutingFeatureToggles, private val walletConnectFeatureToggles: WalletConnectFeatureToggles, ) { - fun createChild(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { - return if (routingFeatureToggles.isNavigationRefactoringEnabled) { - createChildNew(route, contextFactory(route)) - } else { - createChildLegacy(route, contextFactory) - } - } - @Suppress("LongMethod", "CyclomaticComplexMethod") - private fun createChildNew(route: AppRoute, context: AppComponentContext): Child { + fun createChild(route: AppRoute, context: AppComponentContext): Child { return when (route) { is AppRoute.Initial -> { Child.Initial @@ -419,355 +407,5 @@ internal class ChildFactory @Inject constructor( ) } } - // endregion - } - - @Suppress("LongMethod", "CyclomaticComplexMethod") - private fun createChildLegacy(route: AppRoute, contextFactory: (route: AppRoute) -> AppComponentContext): Child { - componentContexts[route] = contextFactory(route) - - // region Child creation - return when (route) { - is AppRoute.Initial -> { - Child.Initial - } - is AppRoute.AccessCodeRecovery -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = accessCodeRecoveryComponentFactory, - ) - } - is AppRoute.AppCurrencySelector -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = appCurrencySelectorComponentFactory, - ) - } - is AppRoute.Send -> { - if (sendFeatureToggles.isSendV2Enabled) { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = com.tangem.features.send.v2.api.SendComponent.Params( - userWalletId = route.userWalletId, - currency = route.currency, - transactionId = route.transactionId, - amount = route.amount, - tag = route.tag, - destinationAddress = route.destinationAddress, - ), - componentFactory = sendComponentFactoryV2, - ) - } else { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SendComponent.Params( - userWalletId = route.userWalletId, - currency = route.currency, - transactionId = route.transactionId, - amount = route.amount, - tag = route.tag, - destinationAddress = route.destinationAddress, - ), - componentFactory = sendComponentFactory, - ) - } - } - is AppRoute.AppSettings -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = appSettingsComponentFactory, - ) - } - is AppRoute.CardSettings -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = CardSettingsComponent.Params(userWalletId = route.userWalletId), - componentFactory = cardSettingsComponentFactory, - ) - } - is AppRoute.Details -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = DetailsComponent.Params(route.userWalletId), - componentFactory = detailsComponentFactory, - ) - } - is AppRoute.DetailsSecurity -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SecurityModeComponent.Params( - userWalletId = route.userWalletId, - ), - componentFactory = securityModeComponentFactory, - ) - } - is AppRoute.Disclaimer -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = DisclaimerComponent.Params(route.isTosAccepted), - componentFactory = disclaimerComponentFactory, - ) - } - is AppRoute.Home -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = homeComponentFactory, - ) - } - is AppRoute.ManageTokens -> { - val source = when (route.source) { - AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS - AppRoute.ManageTokens.Source.ONBOARDING -> ManageTokensSource.ONBOARDING - AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES - } - - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = ManageTokensComponent.Params(route.userWalletId, source), - componentFactory = manageTokensComponentFactory, - ) - } - is AppRoute.QrScanning -> { - val source = when (route.source) { - is AppRoute.QrScanning.Source.Send -> SourceType.SEND - is AppRoute.QrScanning.Source.WalletConnect -> SourceType.WALLET_CONNECT - } - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = QrScanningComponent.Params( - source = source, - networkName = (route.source as? AppRoute.QrScanning.Source.Send)?.networkName, - ), - componentFactory = qrScanningComponentFactory, - ) - } - is AppRoute.ReferralProgram -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = ReferralComponent.Params(route.userWalletId), - componentFactory = referralComponentFactory, - ) - } - is AppRoute.ResetToFactory -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = ResetCardComponent.Params( - userWalletId = route.userWalletId, - cardId = route.cardId, - isActiveBackupStatus = route.isActiveBackupStatus, - backupCardsCount = route.backupCardsCount, - ), - componentFactory = resetCardComponentFactory, - ) - } - is AppRoute.Swap -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SwapComponent.Params( - currencyFrom = route.currencyFrom, - currencyTo = route.currencyTo, - userWalletId = route.userWalletId, - isInitialReverseOrder = route.isInitialReverseOrder, - screenSource = route.screenSource, - ), - componentFactory = swapComponentFactory, - ) - } - is AppRoute.Wallet -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = walletComponentFactory, - ) - } - is AppRoute.WalletConnectSessions -> { - if (walletConnectFeatureToggles.isRedesignedWalletConnectEnabled) { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = redesignedWalletConnectComponentFactory, - ) - } else { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = walletConnectComponentFactory, - ) - } - } - is AppRoute.CurrencyDetails -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = TokenDetailsComponent.Params( - userWalletId = route.userWalletId, - currency = route.currency, - ), - componentFactory = tokenDetailsComponentFactory, - ) - } - is AppRoute.Welcome -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = WelcomeComponent.Params( - intent = route.intent, - ), - componentFactory = welcomeComponentFactory, - ) - } - is AppRoute.TesterMenu -> { - Child.LegacyIntent(testerRouter.getEntryIntent()) - } - is AppRoute.Staking -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = StakingComponent.Params( - userWalletId = route.userWalletId, - cryptoCurrencyId = route.cryptoCurrencyId, - yieldId = route.yieldId, - ), - componentFactory = stakingComponentFactory, - ) - } - is AppRoute.PushNotification -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = Unit, - componentFactory = pushNotificationsComponentFactory, - ) - } - is AppRoute.WalletSettings -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = WalletSettingsComponent.Params(route.userWalletId), - componentFactory = walletSettingsComponentFactory, - ) - } - is AppRoute.MarketsTokenDetails -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = MarketsTokenDetailsComponent.Params( - token = route.token, - appCurrency = route.appCurrency, - showPortfolio = route.showPortfolio, - analyticsParams = route.analyticsParams?.let { - MarketsTokenDetailsComponent.AnalyticsParams( - blockchain = it.blockchain, - source = it.source, - ) - }, - ), - componentFactory = marketsTokenDetailsComponentFactory, - ) - } - is AppRoute.Onramp -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = OnrampComponent.Params( - userWalletId = route.userWalletId, - cryptoCurrency = route.currency, - source = route.source, - ), - componentFactory = onrampComponentFactory, - ) - } - is AppRoute.OnrampSuccess -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = OnrampSuccessComponent.Params(route.txId), - componentFactory = onrampSuccessComponentFactory, - ) - } - is AppRoute.BuyCrypto -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = BuyCryptoComponent.Params(userWalletId = route.userWalletId), - componentFactory = buyCryptoComponentFactory, - ) - } - is AppRoute.SellCrypto -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SellCryptoComponent.Params(userWalletId = route.userWalletId), - componentFactory = sellCryptoComponentFactory, - ) - } - is AppRoute.SwapCrypto -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = SwapSelectTokensComponent.Params(userWalletId = route.userWalletId), - componentFactory = swapSelectTokensComponentFactory, - ) - } - is AppRoute.Onboarding -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = OnboardingEntryComponent.Params( - scanResponse = route.scanResponse, - mode = when (route.mode) { - AppRoute.Onboarding.Mode.Onboarding -> OnboardingEntryComponent.Mode.Onboarding - AppRoute.Onboarding.Mode.AddBackupWallet1 -> OnboardingEntryComponent.Mode.AddBackupWallet1 - AppRoute.Onboarding.Mode.WelcomeOnlyTwin -> OnboardingEntryComponent.Mode.WelcomeOnlyTwin - AppRoute.Onboarding.Mode.RecreateWalletTwin -> - OnboardingEntryComponent.Mode.RecreateWalletTwin - AppRoute.Onboarding.Mode.ContinueFinalize -> - OnboardingEntryComponent.Mode.ContinueFinalize - }, - ), - componentFactory = onboardingEntryComponentFactory, - ) - } - is AppRoute.Stories -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = StoriesComponent.Params( - storyId = route.storyId, - nextScreen = route.nextScreen, - screenSource = route.screenSource, - ), - componentFactory = storiesComponentFactory, - ) - } - is AppRoute.NFT -> - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = NFTComponent.Params( - userWalletId = route.userWalletId, - walletName = route.walletName, - ), - componentFactory = nftComponentFactory, - ) - is AppRoute.NFTSend -> { - route.asComponentChild( - contextProvider = contextProvider(route, contextFactory), - params = NFTSendComponent.Params( - userWalletId = route.userWalletId, - nftAsset = route.nftAsset, - nftCollectionName = route.nftCollectionName, - ), - componentFactory = nftSendComponentFactory, - ) - } - } - // endregion - } - - fun doOnDestroy() { - componentContexts.clear() - } - - private fun contextProvider( - appRoute: AppRoute, - contextFactory: (route: AppRoute) -> AppComponentContext, - ): Provider = Provider { - componentContexts.getOrPut(appRoute) { contextFactory(appRoute) } - } - - private companion object { - - val componentContexts = WeakHashMap() } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt index f20d2b096b..6a9fac9177 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/RouteMappers.kt @@ -1,43 +1,9 @@ package com.tangem.tap.routing.utils -import androidx.fragment.app.Fragment -import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.tap.DecomposeFragment import com.tangem.tap.routing.component.RoutingComponent.Child -import com.tangem.utils.Provider - -internal fun AppRoute.asFragmentChild(fragmentProvider: Provider): Child { - val provider = Provider { - val bundle = (this as? RouteBundleParams)?.getBundle() - - fragmentProvider().apply { - arguments = bundle - } - } - - return Child.LegacyFragment(path, provider) -} - -internal fun > AppRoute.asComponentChild( - contextProvider: Provider, - params: P, - componentFactory: F, -): Child { - val fragmentProvider = Provider { - DecomposeFragment.newInstance( - tag = path, - contextProvider = contextProvider, - params = params, - componentFactory = componentFactory, - ) - } - - return Child.LegacyFragment(path, fragmentProvider) -} internal fun > createComponentChild( context: AppComponentContext, diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index dac7964d66..0000000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ 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 5077c92b05..239af3a94b 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 @@ -7,10 +7,6 @@ "name": "ONRAMP_ENABLED", "version": "undefined" }, - { - "name": "NAVIGATION_REFACTORING", - "version": "5.23.0" - }, { "name": "VISA_ONBOARDING_ENABLED", "version": "undefined" From f49f013aa2b731406de22b0b93f2ecaab8375b7e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 09:56:19 +0300 Subject: [PATCH 132/151] Updated on 2026-08-14 --- .../data/quotes/store/DefaultQuotesStoreV2.kt | 18 ++++++++++++++---- .../store/QuotesStoreUpdateMethodsTest.kt | 4 +--- 2 files changed, 15 insertions(+), 7 deletions(-) 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 9fbbe97e50..1b4c48a05a 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 @@ -64,13 +64,23 @@ internal class DefaultQuotesStoreV2( } override suspend fun storeError(currenciesIds: Set) { - updateStatusSourceInRuntime(currenciesIds = currenciesIds, source = StatusSource.ONLY_CACHE) + updateStatusSourceInRuntime( + currenciesIds = currenciesIds, + ifNotFound = Quote::Empty, + source = StatusSource.ONLY_CACHE, + ) } - private suspend fun updateStatusSourceInRuntime(currenciesIds: Set, source: StatusSource) { + private suspend fun updateStatusSourceInRuntime( + currenciesIds: Set, + ifNotFound: (CryptoCurrency.RawID) -> Quote? = { null }, + source: StatusSource, + ) { runtimeStore.update(default = emptySet()) { stored -> - val updatedQuotes = currenciesIds.mapTo(hashSetOf()) { id -> - val quote = stored.firstOrNull { it.rawCurrencyId == id } ?: Quote.Empty(id) + val updatedQuotes = currenciesIds.mapNotNullTo(hashSetOf()) { id -> + val quote = stored.firstOrNull { it.rawCurrencyId == id } + ?: ifNotFound(id) + ?: return@mapNotNullTo null quote.copySealed(source = source) } diff --git a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt index 407b640fd9..b0624da6a3 100644 --- a/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt +++ b/data/quotes/src/test/java/com/tangem/data/quotes/store/QuotesStoreUpdateMethodsTest.kt @@ -37,9 +37,7 @@ internal class QuotesStoreUpdateMethodsTest { store.refresh(currenciesIds = currenciesIds) - val runtimeExpected = currenciesIds.map(Quote::Empty).toSet() - - Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(runtimeExpected) + Truth.assertThat(runtimeStore.getSyncOrNull()).isEqualTo(emptySet()) Truth.assertThat(persistenceStore.data.firstOrNull()).isEqualTo(emptyMap>()) } From 37dc4638088cb166466a256e730ab542021b59f6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 11:44:32 +0400 Subject: [PATCH 133/151] Updated on 2026-08-14 --- data/wallet-connect/build.gradle.kts | 1 + .../di/WalletConnectDataModule.kt | 3 + .../BlockAidEthereumVerificationDelegate.kt | 50 -------------- .../DefaultWcEthMessageSignUseCase.kt | 11 ++- .../DefaultWcEthSendTransactionUseCase.kt | 11 ++- .../DefaultWcEthSignTransactionUseCase.kt | 11 ++- .../network/ethereum/WcEthNetwork.kt | 12 +++- ...efaultWcSolanaSignAllTransactionUseCase.kt | 10 +++ .../DefaultWcSolanaSignTransactionUseCase.kt | 10 +++ .../network/solana/WcSolanaNetwork.kt | 22 +++++- .../walletconnect/sign/BaseWcSignUseCase.kt | 1 + .../utils/BlockAidVerificationDelegate.kt | 67 +++++++++++++++++++ .../WcBlockAidEligibleTransactionUseCase.kt | 5 +- .../solana/WcSolanaSignTransactionUseCase.kt | 7 +- 14 files changed, 157 insertions(+), 64 deletions(-) delete mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/BlockAidEthereumVerificationDelegate.kt create mode 100644 data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt diff --git a/data/wallet-connect/build.gradle.kts b/data/wallet-connect/build.gradle.kts index d03ae4e940..2b9321f602 100644 --- a/data/wallet-connect/build.gradle.kts +++ b/data/wallet-connect/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(deps.arrow.core) implementation(projects.domain.blockaid) implementation(projects.domain.blockaid.models) + implementation(projects.libs.crypto) /* Tests */ testImplementation(projects.common.test) 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 0afaeb4dd7..77752b86b6 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 @@ -35,6 +35,7 @@ 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.GetWalletsUseCase +import com.tangem.lib.crypto.UserWalletManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -146,11 +147,13 @@ internal object WalletConnectDataModule { excludedBlockchains: ExcludedBlockchains, sessionsManager: WcSessionsManager, factories: WcSolanaNetwork.Factories, + walletManager: UserWalletManager, ): WcSolanaNetwork = WcSolanaNetwork( moshi = moshi, sessionsManager = sessionsManager, factories = factories, excludedBlockchains = excludedBlockchains, + walletManager = walletManager, ) @Provides diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/BlockAidEthereumVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/BlockAidEthereumVerificationDelegate.kt deleted file mode 100644 index ba45ffbdcd..0000000000 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/BlockAidEthereumVerificationDelegate.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.data.walletconnect.network.ethereum - -import com.domain.blockaid.models.transaction.CheckTransactionResult -import com.domain.blockaid.models.transaction.TransactionData -import com.domain.blockaid.models.transaction.TransactionParams -import com.tangem.domain.blockaid.BlockAidVerifier -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.tokens.model.Network -import com.tangem.domain.walletconnect.model.WcEthMethod -import com.tangem.domain.walletconnect.model.WcSession -import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import timber.log.Timber -import javax.inject.Inject - -internal class BlockAidEthereumVerificationDelegate @Inject constructor( - private val blockAidVerifier: BlockAidVerifier, -) { - - fun getSecurityStatus( - network: Network, - method: WcEthMethod, - rawSdkRequest: WcSdkSessionRequest, - session: WcSession, - ): Flow> = flow { - emit(Lce.Loading(partialContent = null)) - blockAidVerifier.verifyTransaction( - TransactionData( - chain = network.name, - accountAddress = when (method) { - is WcEthMethod.MessageSign -> method.account - is WcEthMethod.SendTransaction -> method.transaction.from - is WcEthMethod.SignTransaction -> method.transaction.from - }, - method = rawSdkRequest.request.method, - domainUrl = session.sdkModel.appMetaData.url, - params = TransactionParams.Evm(rawSdkRequest.request.params), - ), - ).fold( - ifLeft = { - Timber.e("Failed to verify transaction: ${it.localizedMessage}") - emit(Lce.Error(it)) - }, - ifRight = { - emit(Lce.Content(it)) - }, - ) - } -} \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt index ed0c48b91b..4472b581fa 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt @@ -7,6 +7,7 @@ import com.tangem.blockchain.common.HEX_PREFIX import com.tangem.blockchain.extensions.isAscii import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey +import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector @@ -30,11 +31,17 @@ internal class DefaultWcEthMessageSignUseCase @AssistedInject constructor( @Assisted private val method: WcEthMethod.MessageSign, private val walletManagersFacade: WalletManagersFacade, private val signUseCase: SignUseCase, - blockAidDelegate: BlockAidEthereumVerificationDelegate, + blockAidDelegate: BlockAidVerificationDelegate, ) : BaseWcSignUseCase(), WcEthMessageSignUseCase { - override val securityStatus = blockAidDelegate.getSecurityStatus(network, method, rawSdkRequest, session) + override val securityStatus = blockAidDelegate.getSecurityStatus( + network = network, + method = method, + rawSdkRequest = rawSdkRequest, + session = session, + accountAddress = context.accountAddress, + ) override suspend fun SignCollector.onSign( state: WcSignState, 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 index 00f485605d..e4bd53eb97 100644 --- 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 @@ -3,6 +3,7 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex +import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector @@ -25,11 +26,17 @@ internal class DefaultWcEthSendTransactionUseCase @AssistedInject constructor( @Assisted private val method: WcEthMethod.SendTransaction, override val respondService: WcRespondService, private val sendTransaction: SendTransactionUseCase, - blockAidDelegate: BlockAidEthereumVerificationDelegate, + blockAidDelegate: BlockAidVerificationDelegate, ) : BaseWcSignUseCase(), WcEthSendTransactionUseCase { - override val securityStatus = blockAidDelegate.getSecurityStatus(network, method, rawSdkRequest, session) + override val securityStatus = blockAidDelegate.getSecurityStatus( + network = network, + method = method, + rawSdkRequest = rawSdkRequest, + session = session, + accountAddress = context.accountAddress, + ) private val converter = EthTransactionParamsConverter(context) 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 index a4ad2df2a1..e3b1fab7dc 100644 --- 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 @@ -4,6 +4,7 @@ import arrow.core.left import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.formatHex import com.tangem.common.extensions.toHexString +import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.data.walletconnect.respond.WcRespondService import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector @@ -27,11 +28,17 @@ internal class DefaultWcEthSignTransactionUseCase @AssistedInject constructor( private val prepareForSend: PrepareForSendUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted private val method: WcEthMethod.SignTransaction, - blockAidDelegate: BlockAidEthereumVerificationDelegate, + blockAidDelegate: BlockAidVerificationDelegate, ) : BaseWcSignUseCase(), WcEthSignTransactionUseCase { - override val securityStatus = blockAidDelegate.getSecurityStatus(network, method, rawSdkRequest, session) + override val securityStatus = blockAidDelegate.getSecurityStatus( + network = network, + method = method, + rawSdkRequest = rawSdkRequest, + session = session, + accountAddress = context.accountAddress, + ) private val converter = EthTransactionParamsConverter(context) 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 0611504329..e0bb31b917 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 @@ -39,7 +39,17 @@ internal class WcEthNetwork( 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) + val accountAddress = when (method) { + is WcEthMethod.MessageSign -> method.account + is WcEthMethod.SendTransaction -> method.transaction.from + is WcEthMethod.SignTransaction -> method.transaction.from + } + val context = WcMethodUseCaseContext( + session = session, + rawSdkRequest = request, + network = network, + accountAddress = accountAddress, + ) return when (method) { is WcEthMethod.MessageSign -> factories.messageSign.create(context, method) is WcEthMethod.SendTransaction -> factories.sendTransaction.create(context, method) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt index 4724192a80..ed8685df0b 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignAllTransactionUseCase.kt @@ -8,6 +8,7 @@ import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.transaction.usecase.PrepareForSendUseCase import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.sign.WcSignState @@ -25,9 +26,18 @@ internal class DefaultWcSolanaSignAllTransactionUseCase @AssistedInject construc private val prepareForSend: PrepareForSendUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted private val method: WcSolanaMethod.SignAllTransaction, + blockAidDelegate: BlockAidVerificationDelegate, ) : BaseWcSignUseCase>(), WcSolanaSignAllTransactionUseCase { + override val securityStatus = blockAidDelegate.getSecurityStatus( + network = network, + method = method, + rawSdkRequest = rawSdkRequest, + session = session, + accountAddress = context.accountAddress, + ) + override suspend fun SignCollector>.onSign( state: WcSignState>, ) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt index 4a497421ff..e923e85969 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/DefaultWcSolanaSignTransactionUseCase.kt @@ -8,6 +8,7 @@ import com.tangem.data.walletconnect.sign.BaseWcSignUseCase import com.tangem.data.walletconnect.sign.SignCollector import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.WcMethodUseCaseContext +import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate import com.tangem.domain.transaction.usecase.PrepareForSendUseCase import com.tangem.domain.walletconnect.model.WcSolanaMethod import com.tangem.domain.walletconnect.usecase.sign.WcSignState @@ -23,9 +24,18 @@ internal class DefaultWcSolanaSignTransactionUseCase @AssistedInject constructor private val prepareForSend: PrepareForSendUseCase, @Assisted override val context: WcMethodUseCaseContext, @Assisted private val method: WcSolanaMethod.SignTransaction, + blockAidDelegate: BlockAidVerificationDelegate, ) : BaseWcSignUseCase(), WcSolanaSignTransactionUseCase { + override val securityStatus = blockAidDelegate.getSecurityStatus( + network = network, + method = method, + rawSdkRequest = rawSdkRequest, + session = session, + accountAddress = context.accountAddress, + ) + override suspend fun SignCollector.onSign(state: WcSignState) { val hash = prepareForSend.invoke(transactionData = state.signModel, userWallet = wallet, network = network) .onLeft { error -> 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 fd4e3b5832..30221cd33b 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 @@ -16,13 +16,16 @@ 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 com.tangem.lib.crypto.UserWalletManager import jakarta.inject.Inject +import timber.log.Timber -internal class WcSolanaNetwork constructor( +internal class WcSolanaNetwork( private val moshi: Moshi, private val sessionsManager: WcSessionsManager, private val factories: Factories, private val excludedBlockchains: ExcludedBlockchains, + private val walletManager: UserWalletManager, ) : WcNamespaceConverter, WcRequestToUseCaseConverter { override val namespaceKey: NamespaceKey = NamespaceKey("solana") @@ -38,7 +41,13 @@ internal class WcSolanaNetwork constructor( val method: WcSolanaMethod = 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) + val accountAddress = getAccountAddress(network) + val context = WcMethodUseCaseContext( + session = session, + rawSdkRequest = request, + network = network, + accountAddress = accountAddress, + ) return when (method) { is WcSolanaMethod.SignMessage -> TODO() is WcSolanaMethod.SignTransaction -> factories.signTransaction.create(context, method) @@ -46,6 +55,15 @@ internal class WcSolanaNetwork constructor( } } + private suspend fun getAccountAddress(network: Network): String? { + return try { + walletManager.getWalletAddress(network.id.value, network.derivationPath.value) + } catch (exception: Exception) { + Timber.e(exception) + null + } + } + override fun toBlockchain(chainId: CAIP2): Blockchain? { if (chainId.namespace != namespaceKey.key) return null return when (chainId.reference) { 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 8967ae0706..a395e1bf2f 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 @@ -60,6 +60,7 @@ internal class WcMethodUseCaseContext( val session: WcSession, val rawSdkRequest: WcSdkSessionRequest, val network: Network, + val accountAddress: String?, ) internal typealias SignCollector = FlowCollector> \ No newline at end of file diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt new file mode 100644 index 0000000000..eb865b17fd --- /dev/null +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/BlockAidVerificationDelegate.kt @@ -0,0 +1,67 @@ +package com.tangem.data.walletconnect.utils + +import com.domain.blockaid.models.transaction.* +import com.tangem.domain.blockaid.BlockAidVerifier +import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.walletconnect.model.WcEthMethod +import com.tangem.domain.walletconnect.model.WcMethod +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.model.WcSolanaMethod +import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest +import kotlinx.coroutines.flow.flow +import timber.log.Timber +import javax.inject.Inject + +internal class BlockAidVerificationDelegate @Inject constructor( + private val blockAidVerifier: BlockAidVerifier, +) { + + fun getSecurityStatus( + network: Network, + method: WcMethod, + rawSdkRequest: WcSdkSessionRequest, + session: WcSession, + accountAddress: String?, + ): LceFlow = flow { + emit(Lce.Loading(partialContent = null)) + val failedResult = CheckTransactionResult( + validation = ValidationResult.FAILED_TO_VALIDATE, + simulation = SimulationResult.FailedToSimulate, + ) + if (accountAddress.isNullOrEmpty()) { + emit(Lce.Content(failedResult)) + return@flow + } + when (method) { + is WcEthMethod -> TransactionParams.Evm(rawSdkRequest.request.params) + is WcSolanaMethod.SignAllTransaction -> TransactionParams.Solana(method.transaction) + is WcSolanaMethod.SignTransaction -> TransactionParams.Solana(listOf(method.transaction)) + is WcSolanaMethod.SignMessage -> { + // BlockAid doesn't support solana_signMessage + emit(Lce.Content(failedResult)) + return@flow + } + else -> null + }?.let { params -> + blockAidVerifier.verifyTransaction( + TransactionData( + chain = network.name, + accountAddress = accountAddress, + method = rawSdkRequest.request.method, + domainUrl = session.sdkModel.appMetaData.url, + params = params, + ), + ).fold( + ifLeft = { + Timber.e("Failed to verify transaction: ${it.localizedMessage}") + emit(Lce.Error(it)) + }, + ifRight = { + emit(Lce.Content(it)) + }, + ) + } ?: emit(Lce.Content(failedResult)) + } +} \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/blockaid/WcBlockAidEligibleTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/blockaid/WcBlockAidEligibleTransactionUseCase.kt index 8cf1497baf..31733942f8 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/blockaid/WcBlockAidEligibleTransactionUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/blockaid/WcBlockAidEligibleTransactionUseCase.kt @@ -1,10 +1,9 @@ package com.tangem.domain.walletconnect.usecase.blockaid import com.domain.blockaid.models.transaction.CheckTransactionResult -import com.tangem.domain.core.lce.Lce -import kotlinx.coroutines.flow.Flow +import com.tangem.domain.core.lce.LceFlow interface WcBlockAidEligibleTransactionUseCase { - val securityStatus: Flow> + val securityStatus: LceFlow } \ No newline at end of file diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt index 92eb17320f..a99cd1d71a 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/solana/WcSolanaSignTransactionUseCase.kt @@ -4,13 +4,15 @@ import com.tangem.blockchain.common.TransactionData 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.blockaid.WcBlockAidEligibleTransactionUseCase import com.tangem.domain.walletconnect.usecase.sign.WcSignState import com.tangem.domain.walletconnect.usecase.sign.WcSignUseCase import kotlinx.coroutines.flow.Flow interface WcSolanaSignTransactionUseCase : WcSignUseCase, - WcSignUseCase.SimpleRun { + WcSignUseCase.SimpleRun, + WcBlockAidEligibleTransactionUseCase { override val network: Network override val rawSdkRequest: WcSdkSessionRequest @@ -23,7 +25,8 @@ interface WcSolanaSignTransactionUseCase : interface WcSolanaSignAllTransactionUseCase : WcSignUseCase, - WcSignUseCase.SimpleRun> { + WcSignUseCase.SimpleRun>, + WcBlockAidEligibleTransactionUseCase { override val network: Network override val rawSdkRequest: WcSdkSessionRequest From c1c3b52715e2095c951c409b9d7cabfe03c86bbd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 12:46:51 +0400 Subject: [PATCH 134/151] Updated on 2026-08-14 --- .../datasource/api/common/config/BlockAid.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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 index 008cd9ef09..f05797c816 100644 --- 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 @@ -2,9 +2,10 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.utils.ProviderSuspend +import kotlinx.coroutines.flow.first internal class BlockAid( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val configStorage: EnvironmentConfigStorage, ) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD @@ -17,9 +18,14 @@ internal class BlockAid( environment = ApiEnvironment.PROD, baseUrl = "https://api.blockaid.io/v0/", headers = buildMap { - environmentConfigStorage.getConfigSync().blockAidApiKey?.let { apiKey -> - put("X-API-KEY", ProviderSuspend { apiKey }) - } + put( + key = "X-API-KEY", + value = ProviderSuspend { + requireNotNull( + configStorage.getConfig().first { !it.blockAidApiKey.isNullOrEmpty() }.blockAidApiKey, + ) + }, + ) put("accept", ProviderSuspend { "application/json" }) put("content-type", ProviderSuspend { "application/json" }) }, From b280cd9e5e13ce2b1d05c214fb83f9caaa963edd Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 14:02:26 +0500 Subject: [PATCH 135/151] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../di/WalletConnectDataModule.kt | 12 +- .../pair/CaipNamespaceDelegate.kt | 5 + .../usecase/WcSessionsUseCase.kt | 4 + .../connections/components/AlertsComponent.kt | 19 ++ .../components/ConnectionsComponent.kt | 13 +- .../components/WcConnectedAppInfoComponent.kt | 30 +++ .../connections/entity/WcAppInfoUM.kt | 2 +- .../entity/WcConnectedAppInfoUM.kt | 15 ++ .../connections/entity/WcNetworkInfoItem.kt | 10 + .../connections/model/WcAppInfoModel.kt | 3 +- .../model/WcConnectedAppInfoModel.kt | 88 +++++++++ .../connections/model/WcConnectionsModel.kt | 30 ++- .../transformers/WcAppInfoTransformer.kt | 16 +- .../connections/routes/WcAppInfoRoutes.kt | 7 + ...s.kt => WcConnectionsBottomSheetConfig.kt} | 7 +- .../connections/ui/InternalComponents.kt | 31 +++ .../connections/ui/WcAppInfoContent.kt | 51 +++-- .../connections/ui/WcConnectedAppInfoBS.kt | 185 ++++++++++++++++++ .../di/WalletConnectModelModule.kt | 6 + 20 files changed, 491 insertions(+), 44 deletions(-) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoComponent.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcNetworkInfoItem.kt create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt rename features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/{WcConnectionsBottomSheetRoutes.kt => WcConnectionsBottomSheetConfig.kt} (54%) create mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcConnectedAppInfoBS.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 90a9ac6c99..02d8bd7799 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1292,6 +1292,7 @@ Network Networks Wallet + Connected networks View your wallet balance and activity Sign transactions without your notice Request approval for transactions 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 77752b86b6..16f2cde7ad 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 @@ -7,11 +7,6 @@ import com.tangem.data.walletconnect.DefaultWalletConnectRepository import com.tangem.data.walletconnect.initialize.DefaultWcInitializeUseCase import com.tangem.data.walletconnect.network.ethereum.WcEthNetwork 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.DefaultWcPairService -import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase -import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.pair.* import com.tangem.data.walletconnect.request.DefaultWcRequestService import com.tangem.data.walletconnect.request.DefaultWcRequestUseCaseFactory @@ -31,6 +26,7 @@ import com.tangem.domain.walletconnect.WcRequestUseCaseFactory 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.usecase.disconnect.WcDisconnectUseCase import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -197,6 +193,12 @@ internal object WalletConnectDataModule { return DefaultWcRequestUseCaseFactory(diHelperBox.handlers) } + @Provides + @Singleton + fun providesWcDisconnectUseCase(sessionsManager: WcSessionsManager): WcDisconnectUseCase { + return WcDisconnectUseCase(sessionsManager) + } + internal class DiHelperBox( val converters: Set, val handlers: Set, 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 597bdecefb..cdc248d8b7 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 @@ -34,9 +34,14 @@ internal class CaipNamespaceDelegate constructor( requiredNamespaces[namespaceKey]?.methods?.let { addAll(it) } optionalNamespaces[namespaceKey]?.methods?.let { addAll(it) } } + val events = buildSet { + requiredNamespaces[namespaceKey]?.events?.let { addAll(it) } + optionalNamespaces[namespaceKey]?.events?.let { addAll(it) } + } session.chains.add(account.chainId.raw) session.accounts.add(account.raw) session.methods.addAll(methods) + session.events.addAll(events) } return result.mapValues { (namespaceKey, session) -> Wallet.Model.Namespace.Session( 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 138c4a419f..d15047f8e9 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 @@ -14,4 +14,8 @@ class WcSessionsUseCase(private val sessionsManager: WcSessionsManager) { suspend fun invokeSync(): Map> { return sessionsManager.sessions.first() } + + suspend fun findByTopic(topic: String): WcSession? { + return sessionsManager.findSessionByTopic(topic) + } } \ No newline at end of file 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 index a03ae3a26d..5ba2668002 100644 --- 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 @@ -7,6 +7,7 @@ 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 +import kotlinx.serialization.Serializable internal class AlertsComponent( appComponentContext: AppComponentContext, @@ -31,17 +32,35 @@ internal class AlertsComponent( internal data class Params(val alertType: AlertType) @Immutable + @Serializable internal sealed class AlertType : TangemBottomSheetConfigContent { abstract val onDismiss: () -> Unit + @Serializable data class VerifiedDomain(val appName: String, override val onDismiss: () -> Unit) : AlertType() + + @Serializable data class UnknownDomain(val onConnect: () -> Unit, override val onDismiss: () -> Unit) : AlertType() + + @Serializable data class UnsafeDomain(val onConnect: () -> Unit, override val onDismiss: () -> Unit) : AlertType() + + @Serializable data class UnsupportedNetworks(val appName: String, override val onDismiss: () -> Unit) : AlertType() + + @Serializable data class WcDisconnected(override val onDismiss: () -> Unit) : AlertType() + + @Serializable data class UnknownError(val errorCode: Int, override val onDismiss: () -> Unit) : AlertType() + + @Serializable data class WrongCardSelected(override val onDismiss: () -> Unit) : AlertType() + + @Serializable data class ConnectionTimeout(val onTryAgain: () -> Unit, override val onDismiss: () -> Unit) : AlertType() + + @Serializable data class MaliciousTransaction( val descriptionMessage: String, val onConnect: () -> Unit, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt index 0c311f6f4a..0fd5c6d2d5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/ConnectionsComponent.kt @@ -15,7 +15,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.features.walletconnect.connections.model.WcConnectionsModel -import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetRoutes +import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetConfig import com.tangem.features.walletconnect.connections.ui.WcConnectionsContent internal class ConnectionsComponent( @@ -40,11 +40,11 @@ internal class ConnectionsComponent( } private fun bottomSheetChild( - config: WcConnectionsBottomSheetRoutes, + config: WcConnectionsBottomSheetConfig, componentContext: ComponentContext, ): ComposableBottomSheetComponent { return when (config) { - is WcConnectionsBottomSheetRoutes.AppInfo -> WcAppInfoContainerComponent( + is WcConnectionsBottomSheetConfig.AppInfo -> WcAppInfoContainerComponent( appComponentContext = childByContext(componentContext), params = WcAppInfoContainerComponent.Params( wcUrl = config.wcUrl, @@ -52,6 +52,13 @@ internal class ConnectionsComponent( onDismiss = model.bottomSheetNavigation::dismiss, ), ) + is WcConnectionsBottomSheetConfig.ConnectedApp -> WcConnectedAppInfoComponent( + appComponentContext = childByContext(componentContext), + params = WcConnectedAppInfoComponent.Params( + topic = config.topic, + onDismiss = model.bottomSheetNavigation::dismiss, + ), + ) } } } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoComponent.kt new file mode 100644 index 0000000000..cfdaa99d34 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcConnectedAppInfoComponent.kt @@ -0,0 +1,30 @@ +package com.tangem.features.walletconnect.connections.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.walletconnect.connections.model.WcConnectedAppInfoModel +import com.tangem.features.walletconnect.connections.ui.WcConnectedAppInfoBS + +internal class WcConnectedAppInfoComponent( + params: Params, + appComponentContext: AppComponentContext, +) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { + + private val model: WcConnectedAppInfoModel = getOrCreateModel(params = params) + + override fun dismiss() { + model.dismiss() + } + + @Composable + override fun BottomSheet() { + val state by model.uiState.collectAsStateWithLifecycle() + state?.let { WcConnectedAppInfoBS(it) } + } + + data class Params(val topic: String, 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 index 413724be60..f7d17a2af5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -50,5 +50,5 @@ sealed class WcAppInfoSecurityNotification( @Immutable internal sealed class WcNetworksInfo { data class MissingRequiredNetworkInfo(val networks: String) : WcNetworksInfo() - data class ContainsAllRequiredNetworks(val icons: ImmutableList) : WcNetworksInfo() + data class ContainsAllRequiredNetworks(val items: ImmutableList) : WcNetworksInfo() } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt new file mode 100644 index 0000000000..5fe46765ca --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcConnectedAppInfoUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.walletconnect.connections.entity + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import kotlinx.collections.immutable.ImmutableList + +data class WcConnectedAppInfoUM( + val appName: String, + val appIcon: String, + val isVerified: Boolean, + val appSubtitle: String, + val walletName: String, + val networks: ImmutableList, + val disconnectButtonConfig: WcPrimaryButtonConfig, + val onDismiss: () -> Unit, +) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcNetworkInfoItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcNetworkInfoItem.kt new file mode 100644 index 0000000000..498b1b759c --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcNetworkInfoItem.kt @@ -0,0 +1,10 @@ +package com.tangem.features.walletconnect.connections.entity + +import androidx.annotation.DrawableRes + +data class WcNetworkInfoItem( + val id: String, + @DrawableRes val icon: Int, + val name: String, + val symbol: String, +) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt index d2c9722fbc..1d36aed1f4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcAppInfoModel.kt @@ -53,7 +53,7 @@ internal class WcAppInfoModel @Inject constructor( appInfoUiState.transformerUpdate( WcConnectButtonProgressTransformer(showProgress = false), ) - // TODO: wc dismiss and reload sessions + params.onDismiss() } is WcPairState.Error -> { // TODO: wc show toast/snackbar/alert? @@ -78,6 +78,7 @@ internal class WcAppInfoModel @Inject constructor( } fun dismiss() { + wcPairUseCase.reject() params.onDismiss() } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt new file mode 100644 index 0000000000..6126b5f404 --- /dev/null +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectedAppInfoModel.kt @@ -0,0 +1,88 @@ +package com.tangem.features.walletconnect.connections.model + +import androidx.compose.runtime.Stable +import com.domain.blockaid.models.dapp.CheckDAppResult +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.walletconnect.model.WcSession +import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase +import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase +import com.tangem.features.walletconnect.connections.components.WcConnectedAppInfoComponent +import com.tangem.features.walletconnect.connections.entity.WcConnectedAppInfoUM +import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@Stable +@ModelScoped +internal class WcConnectedAppInfoModel @Inject constructor( + private val wcSessionsUseCase: WcSessionsUseCase, + private val messageSender: UiMessageSender, + private val disconnectUseCase: WcDisconnectUseCase, + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, +) : Model() { + + private val params = paramsContainer.require() + val uiState: StateFlow + field = MutableStateFlow(null) + + init { + loadDAppInfo(params.topic) + } + + private fun loadDAppInfo(topic: String) { + modelScope.launch { + val session = wcSessionsUseCase.findByTopic(topic = topic) + if (session == null) { + Timber.e("Can not find WcSession by topic: $topic") + messageSender.send(SnackbarMessage(stringReference("Can not find WcSession by topic: $topic"))) + dismiss() + } else { + uiState.update { state -> + WcConnectedAppInfoUM( + appName = session.sdkModel.appMetaData.name, + appIcon = session.sdkModel.appMetaData.icons.firstOrNull().orEmpty(), + isVerified = session.securityStatus == CheckDAppResult.SAFE, + appSubtitle = session.sdkModel.appMetaData.description, + walletName = session.wallet.name, + networks = persistentListOf(), // TODO(wc): Nikolai & Doston: Where to find networks??? + disconnectButtonConfig = WcPrimaryButtonConfig( + showProgress = false, + enabled = true, + onClick = { disconnect(session) }, + ), + onDismiss = ::dismiss, + ) + } + } + } + } + + private fun disconnect(session: WcSession) { + uiState.update { state -> + state?.copy( + disconnectButtonConfig = state.disconnectButtonConfig.copy(showProgress = true), + ) + } + modelScope.launch { + disconnectUseCase.disconnect(session) + messageSender.send(SnackbarMessage(message = stringReference("dApp disconnected"))) + dismiss() + } + } + + fun dismiss() { + params.onDismiss() + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 69216f06c3..11a6d2a74c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -16,16 +16,19 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase +import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.usecase.WcSessionsUseCase +import com.tangem.domain.walletconnect.usecase.disconnect.WcDisconnectUseCase import com.tangem.features.walletconnect.connections.entity.WcConnectionsState import com.tangem.features.walletconnect.connections.entity.WcConnectionsTopAppBarConfig import com.tangem.features.walletconnect.connections.model.transformers.WcSessionsTransformer -import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetRoutes +import com.tangem.features.walletconnect.connections.routes.WcConnectionsBottomSheetConfig import com.tangem.features.walletconnect.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.transformer.update import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -35,12 +38,13 @@ internal class WcConnectionsModel @Inject constructor( private val uiMessageSender: UiMessageSender, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val wcSessionsUseCase: WcSessionsUseCase, + private val wcDisconnectUseCase: WcDisconnectUseCase, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { val uiState: StateFlow field = MutableStateFlow(getInitialState()) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { listenQrUpdates() @@ -50,39 +54,43 @@ internal class WcConnectionsModel @Inject constructor( private fun listenQrUpdates() { listenToQrScanningUseCase(SourceType.WALLET_CONNECT) .getOrElse { emptyFlow() } - .onEach { wcUrl -> bottomSheetNavigation.activate(WcConnectionsBottomSheetRoutes.AppInfo(wcUrl)) } + .onEach { wcUrl -> bottomSheetNavigation.activate(WcConnectionsBottomSheetConfig.AppInfo(wcUrl)) } .launchIn(modelScope) } private fun listenWcSessions() { wcSessionsUseCase.invoke() .conflate() + .onEach { Timber.tag("ddk9499").d("listenWcSessions before: ${it.size}") } .distinctUntilChanged() .onEach { sessionsMap -> + Timber.tag("ddk9499").d("listenWcSessions after: ${sessionsMap.size}") uiState.update( WcSessionsTransformer( sessionsMap = sessionsMap, - openAppInfoModal = { Timber.d("Open app info for session: $it") }, + openAppInfoModal = ::openAppInfoModal, ), ) } .launchIn(modelScope) } + private fun openAppInfoModal(session: WcSession) { + bottomSheetNavigation.activate(WcConnectionsBottomSheetConfig.ConnectedApp(session.sdkModel.topic)) + } + private fun openQrScanScreen() { router.push(AppRoute.QrScanning(source = AppRoute.QrScanning.Source.WalletConnect)) } - private fun disconnectAll() { + private fun showDisconnectAllDialog() { val message = DialogMessage( title = resourceReference(R.string.wc_disconnect_all_alert_title), message = resourceReference(R.string.wc_disconnect_all_alert_desc), firstActionBuilder = { EventMessageAction( title = resourceReference(R.string.common_disconnect), - onClick = { - // TODO(wc): disconnect all - }, + onClick = ::disconnectAllSessions, ) }, secondActionBuilder = { cancelAction() }, @@ -90,6 +98,10 @@ internal class WcConnectionsModel @Inject constructor( uiMessageSender.send(message) } + private fun disconnectAllSessions() { + modelScope.launch { wcDisconnectUseCase.disconnectAll() } + } + private fun getInitialState(): WcConnectionsState { return WcConnectionsState( topAppBarConfig = WcConnectionsTopAppBarConfig( @@ -101,7 +113,7 @@ internal class WcConnectionsModel @Inject constructor( disconnectAllItem = TangemDropdownMenuItem( title = resourceReference(R.string.wc_disconnect_all), textColorProvider = { TangemTheme.colors.text.warning }, - onClick = ::disconnectAll, + onClick = ::showDisconnectAllDialog, ), ), connections = persistentListOf(), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index d64ddbd43b..134338c410 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -4,10 +4,7 @@ import com.domain.blockaid.models.dapp.CheckDAppResult import com.tangem.core.ui.extensions.iconResId import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.wallets.models.UserWallet -import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification -import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM -import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo -import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig +import com.tangem.features.walletconnect.connections.entity.* import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toImmutableList @@ -53,8 +50,15 @@ internal class WcAppInfoTransformer( ) } else { WcNetworksInfo.ContainsAllRequiredNetworks( - icons = (proposalNetwork.required + proposalNetwork.available) - .map { it.iconResId } + items = (proposalNetwork.required + proposalNetwork.available) + .map { + WcNetworkInfoItem( + id = it.id.value, + icon = it.iconResId, + name = it.name, + symbol = it.currencySymbol, + ) + } .toImmutableList(), ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index 8c0d1fcc2c..74a5c349f2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -8,8 +8,15 @@ import kotlinx.serialization.Serializable @Serializable @Immutable internal sealed class WcAppInfoRoutes : TangemBottomSheetConfigContent { + @Serializable data object AppInfo : WcAppInfoRoutes() + + @Serializable data object SelectWallet : WcAppInfoRoutes() + + @Serializable data object SelectNetworks : WcAppInfoRoutes() + + @Serializable data class Alert(val alertType: AlertsComponent.AlertType) : WcAppInfoRoutes() } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetConfig.kt similarity index 54% rename from features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetRoutes.kt rename to features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetConfig.kt index 66f2dc4f1e..b83e610fd8 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcConnectionsBottomSheetConfig.kt @@ -3,7 +3,10 @@ package com.tangem.features.walletconnect.connections.routes import kotlinx.serialization.Serializable @Serializable -internal sealed class WcConnectionsBottomSheetRoutes { +internal sealed class WcConnectionsBottomSheetConfig { @Serializable - data class AppInfo(val wcUrl: String) : WcConnectionsBottomSheetRoutes() + data class AppInfo(val wcUrl: String) : WcConnectionsBottomSheetConfig() + + @Serializable + data class ConnectedApp(val topic: String) : WcConnectionsBottomSheetConfig() } \ 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 index 2f583ee287..960e4b2e74 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/InternalComponents.kt @@ -1,5 +1,6 @@ package com.tangem.features.walletconnect.connections.ui +import androidx.annotation.DrawableRes import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -10,6 +11,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.tangem.core.ui.res.TangemTheme @@ -64,4 +66,33 @@ internal fun WcAppInfoItem( ) } } +} + +@Composable +internal fun WcNetworkInfoItem(@DrawableRes icon: Int, name: String, symbol: String, modifier: Modifier = Modifier) { + Row( + modifier = modifier.padding(vertical = 14.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.size(24.dp), + painter = painterResource(icon), + contentDescription = null, + tint = Color.Unspecified, + ) + Text( + modifier = Modifier.padding(start = 12.dp).weight(1f, fill = false), + text = name, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = symbol, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } } \ 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 index eef9435ca0..f248d6323c 100644 --- 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 @@ -37,10 +37,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemColorPalette import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification -import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM -import com.tangem.features.walletconnect.connections.entity.WcNetworksInfo -import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig +import com.tangem.features.walletconnect.connections.entity.* import com.tangem.features.walletconnect.impl.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -308,7 +305,7 @@ private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier color = TangemTheme.colors.text.primary1, ) when (networksInfo) { - is WcNetworksInfo.ContainsAllRequiredNetworks -> NetworkIcons(icons = networksInfo.icons) + is WcNetworksInfo.ContainsAllRequiredNetworks -> NetworkIcons(items = networksInfo.items) is WcNetworksInfo.MissingRequiredNetworkInfo -> Unit } Icon( @@ -327,17 +324,17 @@ private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier @Suppress("MagicNumber") @Composable -private fun NetworkIcons(icons: ImmutableList, modifier: Modifier = Modifier) { - val (iconsToShow, remainingCount) = remember(icons) { +private fun NetworkIcons(items: ImmutableList, modifier: Modifier = Modifier) { + val (iconsToShow, remainingCount) = remember(items) { when { - icons.size <= 4 -> icons to 0 - else -> icons.take(3) to icons.size - 3 + items.size <= 4 -> items to 0 + else -> items.take(3) to items.size - 3 } } Box(modifier = modifier.wrapContentWidth()) { - iconsToShow.forEachIndexed { index, iconRes -> + iconsToShow.forEachIndexed { index, item -> Image( - painter = painterResource(id = iconRes), + painter = painterResource(id = item.icon), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier @@ -569,12 +566,32 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider( + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = state.onDismiss, + content = state, + ), + containerColor = TangemTheme.colors.background.tertiary, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(R.string.wc_wallet_connect), + endIconRes = R.drawable.ic_close_24, + onEndClick = state.onDismiss, + ) + }, + content = { + WcConnectedAppInfoBSContent( + state, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize(), + ) + }, + ) +} + +@Composable +private fun WcConnectedAppInfoBSContent(state: WcConnectedAppInfoUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + val blocksModifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) + .background(color = TangemTheme.colors.background.action) + .fillMaxWidth() + AppInfoFirstBlock( + state = state, + modifier = Modifier + .padding(top = 8.dp) + .then(blocksModifier), + ) + NetworksBlock( + networks = state.networks, + modifier = Modifier + .padding(top = 14.dp) + .then(blocksModifier), + ) + SecondaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + text = stringResourceSafe(R.string.common_disconnect), + enabled = state.disconnectButtonConfig.enabled, + showProgress = state.disconnectButtonConfig.showProgress, + onClick = state.disconnectButtonConfig.onClick, + ) + } +} + +@Composable +private fun AppInfoFirstBlock(state: WcConnectedAppInfoUM, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + WcAppInfoItem( + iconUrl = state.appIcon, + title = state.appName, + subtitle = state.appSubtitle, + isVerified = state.isVerified, + ) + HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens.spacing12), + 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 = state.walletName, + textAlign = TextAlign.End, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +@Composable +private fun NetworksBlock(networks: ImmutableList, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + Text( + modifier = Modifier + .padding(top = 12.dp, bottom = 4.dp) + .padding(horizontal = 12.dp), + text = stringResourceSafe(R.string.wc_connected_networks), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + networks.fastForEach { network -> + key(network.id) { WcNetworkInfoItem(icon = network.icon, name = network.name, symbol = network.symbol) } + } + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WcConnectedAppInfoBS_Preview() { + TangemThemePreview { + WcConnectedAppInfoBS( + state = WcConnectedAppInfoUM( + appName = "React App", + appIcon = "", + isVerified = true, + appSubtitle = "react-app.walletconnect.com", + walletName = "Tangem 2.0", + networks = persistentListOf( + WcNetworkInfoItem( + id = "1", + icon = R.drawable.img_optimism_22, + name = "img_optimism_22img_optimism_22", + symbol = "optimism", + ), + WcNetworkInfoItem(id = "2", icon = R.drawable.img_bsc_22, name = "img_bsc_22", symbol = "bsc"), + WcNetworkInfoItem( + id = "3", + icon = R.drawable.img_solana_22, + name = "img_solana_22", + symbol = "solana", + ), + ), + disconnectButtonConfig = WcPrimaryButtonConfig(showProgress = false, enabled = true, onClick = {}), + onDismiss = {}, + ), + ) + } +} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt index bb589438d6..00039ca420 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt @@ -3,6 +3,7 @@ package com.tangem.features.walletconnect.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.walletconnect.connections.model.WcAppInfoModel +import com.tangem.features.walletconnect.connections.model.WcConnectedAppInfoModel import com.tangem.features.walletconnect.connections.model.WcConnectionsModel import dagger.Binds import dagger.Module @@ -23,4 +24,9 @@ internal interface WalletConnectModelModule { @IntoMap @ClassKey(WcAppInfoModel::class) fun bindWcAppInfoModel(model: WcAppInfoModel): Model + + @Binds + @IntoMap + @ClassKey(WcConnectedAppInfoModel::class) + fun bindWcConnectedAppInfoModel(model: WcConnectedAppInfoModel): Model } \ No newline at end of file From a0f6e4a0080c322a5049b09b4cdd73b1b638428d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 16:35:51 +0700 Subject: [PATCH 136/151] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/TangemApplication.kt | 6 +++--- .../data/DefaultLegacyWalletConnectRepository.kt | 9 ++++----- .../domain/LegacyWalletConnectRepository.kt | 2 +- .../data/walletconnect/di/WalletConnectDataModule.kt | 5 ----- .../initialize/DefaultWcInitializeUseCase.kt | 8 +------- .../usecase/initialize/WcInitializeUseCase.kt | 2 +- 6 files changed, 10 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index c4e6516129..d61bb4e130 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -297,9 +297,6 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat appScope.launch { launch(Dispatchers.IO) { loadNativeLibraries() - walletConnect2Repository.init( - projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId, - ) updateLogFiles() } } @@ -323,6 +320,9 @@ abstract class TangemApplication : Application(), ImageLoaderFactory, Configurat ) appStateHolder.mainStore = store + walletConnect2Repository.init( + projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId, + ) } private fun createReduxStore(): Store { diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt index 12c73bd711..32a138b4eb 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/data/DefaultLegacyWalletConnectRepository.kt @@ -45,10 +45,9 @@ internal class DefaultLegacyWalletConnectRepositoryFacade constructor( if (isNewWc) stub.currentSessions else legacy.currentSessions } - override suspend fun init(projectId: String) { + override fun init(projectId: String) { if (isNewWc) { - stub.init(projectId) - wcInitializeUseCase.init() // TODO(wc) Nikolai: Delete after RoutingComponent finished. + wcInitializeUseCase.init(projectId) } else { legacy.init(projectId) } @@ -96,7 +95,7 @@ internal class LegacyWalletConnectRepositoryStub : LegacyWalletConnectRepository override val activeSessions: Flow> = emptyFlow() override val currentSessions: List = listOf() - override suspend fun init(projectId: String) = Unit + override fun init(projectId: String) = Unit override fun setUserNamespaces(userNamespaces: Map>) = Unit @@ -141,7 +140,7 @@ internal class DefaultLegacyWalletConnectRepository( /** * @param projectId Project ID at https://cloud.walletconnect.com/ */ - override suspend fun init(projectId: String) { + override fun init(projectId: String) { val relayUrl = "relay.walletconnect.com" val serverUrl = "wss://$relayUrl?projectId=$projectId" val connectionType = ConnectionType.AUTOMATIC diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt index 10092dc183..59d5954e00 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect2/domain/LegacyWalletConnectRepository.kt @@ -13,7 +13,7 @@ interface LegacyWalletConnectRepository { val currentSessions: List - suspend fun init(projectId: String) + fun init(projectId: String) fun setUserNamespaces(userNamespaces: Map>) 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 16f2cde7ad..8d43fc1ebe 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 @@ -16,7 +16,6 @@ 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 -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.tokens.repository.CurrenciesRepository @@ -61,15 +60,11 @@ internal object WalletConnectDataModule { sessionsManager: DefaultWcSessionsManager, networkService: DefaultWcRequestService, pairSdkDelegate: WcPairSdkDelegate, - environmentConfigStorage: EnvironmentConfigStorage, - dispatchers: CoroutineDispatcherProvider, ): WcInitializeUseCase = DefaultWcInitializeUseCase( application = application, sessionsManager = sessionsManager, networkService = networkService, pairSdkDelegate = pairSdkDelegate, - environmentConfigStorage = environmentConfigStorage, - dispatchers = dispatchers, ) @Provides 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 64ea5578f6..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 @@ -10,10 +10,7 @@ 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 -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.walletconnect.usecase.initialize.WcInitializeUseCase -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext import timber.log.Timber internal class DefaultWcInitializeUseCase( @@ -21,8 +18,6 @@ internal class DefaultWcInitializeUseCase( private val sessionsManager: DefaultWcSessionsManager, private val networkService: DefaultWcRequestService, private val pairSdkDelegate: WcPairSdkDelegate, - private val environmentConfigStorage: EnvironmentConfigStorage, - private val dispatchers: CoroutineDispatcherProvider, ) : WcInitializeUseCase { private val wcSdkObservers = mutableSetOf( @@ -31,8 +26,7 @@ internal class DefaultWcInitializeUseCase( pairSdkDelegate, ) - override suspend fun init() = withContext(dispatchers.io) { - val projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId + override fun init(projectId: String) { val relayUrl = "relay.walletconnect.com" val serverUrl = "wss://$relayUrl?projectId=$projectId" val connectionType = ConnectionType.AUTOMATIC diff --git a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt index 0e00484903..71f465a43f 100644 --- a/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt +++ b/domain/wallet-connect/src/main/kotlin/com/tangem/domain/walletconnect/usecase/initialize/WcInitializeUseCase.kt @@ -1,5 +1,5 @@ package com.tangem.domain.walletconnect.usecase.initialize interface WcInitializeUseCase { - suspend fun init() + fun init(projectId: String) } \ No newline at end of file From 6c8b59c569d032a30612c346d0f8e5ddcedd424d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 16:08:31 +0500 Subject: [PATCH 137/151] Updated on 2026-08-14 --- .../FirebasePushNotificationsTokenProvider.kt | 2 +- .../local/preferences/PreferencesKeys.kt | 2 + .../DefaultNotificationsRepository.kt | 12 -- .../DefaultNotificationsRepositoryTest.kt | 40 ---- data/wallets/build.gradle.kts | 8 + .../data/wallets/DefaultWalletsRepository.kt | 47 ++++- .../wallets/DefaultWalletsRepositoryTest.kt | 176 ++++++++++++++++++ domain/notifications/build.gradle.kts | 20 +- .../repository/NotificationsRepository.kt | 6 - .../wallets/repository/WalletsRepository.kt | 6 + .../GetIsNotificationsEnabledUseCase.kt | 17 ++ .../usecase/SetNotificationsEnabledUseCase.kt | 18 ++ 12 files changed, 288 insertions(+), 66 deletions(-) create mode 100644 data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsNotificationsEnabledUseCase.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt index eaf9730f21..02a9b8be72 100644 --- a/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt +++ b/app/src/main/java/com/tangem/tap/data/FirebasePushNotificationsTokenProvider.kt @@ -4,7 +4,7 @@ import com.google.firebase.messaging.FirebaseMessaging import com.tangem.utils.notifications.PushNotificationsTokenProvider import kotlinx.coroutines.tasks.await -class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { +internal class FirebasePushNotificationsTokenProvider : PushNotificationsTokenProvider { override suspend fun getToken(): String { return FirebaseMessaging.getInstance().token.await() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 5177c8d4aa..83c7a513f0 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -125,6 +125,8 @@ object PreferencesKeys { // region Notifications val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") } + + val NOTIFICATIONS_ENABLED_STATES_KEY by lazy { stringPreferencesKey(name = "notificationsEnabledStates") } // endregion fun getShouldShowStoriesKey(storyId: String) = booleanPreferencesKey("shouldShowStories_$storyId") diff --git a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt index 8648ace195..57d8a6344a 100644 --- a/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt +++ b/data/notifications/src/main/java/com/tangem/data/notifications/DefaultNotificationsRepository.kt @@ -44,14 +44,6 @@ internal class DefaultNotificationsRepository @Inject constructor( return appPreferencesStore.getSyncOrNull(PreferencesKeys.NOTIFICATIONS_APPLICATION_ID_KEY) } - override suspend fun setNotificationsEnabledForWallet(walletId: String, enabled: Boolean) = - withContext(dispatchers.io) { - tangemTechApi.setNotificationsEnabled( - walletId = walletId, - body = WalletBody(notifyStatus = enabled), - ).getOrThrow() - } - override suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) = withContext(dispatchers.io) { tangemTechApi.associateApplicationIdWithWallets( @@ -62,10 +54,6 @@ internal class DefaultNotificationsRepository @Inject constructor( ).getOrThrow() } - override suspend fun isNotificationsEnabledForWallet(walletId: String): Boolean = withContext(dispatchers.io) { - tangemTechApi.getWalletById(walletId).getOrThrow().notifyStatus - } - override suspend fun setWalletName(walletId: String, walletName: String) = withContext(dispatchers.io) { tangemTechApi.updateWallet( walletId, diff --git a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt index 65c9a1220d..305732cde6 100644 --- a/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt +++ b/data/notifications/src/test/java/com/tangem/data/notifications/DefaultNotificationsRepositoryTest.kt @@ -103,26 +103,6 @@ class DefaultNotificationsRepositoryTest { assertThat(result).isEqualTo(expectedAppId) } - @Test - fun `GIVEN wallet id and enabled status WHEN setNotificationsEnabledForWallet THEN updates notification status`() = - runTest { - // GIVEN - val walletId = "test-wallet-id" - val enabled = true - coEvery { - tangemTechApi.setNotificationsEnabled( - walletId, - WalletBody(notifyStatus = enabled), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.setNotificationsEnabledForWallet(walletId, enabled) - - // THEN - coVerify { tangemTechApi.setNotificationsEnabled(walletId, WalletBody(notifyStatus = enabled)) } - } - @Test fun `GIVEN application id and wallet list WHEN associateApplicationIdWithWallets THEN associates them`() = runTest { // GIVEN @@ -142,26 +122,6 @@ class DefaultNotificationsRepositoryTest { coVerify { tangemTechApi.associateApplicationIdWithWallets(appId, wallets.map { WalletIdBody(it) }) } } - @Test - fun `GIVEN wallet id WHEN isNotificationsEnabledForWallet THEN returns notification status`() = runTest { - // GIVEN - val walletId = "test-wallet-id" - val expectedStatus = true - coEvery { tangemTechApi.getWalletById(walletId) } returns ApiResponse.Success( - WalletResponse( - notifyStatus = expectedStatus, - name = "Test Wallet", - id = walletId, - ), - ) - - // WHEN - val result = repository.isNotificationsEnabledForWallet(walletId) - - // THEN - assertThat(result).isEqualTo(expectedStatus) - } - @Test fun `GIVEN wallet id and name WHEN setWalletName THEN updates wallet name`() = runTest { // GIVEN diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index 46c3ad34e3..e84f33b3bc 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -34,4 +34,12 @@ dependencies { /** Other deps */ implementation(deps.androidx.datastore) implementation(deps.arrow.core) + + /** tests */ + testImplementation(deps.test.junit) + testImplementation(deps.test.coroutine) + testImplementation(deps.test.truth) + testImplementation(deps.test.mockk) + testImplementation(deps.moshi) + testImplementation(deps.moshi.kotlin) } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 35bc64c215..39096c2551 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -6,6 +6,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.MarkUserWalletWasCreatedBody import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status +import com.tangem.datasource.api.tangemTech.models.WalletBody import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -21,11 +22,9 @@ import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext typealias SeedPhraseNotificationsStatuses = Map @@ -231,4 +230,44 @@ internal class DefaultWalletsRepository( ) } } + + override suspend fun isNotificationsEnabled(userWalletId: UserWalletId, force: Boolean): Boolean = + withContext(dispatchers.io) { + if (force) { + return@withContext loadAndSaveNotificationsEnabled(userWalletId) + } + + val localValue = appPreferencesStore.getObjectMap(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY) + .map { it[userWalletId.stringValue] } + .firstOrNull() + + localValue ?: loadAndSaveNotificationsEnabled(userWalletId) + } + + override suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) { + withContext(dispatchers.io) { + tangemTechApi.setNotificationsEnabled( + walletId = userWalletId.stringValue, + body = WalletBody(notifyStatus = isEnabled), + ).getOrThrow() + setNotificationsEnabledLocally(userWalletId, isEnabled) + } + } + + private suspend fun loadAndSaveNotificationsEnabled(userWalletId: UserWalletId): Boolean { + val walletResponse = tangemTechApi.getWalletById(walletId = userWalletId.stringValue).getOrThrow() + val isEnabled = walletResponse.notifyStatus + setNotificationsEnabledLocally(userWalletId, isEnabled) + return isEnabled + } + + private suspend fun setNotificationsEnabledLocally(userWalletId: UserWalletId, isEnabled: Boolean) { + appPreferencesStore.editData { + it.setObjectMap( + key = PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY, + value = it.getObjectMap(PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY) + .plus(userWalletId.stringValue to isEnabled), + ) + } + } } \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt new file mode 100644 index 0000000000..28f07454c2 --- /dev/null +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -0,0 +1,176 @@ +package com.tangem.data.wallets + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import com.squareup.moshi.Moshi +import com.tangem.datasource.api.common.response.ApiResponse +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.WalletBody +import com.tangem.datasource.api.tangemTech.models.WalletResponse +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import io.mockk.coEvery +import io.mockk.coVerify +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test + +class DefaultWalletsRepositoryTest { + private lateinit var repository: DefaultWalletsRepository + private val preferencesDataStore = mockk>(relaxed = true) + private val appPreferenceStore = AppPreferencesStore( + moshi = Moshi.Builder().build(), + dispatchers = TestingCoroutineDispatcherProvider(), + preferencesDataStore = preferencesDataStore, + ) + private lateinit var tangemTechApi: TangemTechApi + private lateinit var dispatchers: CoroutineDispatcherProvider + + private val testWalletId = UserWalletId("1234567890abcdef") + private val testWalletResponse = WalletResponse( + id = testWalletId.stringValue, + notifyStatus = true, + ) + + @Before + fun setup() { + tangemTechApi = mockk() + dispatchers = TestingCoroutineDispatcherProvider() + repository = DefaultWalletsRepository( + appPreferencesStore = appPreferenceStore, + tangemTechApi = tangemTechApi, + userWalletsStore = mockk(), + seedPhraseNotificationVisibilityStore = mockk(), + dispatchers = dispatchers, + ) + } + + @Test + fun `GIVEN force is true WHEN isNotificationsEnabled THEN should fetch from API and update local storage`() = + runTest { + // GIVEN + coEvery { tangemTechApi.getWalletById(testWalletId.stringValue) } returns ApiResponse.Success( + testWalletResponse, + ) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + val result = repository.isNotificationsEnabled(testWalletId, force = true) + + // THEN + assertThat(result).isTrue() + coVerify(exactly = 1) { + tangemTechApi.getWalletById(testWalletId.stringValue) + } + coVerify(exactly = 0) { + tangemTechApi.setNotificationsEnabled(any(), any()) + } + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN local storage has value WHEN isNotificationsEnabled THEN should return local value`() = runTest { + // GIVEN + val expectedPreferences = """{"${testWalletId.stringValue}":true}""" + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns expectedPreferences + coEvery { tangemTechApi.getWalletById(any()) } returns ApiResponse.Success(testWalletResponse) + + // WHEN + val result = repository.isNotificationsEnabled(testWalletId, force = false) + + // THEN + assertThat(result).isTrue() + coVerify(inverse = true) { + tangemTechApi.getWalletById(any()) + tangemTechApi.setNotificationsEnabled(any(), any()) + } + } + + @Test + fun `GIVEN local storage is empty WHEN isNotificationsEnabled THEN should fetch from API and update local storage`() = runTest { + // GIVEN + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + coEvery { tangemTechApi.getWalletById(testWalletId.stringValue) } returns ApiResponse.Success( + testWalletResponse, + ) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + val result = repository.isNotificationsEnabled(testWalletId, force = false) + + // THEN + assertThat(result).isTrue() + coVerify(exactly = 1) { + tangemTechApi.getWalletById(testWalletId.stringValue) + } + coVerify(exactly = 0) { + tangemTechApi.setNotificationsEnabled(any(), any()) + } + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN enabled status WHEN setNotificationsEnabled THEN should update API and local storage`() = runTest { + // GIVEN + coEvery { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = true)), + ) + } returns ApiResponse.Success(Unit) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + repository.setNotificationsEnabled(testWalletId, isEnabled = true) + + // THEN + coVerify(exactly = 1) { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = true)), + ) + } + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `GIVEN disabled status WHEN setNotificationsEnabled THEN should update API and local storage`() = runTest { + // GIVEN + coEvery { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = false)), + ) + } returns ApiResponse.Success(Unit) + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // WHEN + repository.setNotificationsEnabled(testWalletId, isEnabled = false) + + // THEN + coVerifyOrder { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = false)), + ) + preferencesDataStore.updateData(any()) + } + coVerify(exactly = 1) { + tangemTechApi.setNotificationsEnabled( + eq(testWalletId.stringValue), + eq(WalletBody(notifyStatus = false)), + ) + } + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } +} \ No newline at end of file diff --git a/domain/notifications/build.gradle.kts b/domain/notifications/build.gradle.kts index eba62986ce..3c44620a5f 100644 --- a/domain/notifications/build.gradle.kts +++ b/domain/notifications/build.gradle.kts @@ -1,17 +1,31 @@ plugins { - alias(deps.plugins.kotlin.jvm) - alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } +android { + namespace = "com.tangem.domain.notifications" +} + dependencies { implementation(projects.core.utils) implementation(projects.domain.core) + implementation(projects.domain.wallets.models) implementation(projects.domain.notifications.models) + implementation(projects.domain.wallets) - /* Tests */ + // region DI + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) + // end + + // region Tests testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) testImplementation(deps.test.truth) testImplementation(deps.test.mockk) + // end } \ No newline at end of file diff --git a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt index 5fecc4b7e4..18575a904c 100644 --- a/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt +++ b/domain/notifications/src/main/java/com/tangem/domain/notifications/repository/NotificationsRepository.kt @@ -11,15 +11,9 @@ interface NotificationsRepository { suspend fun getApplicationId(): String? - @Throws - suspend fun setNotificationsEnabledForWallet(walletId: String, enabled: Boolean) - @Throws suspend fun associateApplicationIdWithWallets(appId: String, wallets: List) - @Throws - suspend fun isNotificationsEnabledForWallet(walletId: String): Boolean - @Throws suspend fun setWalletName(walletId: String, walletName: String) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 6ec22a4eaf..541e601c54 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -37,4 +37,10 @@ interface WalletsRepository { suspend fun enableNFT(userWalletId: UserWalletId) suspend fun disableNFT(userWalletId: UserWalletId) + + @Throws + suspend fun isNotificationsEnabled(userWalletId: UserWalletId, force: Boolean): Boolean + + @Throws + suspend fun setNotificationsEnabled(userWalletId: UserWalletId, isEnabled: Boolean) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsNotificationsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsNotificationsEnabledUseCase.kt new file mode 100644 index 0000000000..9db6370c5c --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetIsNotificationsEnabledUseCase.kt @@ -0,0 +1,17 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class GetIsNotificationsEnabledUseCase( + private val walletsRepository: WalletsRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, force: Boolean): Either = Either.catch { + walletsRepository.isNotificationsEnabled( + userWalletId = userWalletId, + force = force, + ) + } +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt new file mode 100644 index 0000000000..574bca9f93 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SetNotificationsEnabledUseCase.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.wallets.usecase + +import arrow.core.Either +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.domain.wallets.repository.WalletsRepository + +class SetNotificationsEnabledUseCase( + private val walletsRepository: WalletsRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, isEnabled: Boolean): Either = + Either.catch { + walletsRepository.setNotificationsEnabled( + userWalletId = userWalletId, + isEnabled = isEnabled, + ) + } +} \ No newline at end of file From b4d3907dbcd9f1753c3f1f9a1c975acbba68c2bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 28 Apr 2025 18:39:02 +0300 Subject: [PATCH 138/151] Updated on 2026-08-14 --- .../tap/domain/tasks/visa/VisaCardActivationTask.kt | 10 +++++----- .../tasks/visa/VisaCustomerWalletApproveTask.kt | 11 ++++++----- .../domain/walletconnect/WalletConnectSdkHelper.kt | 8 ++++---- .../ethereum/DefaultWcEthMessageSignUseCase.kt | 11 ++++++----- gradle/tangem_dependencies.toml | 2 +- 5 files changed, 22 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt index 521c1444d8..dd4e607371 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -4,7 +4,7 @@ import arrow.core.Either import arrow.core.getOrElse import arrow.core.raise.catch import arrow.core.raise.either -import com.tangem.blockchain.blockchains.ethereum.EthereumUtils +import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.EllipticCurve import com.tangem.common.core.* @@ -359,11 +359,11 @@ class VisaCardActivationTask @AssistedInject constructor( otpStorage.getOTP(cardId) ?: return CompletionResult.Failure(VisaActivationError.MissingRootOTP.tangemError) } - val rsvSignature = EthereumUtils.prepareSignedMessageData( - signedHash = response.signature, - hashToSign = dataToSign.hashToSign.hexToBytes(), + val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( + signature = response.signature, + hash = dataToSign.hashToSign.hexToBytes(), publicKey = derivedPublicKey.publicKey.toDecompressedPublicKey(), - ) + ).asRSVLegacyEVM().toHexString() val signedActivationData = dataToSign.sign( rootOTP = otp.rootOTP.toHexString(), diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt index 2fb700cbd7..55dd835a3e 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCustomerWalletApproveTask.kt @@ -1,7 +1,7 @@ package com.tangem.tap.domain.tasks.visa import arrow.core.getOrElse -import com.tangem.blockchain.blockchains.ethereum.EthereumUtils +import com.tangem.blockchain.common.UnmarshalHelper import com.tangem.common.CompletionResult import com.tangem.common.card.Card import com.tangem.common.card.CardWallet @@ -12,6 +12,7 @@ import com.tangem.common.core.CompletionCallback import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey +import com.tangem.common.extensions.toHexString import com.tangem.core.error.ext.tangemError import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey @@ -175,12 +176,12 @@ class VisaCustomerWalletApproveTask( signTask.run(session) { result -> when (result) { is CompletionResult.Success -> { - val rsvSignature = EthereumUtils.prepareSignedMessageData( - signedHash = result.data.signature, - hashToSign = hashToSign, + val rsvSignature = UnmarshalHelper.unmarshalSignatureExtended( + signature = result.data.signature, + hash = hashToSign, publicKey = extendedPublicKey?.publicKey?.toDecompressedPublicKey() ?: targetWalletPublicKey.toDecompressedPublicKey(), - ) + ).asRSVLegacyEVM().toHexString() scanCard( session = session, 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 ea5b87285c..2ac037d2ec 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 @@ -389,11 +389,11 @@ class WalletConnectSdkHelper { return when (type) { WcSignMessage.WCSignType.SOLANA_MESSAGE -> getSolanaResultString(signedHash) - else -> EthereumUtils.prepareSignedMessageData( - signedHash = signedHash, - hashToSign = hashToSign, + else -> UnmarshalHelper.unmarshalSignatureExtended( + signature = signedHash, + hash = hashToSign, publicKey = wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ) + ).asRSVLegacyEVM().toHexString() } } is CompletionResult.Failure -> { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt index 4472b581fa..6f6a6565a8 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/DefaultWcEthMessageSignUseCase.kt @@ -1,13 +1,14 @@ package com.tangem.data.walletconnect.network.ethereum import arrow.core.left -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.common.UnmarshalHelper import com.tangem.blockchain.extensions.isAscii import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toDecompressedPublicKey import com.tangem.data.walletconnect.utils.BlockAidVerificationDelegate +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.SignCollector @@ -55,11 +56,11 @@ internal class DefaultWcEthMessageSignUseCase @AssistedInject constructor( .onLeft { emit(state.toResult(it.left())) } .getOrNull() ?: return - val respond = EthereumUtils.prepareSignedMessageData( - signedHash = signedHash, - hashToSign = hashToSign, + val respond = UnmarshalHelper.unmarshalSignatureExtended( + signature = signedHash, + hash = hashToSign, publicKey = walletManager.wallet.publicKey.blockchainKey.toDecompressedPublicKey(), - ) + ).asRSVLegacyEVM().toHexString() val wcRespondResult = respondService.respond(rawSdkRequest, respond) emit(state.toResult(wcRespondResult)) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ac75068c2a..0d1f7d70f6 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-1046" +tangemBlockchainSdk = "develop-1051" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-455" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From e9512b1afecf4eaf32d7c1a82733bcbce16fb391 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 29 Apr 2025 00:40:47 +0300 Subject: [PATCH 139/151] Updated on 2026-08-14 --- .../com/tangem/tap/common/DialogManager.kt | 2 - .../java/com/tangem/tap/common/TestActions.kt | 50 +--- .../com/tangem/tap/common/redux/AppDialog.kt | 12 - .../toggleWidget/RefreshBalanceWidget.kt | 118 -------- .../common/ui/AddressInfoBottomSheetDialog.kt | 75 ----- .../color/selector_btn_content_primary.xml | 5 - .../color/selector_btn_content_secondary.xml | 5 - .../main/res/color/selector_btn_primary.xml | 5 - .../main/res/color/selector_btn_secondary.xml | 5 - app/src/main/res/color/selector_chip_shop.xml | 11 - .../res/color/selector_chip_shop_text.xml | 11 - .../main/res/color/selector_chip_stroke.xml | 9 - app/src/main/res/color/selector_chip_text.xml | 9 - app/src/main/res/color/selector_edit_text.xml | 7 - .../color/selector_edit_text_secondary.xml | 5 - .../card_placeholder_wallet.9.png | Bin 172085 -> 0 bytes ...ay_button_no_shadow_background_image.9.png | Bin 263 -> 0 bytes .../buy_with_googlepay_button_content.xml | 54 ---- .../res/drawable/card_placeholder_white.xml | 108 ------- .../googlepay_button_no_shadow_background.xml | 6 - .../res/drawable/googlepay_button_overlay.xml | 12 - app/src/main/res/drawable/ic_access_code.xml | 24 -- .../main/res/drawable/ic_angle_bracket_up.xml | 11 - .../main/res/drawable/ic_arrow_refresh.xml | 9 - .../drawable/ic_baseline_arrow_back_24.xml | 11 - app/src/main/res/drawable/ic_dot.xml | 15 - app/src/main/res/drawable/ic_selected_dot.xml | 12 - .../res/drawable/img_onboarding_success.xml | 14 - .../res/drawable/progress_rounded_shape.xml | 17 -- .../main/res/drawable/selector_tabs_dots.xml | 8 - app/src/main/res/drawable/shape_circle.xml | 9 - app/src/main/res/drawable/shape_ellipse.xml | 18 -- .../drawable/shape_rectangle_rounded_8.xml | 8 - .../res/drawable/shape_refresh_button.xml | 15 - .../res/drawable/shape_success_circle.xml | 8 - .../layout_onboarding_container_bottom.xml | 106 ------- .../main/res/layout/btn_expand_collapse.xml | 24 -- .../res/layout/buy_with_googlepay_button.xml | 31 -- .../layout/dialog_onboarding_address_info.xml | 132 --------- .../res/layout/fragment_onboarding_main.xml | 41 --- .../res/layout/fragment_onboarding_wallet.xml | 275 ------------------ .../res/layout/item_backup_info_adapter.xml | 37 --- .../res/layout/layout_backup_access_code.xml | 45 --- .../layout/layout_backup_access_code_info.xml | 136 --------- .../layout_backup_access_code_submit.xml | 68 ----- .../layout_onboarding_buttons_add_cards.xml | 46 --- .../layout_onboarding_buttons_common.xml | 32 -- .../layout_onboarding_container_bottom.xml | 111 ------- .../layout_onboarding_container_top.xml | 148 ---------- .../res/layout/layout_onboarding_main.xml | 39 --- .../layout_onboarding_manage_tokens.xml | 5 - .../layout/layout_onboarding_seed_phrase.xml | 5 - .../main/res/layout/layout_pseudo_toolbar.xml | 24 -- app/src/main/res/layout/layout_warning.xml | 40 --- .../res/layout/layout_warning_card_action.xml | 69 ----- .../layout/lp_onboarding_create_wallet.xml | 142 --------- .../main/res/layout/lp_onboarding_done.xml | 137 --------- .../layout/lp_onboarding_done_activation.xml | 142 --------- .../lp_onboarding_done_activation_twins.xml | 141 --------- .../res/layout/lp_onboarding_topup_wallet.xml | 141 --------- .../lp_onboarding_topup_wallet_twins.xml | 141 --------- .../res/layout/test_leapfrog_fragment.xml | 133 --------- .../main/res/layout/test_leapfrog_widget.xml | 28 -- .../main/res/layout/view_bg_twins_welcome.xml | 49 ---- app/src/main/res/layout/view_confetti.xml | 13 - .../main/res/layout/view_onboarding_card.xml | 10 - .../res/layout/view_onboarding_progress.xml | 13 - .../view_onboarding_refresh_balance.xml | 49 ---- .../res/layout/view_onboarding_tv_balance.xml | 56 ---- app/src/main/res/menu/menu_onboarding.xml | 8 - app/src/main/res/values/colors.xml | 35 --- app/src/main/res/values/dimens.xml | 40 --- app/src/main/res/values/styles.xml | 142 +-------- core/ui/src/main/res/drawable/ic_heart_24.xml | 9 - .../src/main/res/drawable/ic_infinity_24.xml | 5 - .../src/main/res/drawable/ic_plus_mini_28.xml | 10 - .../src/main/res/drawable/ic_question_24.xml | 9 - .../main/res/drawable/img_card_with_ring.webp | Bin 58862 -> 0 bytes 78 files changed, 2 insertions(+), 3553 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt delete mode 100644 app/src/main/res/color/selector_btn_content_primary.xml delete mode 100644 app/src/main/res/color/selector_btn_content_secondary.xml delete mode 100644 app/src/main/res/color/selector_btn_primary.xml delete mode 100644 app/src/main/res/color/selector_btn_secondary.xml delete mode 100644 app/src/main/res/color/selector_chip_shop.xml delete mode 100644 app/src/main/res/color/selector_chip_shop_text.xml delete mode 100644 app/src/main/res/color/selector_chip_stroke.xml delete mode 100644 app/src/main/res/color/selector_chip_text.xml delete mode 100644 app/src/main/res/color/selector_edit_text.xml delete mode 100644 app/src/main/res/color/selector_edit_text_secondary.xml delete mode 100644 app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png delete mode 100755 app/src/main/res/drawable-xhdpi/googlepay_button_no_shadow_background_image.9.png delete mode 100755 app/src/main/res/drawable/buy_with_googlepay_button_content.xml delete mode 100644 app/src/main/res/drawable/card_placeholder_white.xml delete mode 100755 app/src/main/res/drawable/googlepay_button_no_shadow_background.xml delete mode 100755 app/src/main/res/drawable/googlepay_button_overlay.xml delete mode 100644 app/src/main/res/drawable/ic_access_code.xml delete mode 100644 app/src/main/res/drawable/ic_angle_bracket_up.xml delete mode 100644 app/src/main/res/drawable/ic_arrow_refresh.xml delete mode 100644 app/src/main/res/drawable/ic_baseline_arrow_back_24.xml delete mode 100644 app/src/main/res/drawable/ic_dot.xml delete mode 100644 app/src/main/res/drawable/ic_selected_dot.xml delete mode 100644 app/src/main/res/drawable/img_onboarding_success.xml delete mode 100644 app/src/main/res/drawable/progress_rounded_shape.xml delete mode 100644 app/src/main/res/drawable/selector_tabs_dots.xml delete mode 100644 app/src/main/res/drawable/shape_circle.xml delete mode 100644 app/src/main/res/drawable/shape_ellipse.xml delete mode 100644 app/src/main/res/drawable/shape_rectangle_rounded_8.xml delete mode 100644 app/src/main/res/drawable/shape_refresh_button.xml delete mode 100644 app/src/main/res/drawable/shape_success_circle.xml delete mode 100644 app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml delete mode 100644 app/src/main/res/layout/btn_expand_collapse.xml delete mode 100755 app/src/main/res/layout/buy_with_googlepay_button.xml delete mode 100644 app/src/main/res/layout/dialog_onboarding_address_info.xml delete mode 100644 app/src/main/res/layout/fragment_onboarding_main.xml delete mode 100644 app/src/main/res/layout/fragment_onboarding_wallet.xml delete mode 100644 app/src/main/res/layout/item_backup_info_adapter.xml delete mode 100644 app/src/main/res/layout/layout_backup_access_code.xml delete mode 100644 app/src/main/res/layout/layout_backup_access_code_info.xml delete mode 100644 app/src/main/res/layout/layout_backup_access_code_submit.xml delete mode 100644 app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml delete mode 100644 app/src/main/res/layout/layout_onboarding_buttons_common.xml delete mode 100644 app/src/main/res/layout/layout_onboarding_container_bottom.xml delete mode 100644 app/src/main/res/layout/layout_onboarding_container_top.xml delete mode 100644 app/src/main/res/layout/layout_onboarding_main.xml delete mode 100644 app/src/main/res/layout/layout_onboarding_manage_tokens.xml delete mode 100644 app/src/main/res/layout/layout_onboarding_seed_phrase.xml delete mode 100644 app/src/main/res/layout/layout_pseudo_toolbar.xml delete mode 100644 app/src/main/res/layout/layout_warning.xml delete mode 100644 app/src/main/res/layout/layout_warning_card_action.xml delete mode 100644 app/src/main/res/layout/lp_onboarding_create_wallet.xml delete mode 100644 app/src/main/res/layout/lp_onboarding_done.xml delete mode 100644 app/src/main/res/layout/lp_onboarding_done_activation.xml delete mode 100644 app/src/main/res/layout/lp_onboarding_done_activation_twins.xml delete mode 100644 app/src/main/res/layout/lp_onboarding_topup_wallet.xml delete mode 100644 app/src/main/res/layout/lp_onboarding_topup_wallet_twins.xml delete mode 100644 app/src/main/res/layout/test_leapfrog_fragment.xml delete mode 100644 app/src/main/res/layout/test_leapfrog_widget.xml delete mode 100644 app/src/main/res/layout/view_bg_twins_welcome.xml delete mode 100644 app/src/main/res/layout/view_confetti.xml delete mode 100644 app/src/main/res/layout/view_onboarding_card.xml delete mode 100644 app/src/main/res/layout/view_onboarding_progress.xml delete mode 100644 app/src/main/res/layout/view_onboarding_refresh_balance.xml delete mode 100644 app/src/main/res/layout/view_onboarding_tv_balance.xml delete mode 100644 app/src/main/res/menu/menu_onboarding.xml delete mode 100644 app/src/main/res/values/dimens.xml delete mode 100644 core/ui/src/main/res/drawable/ic_heart_24.xml delete mode 100644 core/ui/src/main/res/drawable/ic_infinity_24.xml delete mode 100644 core/ui/src/main/res/drawable/ic_plus_mini_28.xml delete mode 100644 core/ui/src/main/res/drawable/ic_question_24.xml delete mode 100644 core/ui/src/main/res/drawable/img_card_with_ring.webp diff --git a/app/src/main/java/com/tangem/tap/common/DialogManager.kt b/app/src/main/java/com/tangem/tap/common/DialogManager.kt index d60fbd6473..814384c569 100644 --- a/app/src/main/java/com/tangem/tap/common/DialogManager.kt +++ b/app/src/main/java/com/tangem/tap/common/DialogManager.kt @@ -58,8 +58,6 @@ class DialogManager : StoreSubscriber { messageRes = R.string.nfc_error_unavailable, context = context, ) - is AppDialog.AddressInfoDialog -> AddressInfoBottomSheetDialog(state.dialog, context) - is AppDialog.TestActionsDialog -> TestActionsBottomSheetDialog(state.dialog, context) is OnboardingDialog.WalletActivationError -> WalletActivationErrorDialog.create(context, state.dialog) is WalletConnectDialog.UnsupportedCard -> SimpleAlertDialog.create( diff --git a/app/src/main/java/com/tangem/tap/common/TestActions.kt b/app/src/main/java/com/tangem/tap/common/TestActions.kt index 955b89e867..5dc923cf58 100644 --- a/app/src/main/java/com/tangem/tap/common/TestActions.kt +++ b/app/src/main/java/com/tangem/tap/common/TestActions.kt @@ -1,16 +1,5 @@ package com.tangem.tap.common -import android.content.Context -import android.os.Bundle -import android.view.View -import android.widget.Button -import androidx.appcompat.widget.LinearLayoutCompat -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.tangem.tap.common.extensions.dispatchDialogHide -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.store - /** [REDACTED_AUTHOR] */ @@ -21,41 +10,4 @@ object TestActions { var testAmountInjectionForWalletManagerEnabled = false } -typealias TestAction = Pair Unit> - -class TestActionsBottomSheetDialog( - private val appDialog: AppDialog.TestActionsDialog, - context: Context, -) : BottomSheetDialog(context) { - - @Suppress("MagicNumber") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(createContentView()) - behavior.state = BottomSheetBehavior.STATE_EXPANDED - behavior.peekHeight = 400 - setOnCancelListener { - store.dispatchDialogHide() - } - } - - private fun createContentView(): View { - val actionButtons = mutableListOf() - appDialog.actionsList.forEach { (actionName, action) -> - val view = Button(context).apply { - text = actionName - setOnClickListener { - action() - cancel() - } - } - actionButtons.add(view) - } - - return LinearLayoutCompat(context).apply { - orientation = LinearLayoutCompat.VERTICAL - actionButtons.forEach { addView(it) } - } - } -} \ No newline at end of file +typealias TestAction = Pair Unit> \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt index 1188cd7d03..8c9f98ebdd 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppDialog.kt @@ -2,9 +2,6 @@ package com.tangem.tap.common.redux import com.tangem.common.extensions.VoidCallback import com.tangem.domain.redux.StateDialog -import com.tangem.tap.common.TestAction -import com.tangem.tap.domain.model.Currency -import com.tangem.tap.domain.model.WalletAddressData import com.tangem.wallet.R /** @@ -18,15 +15,6 @@ sealed class AppDialog : StateDialog { val onOk: VoidCallback? = null, ) : AppDialog() - internal data class AddressInfoDialog( - val currency: Currency, - val addressData: WalletAddressData, - ) : AppDialog() - - data class TestActionsDialog( - val actionsList: List, - ) : AppDialog() - data class RemoveWalletDialog( val currencyTitle: String, val onOk: () -> Unit, diff --git a/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt b/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt deleted file mode 100644 index 3996e28d5c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/toggleWidget/RefreshBalanceWidget.kt +++ /dev/null @@ -1,118 +0,0 @@ -package com.tangem.tap.common.toggleWidget - -import android.view.View -import android.view.ViewGroup -import android.view.animation.* -import android.widget.ViewSwitcher -import com.tangem.tap.common.entities.ProgressState -import com.tangem.wallet.R - -/** -[REDACTED_AUTHOR] - */ -@Suppress("MagicNumber") -class RefreshBalanceWidget(root: ViewGroup) : ViewStateWidget { - - var isShowing: Boolean? = null - private set - - override val mainView: View = root.findViewById(R.id.btn_refresh_balance) - - private val viewSwitcher: ViewSwitcher by lazy { mainView.findViewById(R.id.switcher_refresh_arrow_to_progress) } - - private val arrowView = mainView.findViewById(R.id.imv_arrow_refresh) - - private var progressViewAnimation: Animation? = null - - init { - viewSwitcher.inAnimation = AlphaAnimation(0f, 1f).apply { - duration = 400 - interpolator = LinearInterpolator() - } - viewSwitcher.outAnimation = AlphaAnimation(1f, 0f).apply { - duration = 400 - interpolator = LinearInterpolator() - } - } - - override fun changeState(state: WidgetState) { - val progressState = state as? ProgressState ?: return - - val currentStateByView = getState() - when { - currentStateByView == progressState -> return - currentStateByView == ProgressState.Done -> if (progressState == ProgressState.Error) return - } - - mainView.isClickable = currentStateByView == ProgressState.Done || currentStateByView == ProgressState.Error - animateState(progressState) - viewSwitcher.showNext() - } - - @Suppress("MagicNumber") - private fun animateState(state: ProgressState) { - when (state) { - ProgressState.Done, ProgressState.Error -> { - progressViewAnimation?.cancel() - progressViewAnimation = null - } - ProgressState.Loading -> { - progressViewAnimation = RotateAnimation( - /* fromDegrees = */ - 0f, - /* toDegrees = */ - 360f, - /* pivotXType = */ - Animation.RELATIVE_TO_SELF, - /* pivotXValue = */ - 0.5f, - /* pivotYType = */ - Animation.RELATIVE_TO_SELF, - /* pivotYValue = */ - 0.5f, - ) - progressViewAnimation?.duration = 700 - progressViewAnimation?.interpolator = AccelerateInterpolator() - progressViewAnimation?.repeatCount = -1 - progressViewAnimation?.let { arrowView.startAnimation(it) } - } - else -> {} - } - } - - fun show(show: Boolean) { - if (show == isShowing) return - - isShowing = show - if (show) { - mainView.startAnimation(ShowAnimation()) - } else { - mainView.startAnimation(AlphaAnimation(1f, 0f).apply { fillAfter = true }) - } - } - - private fun isArrowRefreshActive(): Boolean = viewSwitcher.currentView.id == R.id.fl_arrow_refresh_container - - private fun getState(): ProgressState = if (isArrowRefreshActive()) ProgressState.Done else ProgressState.Loading -} - -@Suppress("MagicNumber") -class ShowAnimation : AnimationSet(true) { - init { - addAnimation( - ScaleAnimation( - 0f, - 1f, - 0f, - 1f, - Animation.RELATIVE_TO_SELF, - 0.5f, - Animation.RELATIVE_TO_SELF, - 0.5f, - ), - ) - addAnimation(AlphaAnimation(0f, 1f)) - duration = 300 - interpolator = AnticipateOvershootInterpolator() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt b/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt deleted file mode 100644 index df54352380..0000000000 --- a/app/src/main/java/com/tangem/tap/common/ui/AddressInfoBottomSheetDialog.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.tangem.tap.common.ui - -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.widget.Toast -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.tangem.core.analytics.Analytics -import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.extensions.* -import com.tangem.tap.common.redux.AppDialog -import com.tangem.tap.domain.model.WalletAddressData -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import com.tangem.wallet.R -import com.tangem.wallet.databinding.DialogOnboardingAddressInfoBinding - -/** -[REDACTED_AUTHOR] - */ -internal class AddressInfoBottomSheetDialog( - private val stateDialog: AppDialog.AddressInfoDialog, - context: Context, -) : BottomSheetDialog(context) { - - var binding: DialogOnboardingAddressInfoBinding? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = DialogOnboardingAddressInfoBinding - .inflate(LayoutInflater.from(context)) - setContentView(binding!!.root) - behavior.state = BottomSheetBehavior.STATE_EXPANDED - setOnCancelListener { - store.dispatchDialogHide() - binding = null - } - } - - override fun show() { - super.show() - Analytics.send(Token.Receive.ScreenOpened(stateDialog.currency.currencySymbol)) - showData(data = stateDialog.addressData) - } - - private fun showData(data: WalletAddressData) = with(binding!!) { - pseudoToolbar.imvClose.setOnClickListener { - dismissWithAnimation = true - cancel() - } - imvQrCode.setImageBitmap(data.shareUrl.toQrCode()) - tvAddress.text = data.address - btnFlCopyAddress.setOnClickListener { - Analytics.send(Token.Receive.ButtonCopyAddress()) - - Toast - .makeText(context, R.string.wallet_notification_address_copied, Toast.LENGTH_SHORT) - .show() - - store.inject(DaggerGraphState::clipboardManager).setText(text = data.address, isSensitive = true) - } - btnFlShare.setOnClickListener { - Analytics.send(Token.Receive.ButtonShareAddress()) - store.dispatchShare(data.shareUrl) - } - val blockchain = stateDialog.currency.blockchain - tvReceiveMessage.text = tvReceiveMessage.getString( - id = R.string.address_qr_code_message_format, - blockchain.getCoinName(), - blockchain.currency, - blockchain.fullName, - ) - } -} \ No newline at end of file diff --git a/app/src/main/res/color/selector_btn_content_primary.xml b/app/src/main/res/color/selector_btn_content_primary.xml deleted file mode 100644 index 70f6aded83..0000000000 --- a/app/src/main/res/color/selector_btn_content_primary.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/color/selector_btn_content_secondary.xml b/app/src/main/res/color/selector_btn_content_secondary.xml deleted file mode 100644 index 41aa7c843c..0000000000 --- a/app/src/main/res/color/selector_btn_content_secondary.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/color/selector_btn_primary.xml b/app/src/main/res/color/selector_btn_primary.xml deleted file mode 100644 index 6e8aab4c79..0000000000 --- a/app/src/main/res/color/selector_btn_primary.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/color/selector_btn_secondary.xml b/app/src/main/res/color/selector_btn_secondary.xml deleted file mode 100644 index 47a88e3645..0000000000 --- a/app/src/main/res/color/selector_btn_secondary.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/color/selector_chip_shop.xml b/app/src/main/res/color/selector_chip_shop.xml deleted file mode 100644 index 052f962892..0000000000 --- a/app/src/main/res/color/selector_chip_shop.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_shop_text.xml b/app/src/main/res/color/selector_chip_shop_text.xml deleted file mode 100644 index 5d5afc33ff..0000000000 --- a/app/src/main/res/color/selector_chip_shop_text.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_stroke.xml b/app/src/main/res/color/selector_chip_stroke.xml deleted file mode 100644 index c2827e783e..0000000000 --- a/app/src/main/res/color/selector_chip_stroke.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/selector_chip_text.xml b/app/src/main/res/color/selector_chip_text.xml deleted file mode 100644 index e205f4e82d..0000000000 --- a/app/src/main/res/color/selector_chip_text.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/selector_edit_text.xml b/app/src/main/res/color/selector_edit_text.xml deleted file mode 100644 index 192514f802..0000000000 --- a/app/src/main/res/color/selector_edit_text.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/selector_edit_text_secondary.xml b/app/src/main/res/color/selector_edit_text_secondary.xml deleted file mode 100644 index df06469793..0000000000 --- a/app/src/main/res/color/selector_edit_text_secondary.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png b/app/src/main/res/drawable-hdpi/card_placeholder_wallet.9.png deleted file mode 100644 index 97ea9bc26022736fe8581a8047e5c6e03d91bd27..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172085 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

*avus*MA16=s}*} z&8mj)7mTN)A}|wDROsoKIa;_(Wv><jiOlAmXS4=6$NF=NCg0n?Et zAkb1BqCvY`SE7yFy`Q!R`L;xbMof(`&gkgU`s{hiGe$j$-yebkHF77ea&o4oR0Q-V zMmHmvJr@R;yzkNV*f32^^8`(34+~CcrdGVSG1BR%ww-_MtOM9)*k#TG;HXuh21&BB z4}8Q+P$lk0)0&KhB+KlJ{r0SC5ipApFwux%@FnZ42lhFSo81%6Ka#*A2F%yOG?Ag0bUvGk;*t{e4XoJa<-~dJ_N6#MGogLPzDg5(1hVx~Z7yGHhG~Uz04Fr$F~)yahGuR`jD|6YKXb?B(vyu3ay= z6Rw>P#k^i;e;jt`*H*~n+W2eX>@$dN<||Hr4a3uPNgR<%@JzCHM5|u=40$IOeX?(I zQmIKQ85KAVCKtV6^k>lfG#aSJYCIU`XY^O=oJ5*FQw#IatrWxWcnsrcLlndQFW%x@ z2CCI!8kX5)8vEg?N%xhzsKTkl{7-uY(yq<7SE0BjP>QCG_UTj0D#0$-pfFLNCh4b2 zmlfi(d8s7oqO|~L*_%3^Gyfpiq1)XsVeI!F6DKD_|1a|_$Puf^DRBA)cy_SN`R8ZJ z6MpP2%S*-XqFt*N?BCrb6ijLT#pp z05{uwH?#PBdw^+t;Bh(zNDy9+I2+!&zqj^6e`JFjEqaW>yJ~(oc4f2HQSokX%E-e} zld(r3w`xi#eU*Ww0-!|1?Kk>+BCfW%CQJtblA z?E6Ci)q9-P!mH~_&92EfPUEwgx%J@V91(mEbxb)K+r~(Np z*=nsAiVZV2^0K6c!oS&iP!Rypb{KbF-rB<6ru(1y>l(3&6%x0-gTn6ct&H}kvp5bDW4(>Ty zdNX=tJU#}=rb_&^9qYJg|owV>@=|Swu9!Sn2_sl-Z zf;!TF`2GW$0~8f#!VU)Z-Av!Yi@%HpNfo+kT7=Ur^h+A3g+S{Jza5g9yPZywoF)V} zxxPNh&U!u89-o~gY}D~#1p?yQk^v^gey|%*Z|4iW0)@^N(8GH4u(MB;XKOX3B&Qp! z@*iJh{<)7|l}0QgtK3?7(``ei!^gXvTIfcAp6bX00XOHo@`4mHap|+z{xX6mqpsf6 zLf#N5>VA+Ph>g3KiE<}zYuZ>^UVf$7&#c1|8ben1ko7!yfQS4%z>qqzlFT4uGx0-F zv2}ccuL-8DfBqfqmb|&OW@M1+7dqujgbd{`aq-k5wY3gcW1gF6>sF<^F1Xb=|NZg% z#EcCLYCe8`B2DwrR25LOv21B+zw6!>&Ha!l-M+`E#*Cm$CB3T9GYcqFDCSMtHb0Fm&8MR#0j<5BJ?Oq$qd* z(+UeJ+?EO0-b`%f0dyj^xmQ|X0Z@=5>Ylo7z$$hHBmV^^@5pIq@;W??2QfCMFmb@M z?W9#+*w(!de}?Q#noaDq5=wthJDFf5cazgB&GuyDyj}>S)~OL^1C~P;x1ZR}OfrM%B7*0*Udz6gtxl6yy}~^Fi?OP0r`JIH^BZZM-TU`x zT91l-XmyZ}PbJ{yTivf~3Qet0;75{UkIa@r)a;$@*wfZwAlo0kPV?YQ5w)qYhR!cA z+|twcOItFY$vp*_m%P7UEOCMcyNXHnozUdMnO5)Q*8_}7s!3$3#(eO>Sn4>Prkyb> zDG5XQS7}}~K14ljEp-|#@fwR3pRd%;pIl|jSB5L+)zr1=N7i_q3b_10lV%ejrW8I5Z!TW4U za+U&*M5lRYheDY;a%k|*im~bZCb`VT5#6=Z?G4R`S~y^!PfLBbrIz*2YI%AmEEJ{0 z)eplO*r*m%e3ofu?fNB^lw@t;en>`Yww080#6*C5vXl-7FwdF0(w240Sx+ojv&k~~ z&S;KX4}-RYv<@F0f%ripm^xwRArHKEc~sBA=HsajSqF_n4M2BCT3U|xJ|bJQl~O1! zJ|R3$q(8ws>HQ_(w%bf0RI<(yU!dWV><>}M!<}a;=W3bxRZ_LC=eEJRN7++T^H)XE z{^dGMK43TYnd0Cw%nbqG%*;$Py)I4P07*V96E&P%ylXjEe8Lj|mja?Dz+Fb(Vc2bC zP$gXO5>vp#!5PNUFs>ZgnfX<-956;#?laCLrX~!Ggze^8NJ-1)pOowGjg>+q3UG62 z8LC~iuR7R243^;|9e@$Q28=HZ#XHndtY3LubA=u&?Sbeh5)HD*mE3m`fe?6 zLIP8zs-po)Vbrvig5470w;#6cR8$XBh1#l}4t9z*Q7tQI^l6o_O6gFFN+NhQ$X9O-Vf`%TklG5uWLj&n6<%{nDgza8X|t*N4I%=vSg zdysRebs%7jGEi%%2CJ4_zr!~0=U?1$_TXSRml~@s*G)%wh2rjtNOyY!k^lB855RPeH9L zRp6@f`Yz))BKaErof$Thva-foL}}M)?`{fNLaTuHu!pqqXTT?T;=VE$sj5E?$4V9U z?f#R8wq6ZHl+oN=f_-c^{kEscZXIwu#aBSe$S6t&iL$U$KmK~ZCV`q|e#RDdV6R*c z#S@M1y*k%pD1;y-R-V7D&)VHF5iXh%KL6}#O9V2geYx_$s}&~noSr&9iX(mAHS{D@ zUi1EMv<$PuuKsY(4Y!*&1C+beykWiCx(qTg6~GLRZh$N6ORqZ|_uPROPZ0a-vi$>+ z`F@f*aepo!&X;B89-b0gYW3wb#JN-)nU(`Fz*sKpo3Wgfa^k?^=IFn-jSi)8m5&@wbiP$*yb`vb(fh9b*nL3dwvLnd@HP*)_&Jvnm( zx8IBe8vN@Mer3uSu;o07J!dXw1m9~7!*8ODjCX*yM85jB z#cAU4AaaJW>+>yl;@uabC#r^qVyy^lvq|nH9yorbM8)RCD+YEP6=KG2sy5(!omN=$IKxmiyHGS*!>^SZ=&BHwC{!~A7al1u-A3Dl3Xlgmc}#!k)ENao9_KzG zGUV^peiS3$B z1Sl36gz02o<4XM6IIYw&_Qz7wILp*BeoMz@c0CW-r70gyMqda%GAlJ5jx3A)A_wV=>$O<*-B@5<-GHe6Q!r>54 zl?rS?1r{Yvz+Id-6Fu7)uB31q#@y0iw6_y$xJ}&@%CGg|2ElX7QQLYp@LHgX{(u&9 z!wSuI46s59w3D3{Rp1Emp~lre?FO&(`IQcz=M;-LD1%z$hs;7|9lneJqPNio+{|#> zh7&IhNsfSMyu)_u#x42ZVkr9f0^10L`s($DE$b_mhwWM#(QuWv`oM_9lGjo7+m z?`qGiZ!@tuoR3v*mrzMoY3zdu+))oM%`H9-s4*hW{028pLNA(-&9~>v&~KCnx$HHp z{;y9}Zwc;?MR@0;BqtQaR_wGR!T6FblyDL1Z;#rhBL!Mc;(!Hs|4&h03Bi+W`rW?% z<*r4=XX=j{m07QB(Tsl(f7B6bxm;Iqh9jDR^{R z|2wZXe~`s9O}Ux&H-JL=*VK|ayAea2+iHt**&GLM`YapLKZH>z4ogroEpC;kL=L+V z4q)ejs^{K+1s_F5UNuv^>USqPHE01CYhc=%BzurVqjGXK?IQLVQj?3#A5%M<5GwDE zPPs>)#o#jv0bjECK63k7`R}O!|B4dWt=Esd#FbAfkQr&(uCm>HR<7IfL-TndBTzgx!_kIzNuzMAE~v>y;Q>IEi>s)zBg+&! zfq|K0jq$Pwlxa<2Hq3Q(M37`o6)9RrE!UrkEbnCB?G7^fH)AV* zLGB(n7(3`JH;l*SheYxJDL$l2#d5yi4t8ZhYFSOhe z?ogQhs^PNBuu-UPLP;rw9y9w*io;$;yO|NzKF|2ve&lg28g5L8h!vV^$HpPI{LP2_ z8)AwO&_Bk+xwaDL6asy=t+2EFC_EDpkIfI;|4|#hur{rg*iM_!g9Iv*CYips+MAv- zXW@a>O1Hny?E(R{?O8f?ws%ra&vIp$XxX{--EH=&ATMy-K88KoJNI9TM5pK1=f*dA zIt_P!=vX46%p$)p!fUHmO!^&`vpho#M@DFYR-@kN<-6GnTxDEA{(E}Q9e-M>6wypy z*zOUliDMtg6<5;BOZYsN9vJ6 zI*H(|WYw}1ZNCGZ$lJkGvG9NQR-1L-9l4Y5(9jg!-P0%j%(>Mp^KzJ0E!wSfRCV6B zwv=_}Fn>T4t*wODaj6-(@5{`~VO9aMX+hNr99-O)P5l#k9hSiW$NUD4_X&oo{&FT| zXNnb=OAMy`RSpN3XN(M}Ew>c6Aw8%eYou+h*?ZwBj3uG#`%CNN#!Aha8C#vz{k!l& z0|BC>8QJOy(gm>qGdl(aD<82U+3j}K8tl}ndX2Ijw?DWFb=4BTJG?|*cAO&S(|}?T zH7j5N>M25L3l2H*97MJtBP!FTs9R0vd1z&Q-=3^?@|B>2P&ejr zh>cu^+ULUoc104sAC+fGOeR*EnBjbDJ*=6&$ z3@=TduHyE?|7EIoJ?n~%KEv+w@wJWT(qUXK`rI8d{_`u*Nz$i19rZ@eq0t&DeG))M z!o_>)KOcY!)kB&)n#UKPi9Q^+@TFKYE3W}{iJ)#_b>`$lX4iIk|WG10&mvbM;3uA0oGC2rSo2I4zLGclbi>;%YFa0yI zF1U$)6T|K2mrrvedO-}l`AKqn;N%@+N`4MN?e9G(9?tR)z(vs9Z&i4h9n=F%b8$(vCy`RRXB_o%^W2`29Od^5WK}3yls^?+U z)Uiro@L2YpiE^&0oBF`hgo-J;3nq7PI9-_};3RLZzvjA+co`kKNGEqkhrP#Ht+hH2 zmB$6GZoc(#kLP$J{QO##ZY{Lb2DZk2z(2iSYe_bJOrSdWQyBAdj9}MQH#WDx$ib`R zwe$t;CiC)AipM@`KWz{H4|FpL@x$G=$GwGt2NW@cr~0~vSjUD3t0>8wJuTG8IaDZr1+UWBEN1~Dl=YQ2;oBEBP-iz z=sPdm46J>tjlFiLkfJgPN7YJ83le(>VnH{i_TnlWD)Ae$U zH*xuPZ@0Yn^^NHP-l4$te8uKs>~BKRH^KKcw4zw1+p1#s*NLWzK~}Z&qA^^d-b|mp zZk{&;?=xgI>DXPZ5B%I3)?NS_zg@vAb6k*WZbT;R!02{7$@JS_bX3{s-P#M0$nJGJGcE6a%@H(^cYxWWwu7T8CUp=*j`QIhvVs^)8n01iS65xDfzp!6r_@v@*ys#dk zCvZ(Vb7=)Tv-gfWq%Zd=X@7@Hzr_egC?e%71+vjh~(UD`hl5L8nZV0BpTn5dOG(gul05i5yhGM^I~yhK`UU z@-}&iOp(N0u4%Qp_24koq*0lxWIxK?`I@b0FC$>L(egXEcGlxB8`~_S(272NcyOAG zKk%Ez;g(c!VWAW$<4s;p9!|y%dJXyKs6;9T#i2aVC5667Npmq)myYkdADo;UMs-?S z2Md=baHpeMl5w1YWj;aEQ*~kDIzavabtggTcQrA>J*q_#R1fH&tn@FJyv?#TDwMc< zR(-AMY=z5?pJ6jZ!(~iLYW#=I6#_>NSs3XL`g65X$8M^a6hRR0Jd&Qajvep&tV9b{-mpiG(dzo%>n|hfUG(`cBXyl~ zpVEXH?j)@g32YS7xIe)BgcmwI`_ZZ1mu@C_3B!CDC@m1XurNYo=)6T^?kFam6&8ez z1GRiUyx9Rk;Wi>t1K9FgjxG{bTc`Jz?ho&Ink!*7;qtv4z))+G1YK_fO|(n92b_Y8 zW|={a>%*C1ZO)|lLDUgEeu5rso~*S>RGW<5?5<`(#hK~ zT*cRxx<9@Fb-|0tr;F5pDDNhcxM8Mbvu{glIF9!GF99vc4I$a`LPerq@X8n1APZt} z#}kaX#)5ZiPBZ#K6|H(r^xV5}1;+D%mn00VPOO@zDNI@*?h|$G%JhyF{um zvyhy7qjOUE?H4D8gRqpE-aH21;(WdD5gog{^Yt$wqU2M3N@91&R70*}qZIO!%X#Ei zKU@QhS0Q@o%f%*XRLq2dAB47jl-J7snEJ$p!1H+5>!$C7+~}>yPk4W=SOlxj713Bj zK2-yh8%PH4Db$aT2}O~GTeeN{Yd%C!D?`19&zU^)UlTlJ7Fo}l6`dqf_p>_fo$|f? z{HwFvQ-1*-lFj&NXrwFe+Z>kZh1c6#k`HutgFp1VgkeR*lll8o&jR3hjvNO?;Kjx4 z&9{d24w_DzMAe#nvV85hhPt~AT?-awC9NGJO>6K9&@(=ZZ^jtV|6&L?fjeMmp_?s> zx<)6zKvmQM|K~SF(9Gc1?zKtPX&Y;2s%PMwM6y}33eTjzIa{h$B{eonvAzw^U(al` z+3*JsCF{}F_E_rS(I5}ET>M7gF;C1PaVtJiuLiAvtT<7^;xFG1;Req^%GOUW$oj*0 z)!QgoGXIMcHaS9@?t*vz2)0b&M9;hqnxIA+rpY%_UW<9F_f6dl4r zL*FyW@he>n8-Ts9#hOqN!()%#?&{{GHE{FOEs5^%^rAGYwGN55>*z<{7RjVzunikZ z?pO}?KfgXQpuv?XyaxZ&+>$?Lm_q2B&o`bFhZ|o=8ZGLzPqeX6V@+U`&)% zgutapkp(Ym+U53G``ClN!OGlm>m@N7A!s1&JFWIoXNvc6F)+mIoTaQ01}T7z1Bv)S1$0RdSxk}BNF_Z&RR(1Gu1oqNUnben!QKew$5S2)Waw(mg~@2ITV`{nT>~zvaHN5 zS|x)%aqI|&Fn9^&JtjN#l#reN(}BaNV@+4qvefh3mA#H!9eR;%K9zrP#l`>2+?QNd zaBjU(Ew|f8yR{;)mB#g4CA3ps{~dR5k{Yvq zGN;x8PEP*};aiMIRd4nH{-xwRo@#csy7q^MZUUK1PY@xCQhEqYbyiN=I;hSLHJaH+MQ3QaZEU~yOQ!Ah{t zD28(P!>4i?=u%D3(l?|V%SR9f{3@CUm#p!KUnAlGYs3!JisAMN*Ctoj9bI{ z)lI@?)+xyE_^=fxex&#x(m&k0ed+UURy~dOg@1+}@cl#lE;~uQ-Y4bDHM^zN4bJ5X zemc}t@4duhg8%hm^ zt{1CJwiSIrr0~LtOm5mgRl`hvX5e`t-2NJe3Bsb7$p){Th=vJtv?F+Om$+p0FWp_RN8iD+1Y+H1hKcgVnrcO~15>O`(x=kzNiXs1JEQh8KEQ|D^_;PLT0iTl6;oTRB&h8Q`-J zQ**$i5{kRHuvFt}m*ufCJFWgf)_%krZ8@HB*%#yo(qyWmIDe~}cZdJvj%gR9)mv%d z5hHlZxdwb4$CaS!Q)83=v_$Q97ff~D&yB3E`Gtg3e!(t3wE>9}m}WH1;I8WHJ30{= zYXq6NpCZP;AMf6;eA9%U5`{Q(y^m)Pj%~DM)X-byq~hSyzI!o zBiHR7+tjiYrc;x69u3!sceA!Zn7yxQWc&+OG{ZXY&KE^21)p>IayHq2KK4yWgJ~*J zEza~p)O8D@V;gEm2zT$HHKJ%DF&D;YxXbbGj=yU58g0J2K)%M}YMu3f+KwV`64>gz zPjQ`YZZ$RK!O_Pl2iywGG1>e!(WLQF1U@3#vwMh{RY6kn((DBEE6-#XoWAc9b|ETi zVS8)-Lsw_eVY9O)0-|TUc`Q}&tg$K&mz@v(kDcu%ZdaHlHb~}^#_!{a6v#G|E~e!- zJCoU5J+sq^G(+DwY&*f=5q8d%{+H3N7Xs;)?}|YV7ue5HeD7CRrzp`-3)P@-@W<X&7MREfU}fl+ z=_NO6uHqG~Vgodxzxx1B1m+LBBUZ{Lq#9R{Ua8^-Z8*HCh`%V{^4t{gitLc^@vwkO zWokT%sb^rO@D&2h@sbVLkQUSjZCOrmiR}C=8_E|Bi+)bK9nYlgW|?)?TE}EpNmPEW zvvL4!^HIl%r`gl~D?IKes~Kn}*bBhWhLSb=fSQ=LT#@_#}_F7|MFF zMFL~GrsuYh2W3DP8+s#>>oL7%Yeg~(C%Dl%qbEN5?TXlBfXei7XCQ>5_k&N;7-Jg> zWw2Lq6>OOBXk96?#Htil)}Ug#lLiafRqPh#3H`cF%2z>1=RT+ky`N5l}<<6`(D(m+C4 z0dmea&&fI99}nP0XbW)qD#Vw__-@0-m{08$6aBCz#h6qD_IPAe#7~R@r@%ws`_&mD z-E~{HlXno}z150?D3p`*7sa4JVF#a@FSkjtXvC;5|J1;40r(P0LyX*A$nw zaIlj}`hua#o6mH81O)B96HLN4UiEszoQ6uYxN@)8=F|6b#{A0QB#A^iS=eXQf?4#^ z3-%uIanW8l6v7htirvL*nw(XDiTjPN05Wt#o}+){rJ+ztBz(PWBcHyKVW~Lw z)@yrX?B3z_4bOOlJ5{<@US-fe__D5+T^wc!6H!J{{Yq3%F9ytBAX}v@5t1Nb<8YGg zbti}Zk@$$*f04qbj`K@}4|EwN@)RFJJo57y2<(ux8-jIH* zQJq@_+zH;C0qgarWG-EH7m}yymPbbg!E6Z8ySg9H0HDX>Zwf13Yp+mUNp8|VSmZcK z3|USUvS<`+Syq`$K2@FE zsn2K)@&`liZjf7wEB<@s+%KOH>i zH7%x9PT)S^B}W4NowF|TW6A*P%%mm6r198|Xv-90lGnxg=@-JV63qB%xEN4k$qQ1hdMTOF=+Y z)0RjnOQ?P8T4cSpvvF+YVi}h|MsWn!|Bh#il>vD6*pg7zD)c6|Hi{(@CF5SOOCeP{ z+PWrNWVuO^L>Fue!^bhq+c}ue3U#1(SqjG|nT~mgSe*do6ux9`|0!#I_A`zI17bPz ztKW98XOqRc7Q;xY`4&C{F9CD>7s?HBk#*dF=>!1pRSeBLiM+tpF2bLm8uYD&M}Cw` zcSPK~r7^|}a1Yzpi6!Idvxnn}+Fa4@FK%dl;`5HXt&i*PZ=nb8PvG~-IY}cLS?o{+ zbe?X;nP8jqljH-Tw^d)`1C^xZzppdQZ@;GK13ueVG&OB_nmV_X%7U_fm+gn*FT#EID`YIyRb8*?`+aOgcFsjq z$qrWqK%BAZ3Zl!jw3oqPSZw#PvOM^HfQv2J7j^z~t8^M?7m+`EC;PI#663skr@!FiH1y?1Qg+nEFz*Cumm+BL){7j}jjw6ynaH z@q&Dc7=puPJ`cf5+mFP}WSib~xm>3~ySxjseyHZp>U6%@UH488v?@1 z%$VQb7(BsL9-n+Z>6Uwp`Xx}~r&JvP4|9ncEql-=Ug*_rc#GYkRn47kV#yA8dsZdKSr5OLC2oCzVSi^bT7+@~52L zaNE^&y6AnxFmu1%g;yl)e5_dP*WJi>Inb7-Z8s^wa9f}f`&@9qoMp%H{x8~Yr1sb7 zP2Q_Nj9N&;2VMXELiV1W&RwOn*7wEfL;v^TN8W^%gPEUoeH6Xi)f zyVk}SM>_+D`en(jLT}J;7apZHpyA+I+TV}F)oxJje(;x%)SRGuea}acu4(<*o*R14 zj`kl`W`IqsEbxop6ZXtan1)@7cc#*B9W!T&zc zb;EnF?Ab>vfmZHiI`=L3@maldI}vbtUQ;O$^F56}A%G8aa>H5db61CF)5zz6eDbR9 zL$(C)pAJZ8gU9zfz|>ErC4kp{nJ;7K`sX^Y5hG2n!}OhecDLid1R;M#M~hn~E|1$9eW>z#^PhADqBulh&muoYIi8yztIU>$lR+*pU}g*15A# zHs-(S{2o=uQqu|HYG2nG-pgC{>p!$f*>4aLj>D{RSJi5Nu1-bRoM+UDqm!{p~aNU+2%jfp|`^jWv?b;{W{%AlRMA z7>)GQjkrX4db562mMEw6sIIhq@b{Jm7Q3cr#NI`KG2eP%%4t)X;7hb>b%-@0-`(ddIcEn9gD0n4xi!B@3L}3*1yI93Lm1 zoyqKP77eVPETFufin)k5g6&(k+Dv$n%sCTjKcIHA(!LieQ0cArcUjIn-j7*Q!gB$sUN;9(V*CQ%T2Gg8=<_(~ib#W1V`j z(4zFf!b>uZ$;f==Tx&94i9oMeSZ$u%v_T8x7sP{JXx-7x%~#C-_dou~rkeJE(wUX4 z!ljHhZlVQ~Ml(L3RuH_&!irVN&4tY_2`I3x5|A|6*0WSu@z$?eZDSWudzq}_s@PY?d!wV#?=~Sxei=lV{8~q|N$0B?KYIJ?=|!EVS!!wmp$tC9#fSfyDy= ze{(w24W_EMcs5y3upYa;;C2QiGS;Vpn^+;4L^d7GSFcPOM;C)bg`V3bwY7HMAyAHI z)B&|V4yunn7RmiZ0`Q209l8YS8}sH?m}N@lQZ&23v?}``iK}kPgNgUDvaH+;%VSH@4CXLA7L&aZ zjI<4tc^p(4JT>%_GZrTEddYJqdm!2#cnlol!Wg_Ub?%MTO6N0H1Ja&-Z2b+|nq^q;r+*P1%JQiyu z*ZlS+%~k*KjaO}!zreYTExVnk;;{kas1x@YwF!XVyg8K$+}>!Od-fT#Yxjrde}3gJ zHP`o!o|OQ+Mb!(<(nX6*nX;@BRDMi>)=2mPkOTW_^oK5ZGX$-|1Dkvo+$zsD;W2Pb zPxOo12;UDEZmf8<@z}$ilE>l!MHvEcHI;=V9e?=${O+O0P z+RcG4J|LxyfGky5quf|%W2b^VbG0EF)H9msZdiZ>8$N(Rsy1-?r%hOMzq7*zS2A87 zJm|M-FkKOPR-(P{JV#4vY7IfTHx#%U>(uJB5o!op!*d0e_I-5NX50%_Pv#)zA}q|| z>cKfHWN|DmDo~6_r{;lRXPe&72)wHm@GF@;+x*|Z{|7eJd#D;s#%3hcwI<_pzXS@ry3k% z?Zj=2-XqrQxpKWhjd=Q*Eu-Mp8E`y2JVYEfL$nHL1c2g8SFV^7$4|*p{H~8HDT{;u zL&ke7t2tgPuV?LvaN6 zJKS-2$5`N?JMbCLYczb^NXB}0W|pa`u96bWg_ujXk;I4Wbju!E1`i)QZdEWs)ywj4 z)ep2c9609t6-$*XJOc5V^N4*4_q+!m zJjqWdy9a`TV;y`~g@X`3J#C0OP&;4wzT|`P=NgRB z5sg{e>E^=y<3Dt|nOGC`e$JMaZR6TCmXrED$u&BGlo?8Nf>*_IG{va}G@y4}f4}tt-i%oS#JT3_CpTMd`y(e#n(rxX zw^Qf~#_b#d=J)sQbv}OdRhtKna|)oBzXztzd?av9VdJwwlpied*R?@xY`!R<+-=oO z*eE9I^RJ3e31!OZ+jxBSty!AO{l-8%*3+wX3JQ+t3>F<+xKJx_>m=+$IL4jrmAeXE zXF!#+K#%d0n0(EHK_)Ez5b?)KuypYvvq1C-vK0CktcBOF-!$jy8*KX5djbgTIwLLU zDCg(pTB}>6fq#*nJD!g7DUbjz6yz91|HM-pBB@KSw8(9?JUye)jDtwdJ=wcDThH9vJLu$JHW0x^fR z(4<)P2LhaMMGudJmNp|@GnT(I=NqgxaID(ST*7(Da8^*5XWn}6{U|Kn+WLf{Nnq(F z(N9BTqYdD~;(kx_b78(H7SeH_u5r&Cwn%drR`}-2S4{nd3#MOSpTZ_#!5m5-|~w)=YYCk|t?>n=SA+pz$1!1whPZju?@2wG&Nard8p0Z9mS&j;JtTkx6H4cYx_4RE8BA88RO7_+V0)6 zYga-Uv66r=9*26ePJTTQ5*#zh5 zH#Pd)*t>KRFCwB|N@C{|`HYH&Tvq{_lG0_VKr zeuqy5LUvdz&)DXmiFJyPPv7~1ag{Wuw$6@|R=KTXS=i{7i88`w`}?9iMFUt#XhVHz zZ?Is*)ytP#iV1RP%mbX8Ey-!1a$xN0n zULpY8ZyGK(nS;lUT0_A6%1Q&!hca-dtxdEVd3-)LqQ>%ejQd=qvPRJ;KR3?;d?_a=)(++S2q_HQk&)-o@Rm-Hld4Z7Mg=c zj#|s-Sk?n;5^cKj^K;Ff z{f7+Fdt+_poPMX>s1SAcgx1WaFPNM0bVX7)miyhSKQ@Z|v38xtOu28PcQ4;Q zS8$Wr3{KXX5=ni-9;3FQaZn2rmQ~k+V?Cf)18|ukFVi~|SsLkh zy`jPC#RLg81Iq^!rkjgnSqD72C2%F!{bG{?5acgz7r-DDx8U_51t7@RkA1`f3NO`K z79ace@ek+=xV`+HID2OFfGk-lkxfKJpscvWY<+T*X==V~=~RGOtjR+U!7(ZWc+G-Y zZbwfT@CHlavPFvp$}7zN!#Dz%x^Gk$-`zu}L84_~|nW+`8ufm3>b- z`%D2ktdO*2DhgKn#j?I^ZM zTcpsCot~lZv6!bkwkhyG=VZL6Z=nyCYGb1(%F-r^qy%zCo12q+&RUQg&2`CiNcKAd z?81#9to}Zl(fBvHoubmbvuC%R2j-NNnZ=4X}BF+0^tD^opon1`5q$TT)y8cC19L5nPT z*KXY~tClS@Z}0lRoL0bCpB84+Kj8VpCoNV0)ZoC7RSk#)WI|QS76)|8rcE}mY}bb$ zTGw_sP!z8JXsf0o3wHdEt5kY;`hcJ;f2~~Sm|c7K8YmZc6`%)AVKNJCt`&6z4s4q= zmM@4d5$u`-lR+)mtGJ3#oAO(&cgzpodDkqftFzXAxYKaOn=W6nAm0HHv2G7@1lO83 zx6-5vkOR~h0|ZJRRd6Hp8}6YqSK^JTt66BZs`nl`6m}f?GUAm)6eMYxB$}5eTB!f^ z+c(Y4*4uW&htyw*s8?{o*%UIU+S=}#o44q}Wt{5-wGs#Uw4^Hki!h&NiZNY|aUXH91aGKm&h?E96;`WV(BbX@tfkm}P zfn2yj?ZmHtyRiTu08p8lblhA3ZmFqxmPejQ?SvCdU{*AR=dnY{E__m8autu0{=n~w zatka^^P<4lEd{Dl+NCE84FbTlVf9)Iaz`S#2FI7k3SC!KZLQl!jvu!vp`&SAuwwxV zY-IW4Jv58?;+MAExDk>|k7`ARlx{e7G7HqK1>y_CtG*?LkFyQ+qO3fg${cQam4b^e zD(J3V9Pm)!htCPfJKNYK%FBJT{-H7h8<@LUa?#{)#CUn2Iscw)zsV^l%fWRhO2x}%0hSnrn1nNfO%*aY%VTd zxop8Fx$%z4C827H5IrHPI*s(cTa4 zA#G#h$(@IwG}87$kLi*do`e$C0y$2{4t=#;Y8RaEMnIw+TNl-j0IbvJ z>%&?GoqEroJJ)7G!;Kv*nQ=7P$%FN=ZgZ9k(q93H7CxT9#z$F>f^jJUI`jRv$a&FS-?f(9M5B&o4PUQpOA;-lr}LX zvy08jrAurQp#t@V%hfp`>FDNKBC}5KMV5~k$exK}tP(3gvIOd1! zaB$r+u{_Wu=Eh>(XL?|LnYt2JKQ`38VKSnP4t)fG5IC`i`fZ|pj`es*3p19+Q10AB zYg1r-orNz;mO?CVcKik&6o>yNg)%A?(H7rEU}6@Qc@>p5X-4Cv=AiQ!UCBN<5d(pAJvJ{J3-B6Qj5T$dJqPw%3pkc#xX6nZ)>yA9 zSrwhaxS^klp=U@l19t;e{E z?{{`tiw*%@ZoQ9K*JvAoHf}Ct0!&wsC9+=;*r=wxD_8^z3-iq_1qzL)_;jpinxP1Q zP2lsxpdJYy8&v| zCMXnu4)%M0CJTK4@Z@jpij}s%(`sj0ZZnc*!MNvm2H>#H5p5z*L5rARF z@=$ap6CC^3hxw7s4S&#Z7Hf>5<4nds|3 zFW%j~$39De9MvE{8`np)w@j8(-1_I6e6)hGPK5LuX|T6#-fTXQr4viwL<(RN>+ABR zORbX6sdMMXv;+f27fK@UxM&4v7)MhqP|PAY?ol{){LEV^jq0HEcyPWOZ|$h8|oojHf=Pd z1xGp!8VJcj_;;km2{eU5GoEoB>2*RKM{pO$uTrt!&A&60rB10VDV5HZV5F*~&30HHxUKxIuD}|0|!-8>jqj`M0tu?5s zg41VYwF3wQf+>I&i6AzuTW5L%Cf<=|?_?{Xuw0TT1Bo^iAy`1EADm(WYVW8mfnW-% z6yX1J%U-q#K_&u@@wyOzwxq7sT0$ExG}=srPG5z3g^zqypcU(Bw2B>M%foX=;IMyT zoFfIty+Hw^rfPwuc&t;_y$*T8&~P8La$tDII?UCE>k-w}{~El9Zm?1t0s5H@$yI@C zz-zDqFMRLeL*~UDI}}8?%6zbIzpXnP@y_nZ*GI-Q_jbCyT=X$O_~6l_){2di2b2^5 zzYjFmSXv1i4gy$XRlpRFZiQlX<^(xU5*Wwrjfxl?^AHnRNw9>Q3kwfCKddnQYOmW0 zlOW0*q|8Q%#H|JEAZ^T$70J!unDJVobhZS|ju&)J)fj-oiC%gr%k5b8VC-W3ogwM1 zNYo^p^C3G1z`b@oG*1?Qf^ILa<`F;3C7;g|NUg12Xu%noEGGgwojQ&6igX(RaILb~ z;ReU;zPP5=1``4xsVi1^3IGmnF9}iTThbbn9OAx$j6;FDaMy=QV6C*W0K=VyV;hXd z5O|K-s%k5BMskY41SWyU2K)Db>OL!NroiswmNY)$UI0Gf=_0#E9}u{R`=1RE(yW(g!+=*6EpNOfw-k_hU6dEbo2~zNYN~#R2kz;@{r7YS#Bbvx z2FEnP1~tf4&E$-W(`|)^+>x#$Fi*2zSpH!mwx2cvq_VRnSMq7P`-XN}3%M3s+`}<< z2~KwdA6}mY8u>v0W4sq!;!u}JB|M(OqRqyRK($DNhDx=t!>1%7e=Lg^{m8T7PnFoQ zZL8eADb`v`dgiGpIoUa;QoLD$#yYxuR!BS`ST-+SzHH}S+B8#R8)$(l0$R4S^_TW= zyuYw>hm;@AnTE!TR_TNJ+P__)+JP6MwrZg%SHKp5i%plLHlcnWtBi7u1=4Q^Bm)`i zx6b!A)wcjsV8O+Y<)1c#BnH*q3*r#(JN%LT3~vHsi(tyD*ChR>%|tW>y3D#&tL(TU z$qE~dNC6*`ZD*C_WZM&)O-bo&n-wwA-}={aJiqb({bap|c0mS8{_vg<9Lg)o7nRCF zX*H=ePO#u}|Dr0Iqmw1s1M%7eQGsI~1Ts~j73oHB?A#<4-V80kOtRy#yg28H*H?#H zfs9U{?x|b%u5bls%)ojaIhcyqdls%<1k7StjQ33dT+S0`&W_u1%(KYmHBX$#NWd}H z;zhC=_lpvSF6~&w$A0vIvs?kx%^TO75v})V#)b<*8|%E$evd-JeSu<|aqc00AP^9> z3IIL|EYK?a3v>&td~AAA6soK&x3nUxtI!bO;SOk=z-_&!^Ry}<1W6*@|dsgic&$Jbjlh|A`6H5 za#{M+$db)bM!IFH1HphtB{6qXmd!{CiA+d;4X`8dj&T(YP~~@|Bu9g~uCCh3#S!=y z%{d~eXFMDOWraWH8N90~Ud&8QlcL8es|9^e_b6JgK_I_89@?3#L$U|xfvCVS69E(F zEOBz(9O%RZJX?zjoIW>#W2}vAZi(o18R9@2&wzU8WA zOLeUgbKs+oEZB@xMWn0c0eV>Rs%Fu_0(tN@z*E#oOOl+Z}O zg=)~%)1?h!hHvf`IL0ehCK)gI!Lzh+6q2)6*CNdaH*VfGr_P==_jEq!29ba~=u2c| zt8Z+yTFk-f7s+R+OCaC2dc_K*`7X7*?i=D<$_x(t0=BAUmC`wvPHWxkEcX}uS@Tp?>J&qkuG zsXT+eAiaAgjj^PAl`HFhnl?1sHa%gVQRpQk+Og$vXMp{fd&g!MN((fcXgTV>lPC87 z^Nb;*mn`wu15trvs!zJrw3)uZiSbS*@z)KH*2wbfG+vpoB3&RJJwtr|i^&aZBVGVE z#|pEs1N>)*3KZ{F)rpK4p2?hSCfWF=R@j9gh}%5cwsT*{dM*Kmc7v;awR4}0Q(&tRKB+;g-Fh&TDQ z$IhI36g2A^C}80I8yP7!83LXJEE0G}COfRlP!twvZeX4u_?rI13xkYjurjZr!U9WD zS4Tp@@IMFl5sUW%S%~M)t+e-y`%4fa!2Wh?WU_zM^%}v8vR+buK=0aBD=jy+Nm=@! z%Qyvk3fDrB0_%D6m8%nqOMoYxqw(|32fG!}x6EvN@(J_$n{U~TRwTp$Hf014vpI;i z|C}~W*Xw{blLYB;AECb>D@oFo0#Ps4uUc(^-;m@H2^AWl>EV)()&(`GEs62TJG;8=juwE->`;0WWqcX9%np@@RqTZ6WkYS+}16u zk=|{oc&UdY3RJAEsuBS8$&j6IzG#}o?Vak)KRV242{$w~TIva489pudm9*P{LcSkraRwia>P=a^xOSuC!dU!H~V0li-ve7pNak;Tb=7 z;erLq$k3sl(!Jo==bpqO2w;r6A}|C;kzlfk%0M!1QRsko z+zU7k(pX&c0HYSEQ{0owJ(99?$s*I;)nksHI%(+vk^Cl>#{jY_#G8dLY`MldxdjgD zJHRC4n6^h+7zZbn$?j=s9=|P%X5iT8oLe?-G B7Q|t7bpW(;4UPerGp?W}Vc8@i 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

6TlGCIj7&E8~fx1SIlFCJz`5dhBY>0ac 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 diff --git a/app/src/main/res/drawable/buy_with_googlepay_button_content.xml b/app/src/main/res/drawable/buy_with_googlepay_button_content.xml deleted file mode 100755 index a893cd0cac..0000000000 --- a/app/src/main/res/drawable/buy_with_googlepay_button_content.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/card_placeholder_white.xml b/app/src/main/res/drawable/card_placeholder_white.xml deleted file mode 100644 index d980dc5599..0000000000 --- a/app/src/main/res/drawable/card_placeholder_white.xml +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml b/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml deleted file mode 100755 index 9543c4bcb6..0000000000 --- a/app/src/main/res/drawable/googlepay_button_no_shadow_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/googlepay_button_overlay.xml b/app/src/main/res/drawable/googlepay_button_overlay.xml deleted file mode 100755 index 70c7a92271..0000000000 --- a/app/src/main/res/drawable/googlepay_button_overlay.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_access_code.xml b/app/src/main/res/drawable/ic_access_code.xml deleted file mode 100644 index d24768aa15..0000000000 --- a/app/src/main/res/drawable/ic_access_code.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/ic_angle_bracket_up.xml b/app/src/main/res/drawable/ic_angle_bracket_up.xml deleted file mode 100644 index 5311e45d76..0000000000 --- a/app/src/main/res/drawable/ic_angle_bracket_up.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/ic_arrow_refresh.xml b/app/src/main/res/drawable/ic_arrow_refresh.xml deleted file mode 100644 index 1acc645cec..0000000000 --- a/app/src/main/res/drawable/ic_arrow_refresh.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml b/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml deleted file mode 100644 index b33d1ca408..0000000000 --- a/app/src/main/res/drawable/ic_baseline_arrow_back_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_dot.xml b/app/src/main/res/drawable/ic_dot.xml deleted file mode 100644 index bef54d1532..0000000000 --- a/app/src/main/res/drawable/ic_dot.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_selected_dot.xml b/app/src/main/res/drawable/ic_selected_dot.xml deleted file mode 100644 index f747ab54ba..0000000000 --- a/app/src/main/res/drawable/ic_selected_dot.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/img_onboarding_success.xml b/app/src/main/res/drawable/img_onboarding_success.xml deleted file mode 100644 index 84574803ea..0000000000 --- a/app/src/main/res/drawable/img_onboarding_success.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/progress_rounded_shape.xml b/app/src/main/res/drawable/progress_rounded_shape.xml deleted file mode 100644 index 7be741d331..0000000000 --- a/app/src/main/res/drawable/progress_rounded_shape.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/selector_tabs_dots.xml b/app/src/main/res/drawable/selector_tabs_dots.xml deleted file mode 100644 index 8b1678434a..0000000000 --- a/app/src/main/res/drawable/selector_tabs_dots.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_circle.xml b/app/src/main/res/drawable/shape_circle.xml deleted file mode 100644 index 59a662d141..0000000000 --- a/app/src/main/res/drawable/shape_circle.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_ellipse.xml b/app/src/main/res/drawable/shape_ellipse.xml deleted file mode 100644 index ca6cad24b5..0000000000 --- a/app/src/main/res/drawable/shape_ellipse.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rectangle_rounded_8.xml b/app/src/main/res/drawable/shape_rectangle_rounded_8.xml deleted file mode 100644 index 4f20cb0e64..0000000000 --- a/app/src/main/res/drawable/shape_rectangle_rounded_8.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/shape_refresh_button.xml b/app/src/main/res/drawable/shape_refresh_button.xml deleted file mode 100644 index cade7f63f3..0000000000 --- a/app/src/main/res/drawable/shape_refresh_button.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_success_circle.xml b/app/src/main/res/drawable/shape_success_circle.xml deleted file mode 100644 index d6c04dd519..0000000000 --- a/app/src/main/res/drawable/shape_success_circle.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml b/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml deleted file mode 100644 index 131299d4d5..0000000000 --- a/app/src/main/res/layout-h680dp/layout_onboarding_container_bottom.xml +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/btn_expand_collapse.xml b/app/src/main/res/layout/btn_expand_collapse.xml deleted file mode 100644 index 8d9c220409..0000000000 --- a/app/src/main/res/layout/btn_expand_collapse.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/buy_with_googlepay_button.xml b/app/src/main/res/layout/buy_with_googlepay_button.xml deleted file mode 100755 index 2553181fd8..0000000000 --- a/app/src/main/res/layout/buy_with_googlepay_button.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/layout/dialog_onboarding_address_info.xml b/app/src/main/res/layout/dialog_onboarding_address_info.xml deleted file mode 100644 index 258b3fbbcc..0000000000 --- a/app/src/main/res/layout/dialog_onboarding_address_info.xml +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/fragment_onboarding_main.xml b/app/src/main/res/layout/fragment_onboarding_main.xml deleted file mode 100644 index b184df2cd2..0000000000 --- a/app/src/main/res/layout/fragment_onboarding_main.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_onboarding_wallet.xml b/app/src/main/res/layout/fragment_onboarding_wallet.xml deleted file mode 100644 index 98ad59717a..0000000000 --- a/app/src/main/res/layout/fragment_onboarding_wallet.xml +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/item_backup_info_adapter.xml b/app/src/main/res/layout/item_backup_info_adapter.xml deleted file mode 100644 index 46b359a8f1..0000000000 --- a/app/src/main/res/layout/item_backup_info_adapter.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_backup_access_code.xml b/app/src/main/res/layout/layout_backup_access_code.xml deleted file mode 100644 index f402774903..0000000000 --- a/app/src/main/res/layout/layout_backup_access_code.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_backup_access_code_info.xml b/app/src/main/res/layout/layout_backup_access_code_info.xml deleted file mode 100644 index 5095c1eca0..0000000000 --- a/app/src/main/res/layout/layout_backup_access_code_info.xml +++ /dev/null @@ -1,136 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_backup_access_code_submit.xml b/app/src/main/res/layout/layout_backup_access_code_submit.xml deleted file mode 100644 index 58b97cc2a6..0000000000 --- a/app/src/main/res/layout/layout_backup_access_code_submit.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml b/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml deleted file mode 100644 index a6fd01cc5b..0000000000 --- a/app/src/main/res/layout/layout_onboarding_buttons_add_cards.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_onboarding_buttons_common.xml b/app/src/main/res/layout/layout_onboarding_buttons_common.xml deleted file mode 100644 index 2e53801d0c..0000000000 --- a/app/src/main/res/layout/layout_onboarding_buttons_common.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/layout_onboarding_container_bottom.xml b/app/src/main/res/layout/layout_onboarding_container_bottom.xml deleted file mode 100644 index 42668c1561..0000000000 --- a/app/src/main/res/layout/layout_onboarding_container_bottom.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/layout_onboarding_container_top.xml b/app/src/main/res/layout/layout_onboarding_container_top.xml deleted file mode 100644 index d3708a8bfc..0000000000 --- a/app/src/main/res/layout/layout_onboarding_container_top.xml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_main.xml b/app/src/main/res/layout/layout_onboarding_main.xml deleted file mode 100644 index 29b34f187f..0000000000 --- a/app/src/main/res/layout/layout_onboarding_main.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_manage_tokens.xml b/app/src/main/res/layout/layout_onboarding_manage_tokens.xml deleted file mode 100644 index cf08eaf50b..0000000000 --- a/app/src/main/res/layout/layout_onboarding_manage_tokens.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_onboarding_seed_phrase.xml b/app/src/main/res/layout/layout_onboarding_seed_phrase.xml deleted file mode 100644 index 017730394f..0000000000 --- a/app/src/main/res/layout/layout_onboarding_seed_phrase.xml +++ /dev/null @@ -1,5 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/layout_pseudo_toolbar.xml b/app/src/main/res/layout/layout_pseudo_toolbar.xml deleted file mode 100644 index 361f8cbb40..0000000000 --- a/app/src/main/res/layout/layout_pseudo_toolbar.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/layout_warning.xml b/app/src/main/res/layout/layout_warning.xml deleted file mode 100644 index 9c19796c5b..0000000000 --- a/app/src/main/res/layout/layout_warning.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/layout/layout_warning_card_action.xml b/app/src/main/res/layout/layout_warning_card_action.xml deleted file mode 100644 index 3b9177f441..0000000000 --- a/app/src/main/res/layout/layout_warning_card_action.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - -