From 99dfa2a392f3a2acd7034e4608d05039ee413435 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Oct 2025 22:38:15 +0500 Subject: [PATCH 01/22] Updated on 2026-08-14 --- .../tangem/datasource/di/YieldSupplyModule.kt | 4 +- .../yieldsupply/DefaultYieldMarketsStore.kt | 10 ++-- .../local/yieldsupply/YieldMarketsStore.kt | 8 +-- .../DefaultYieldSupplyMarketRepository.kt | 8 +-- .../impl/main/entity/LoadingStatusMode.kt | 6 +++ .../impl/main/model/YieldSupplyModel.kt | 39 +++++++------- ...ieldSupplyTokenStatusFailureTransformer.kt | 17 ++++++ ...ieldSupplyTokenStatusSuccessTransformer.kt | 42 +++++++++++++++ .../active/YieldSupplyActiveComponent.kt | 16 +++++- .../entity/YieldSupplyActiveContentUM.kt | 1 + .../active/model/YieldSupplyActiveModel.kt | 22 ++++++++ .../active/ui/YieldSupplyActiveContent.kt | 52 ++++++++++++++++++- 12 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/LoadingStatusMode.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusFailureTransformer.kt create mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt index 8b174ebbc1..2b7cfecd0d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/YieldSupplyModule.kt @@ -4,11 +4,11 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi +import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse import com.tangem.datasource.local.yieldsupply.DefaultYieldMarketsStore import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.listTypes -import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -34,7 +34,7 @@ object YieldSupplyModule { persistenceStore = DataStoreFactory.create( serializer = MoshiDataStoreSerializer( moshi = moshi, - types = listTypes(), + types = listTypes(), defaultValue = emptyList(), ), produceFile = { context.dataStoreFile(fileName = "yield_markets_cache") }, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt index b282890523..7f4dbc5eb7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/DefaultYieldMarketsStore.kt @@ -1,21 +1,21 @@ package com.tangem.datasource.local.yieldsupply import androidx.datastore.core.DataStore -import com.tangem.domain.yield.supply.models.YieldMarketToken +import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull internal class DefaultYieldMarketsStore( - private val persistenceStore: DataStore>, + private val persistenceStore: DataStore>, ) : YieldMarketsStore { - override fun get(): Flow> = persistenceStore.data + override fun get(): Flow> = persistenceStore.data - override suspend fun getSyncOrNull(): List? { + override suspend fun getSyncOrNull(): List? { return persistenceStore.data.firstOrNull() } - override suspend fun store(items: List) { + override suspend fun store(items: List) { persistenceStore.updateData { _ -> items } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt index c78c131d7a..8857e6f40a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/yieldsupply/YieldMarketsStore.kt @@ -1,13 +1,13 @@ package com.tangem.datasource.local.yieldsupply -import com.tangem.domain.yield.supply.models.YieldMarketToken +import com.tangem.datasource.api.tangemTech.models.YieldMarketsResponse import kotlinx.coroutines.flow.Flow interface YieldMarketsStore { - fun get(): Flow> + fun get(): Flow> - suspend fun getSyncOrNull(): List? + suspend fun getSyncOrNull(): List? - suspend fun store(items: List) + suspend fun store(items: List) } \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt index 8abd538e91..641a04923e 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt @@ -27,18 +27,20 @@ internal class DefaultYieldSupplyMarketRepository( ) : YieldSupplyMarketRepository { override suspend fun getCachedMarkets(): List? = withContext(dispatchers.io) { - store.getSyncOrNull()?.enrichNetworkIds() + val cache = store.getSyncOrNull().orEmpty() + val domain = cache.map(YieldMarketTokenConverter::convert) + domain.enrichNetworkIds() } override suspend fun updateMarkets(): List = withContext(dispatchers.io) { val response = yieldSupplyApi.getYieldMarkets().getOrThrow() val domain = response.marketDtos.map(YieldMarketTokenConverter::convert) - store.store(domain) + store.store(response.marketDtos) domain } override fun getMarketsFlow(): Flow> = store.get().map { - it.enrichNetworkIds() + it.map(YieldMarketTokenConverter::convert).enrichNetworkIds() } override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketTokenStatus { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/LoadingStatusMode.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/LoadingStatusMode.kt new file mode 100644 index 0000000000..9480ba16ad --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/LoadingStatusMode.kt @@ -0,0 +1,6 @@ +package com.tangem.features.yield.supply.impl.main.entity + +internal enum class LoadingStatusMode { + Initial, + LoadApy, +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index d4c2c34325..829f65d97f 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -8,8 +8,6 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -20,8 +18,10 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.features.yield.supply.api.YieldSupplyComponent -import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM +import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode +import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusFailureTransformer +import com.tangem.features.yield.supply.impl.main.model.transformers.YieldSupplyTokenStatusSuccessTransformer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.DelayedWork import kotlinx.coroutines.CoroutineScope @@ -29,6 +29,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber +import com.tangem.utils.transformer.update import javax.inject.Inject import kotlin.properties.Delegates @@ -102,24 +103,20 @@ internal class YieldSupplyModel @Inject constructor( } } - private fun loadTokenStatus(cryptoCurrency: CryptoCurrency.Token) { + private fun loadTokenStatus(mode: LoadingStatusMode) { + val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return modelScope.launch(dispatchers.default) { - yieldSupplyGetTokenStatusUseCase(cryptoCurrency) + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken) .onRight { tokenStatus -> - val newState = if (tokenStatus.isActive) { - YieldSupplyUM.Initial( - title = resourceReference( - id = R.string.yield_module_token_details_earn_notification_title, - formatArgs = wrappedList(tokenStatus.apy), - ), - onClick = ::onStartEarningClick, - ) - } else { - YieldSupplyUM.Unavailable - } - uiState.update { newState } + uiState.update( + YieldSupplyTokenStatusSuccessTransformer( + tokenStatus = tokenStatus, + onStartEarningClick = ::onStartEarningClick, + mode = mode, + ), + ) }.onLeft { - uiState.update { YieldSupplyUM.Unavailable } + uiState.update(YieldSupplyTokenStatusFailureTransformer(mode)) } } } @@ -178,8 +175,10 @@ internal class YieldSupplyModel @Inject constructor( uiState.update { yieldSupplyUM } - if (yieldSupplyUM is YieldSupplyUM.Loading) { - (cryptoCurrency as? CryptoCurrency.Token)?.let(::loadTokenStatus) + when (yieldSupplyUM) { + is YieldSupplyUM.Loading -> loadTokenStatus(LoadingStatusMode.Initial) + is YieldSupplyUM.Content -> loadTokenStatus(LoadingStatusMode.LoadApy) + else -> Unit } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusFailureTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusFailureTransformer.kt new file mode 100644 index 0000000000..0154fa52ef --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusFailureTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.yield.supply.impl.main.model.transformers + +import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode +import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM +import com.tangem.utils.transformer.Transformer + +internal class YieldSupplyTokenStatusFailureTransformer( + private val mode: LoadingStatusMode, +) : Transformer { + + override fun transform(prevState: YieldSupplyUM): YieldSupplyUM { + return when (mode) { + LoadingStatusMode.Initial -> YieldSupplyUM.Unavailable + LoadingStatusMode.LoadApy -> prevState // TODO apply correct UI + } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt new file mode 100644 index 0000000000..42549b2060 --- /dev/null +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt @@ -0,0 +1,42 @@ +package com.tangem.features.yield.supply.impl.main.model.transformers + +import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus +import com.tangem.features.yield.supply.impl.R +import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode +import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.utils.transformer.Transformer + +internal class YieldSupplyTokenStatusSuccessTransformer( + private val tokenStatus: YieldMarketTokenStatus, + private val onStartEarningClick: () -> Unit, + private val mode: LoadingStatusMode, +) : Transformer { + + override fun transform(prevState: YieldSupplyUM): YieldSupplyUM { + if (!tokenStatus.isActive) return YieldSupplyUM.Unavailable + + return when (mode) { + LoadingStatusMode.Initial -> { + YieldSupplyUM.Initial( + title = resourceReference( + id = R.string.yield_module_token_details_earn_notification_title, + formatArgs = wrappedList(tokenStatus.apy), + ), + onClick = onStartEarningClick, + ) + } + LoadingStatusMode.LoadApy -> { + if (prevState is YieldSupplyUM.Content) { + prevState.copy( + rewardsApy = stringReference("${tokenStatus.apy}%"), + ) + } else { + prevState + } + } + } + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt index 4d7abbb513..db0f137890 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/YieldSupplyActiveComponent.kt @@ -8,16 +8,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp 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.components.SecondaryButton import com.tangem.core.ui.decompose.ComposableModularContentComponent import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.features.yield.supply.impl.subcomponents.active.model.YieldSupplyActiveModel import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveContent import com.tangem.features.yield.supply.impl.subcomponents.active.ui.YieldSupplyActiveTitle import com.tangem.features.yield.supply.impl.R +import com.tangem.features.yield.supply.impl.chart.DefaultYieldSupplyChartComponent import kotlinx.coroutines.flow.StateFlow internal class YieldSupplyActiveComponent( @@ -26,6 +29,12 @@ internal class YieldSupplyActiveComponent( ) : ComposableModularContentComponent, AppComponentContext by appComponentContext { private val model: YieldSupplyActiveModel = getOrCreateModel(params = params) + private val chartComponent = DefaultYieldSupplyChartComponent( + appComponentContext = child("chartComponent"), + params = DefaultYieldSupplyChartComponent.Params( + cryptoCurrency = params.cryptoCurrencyStatusFlow.value.currency as CryptoCurrency.Token, + ), + ) @Composable override fun Title() { @@ -37,7 +46,12 @@ internal class YieldSupplyActiveComponent( val state by model.uiState.collectAsStateWithLifecycle() val isBalanceHidden by params.isBalanceHiddenFlow.collectAsStateWithLifecycle() - YieldSupplyActiveContent(state = state, isBalanceHidden = isBalanceHidden, modifier = Modifier) + YieldSupplyActiveContent( + state = state, + isBalanceHidden = isBalanceHidden, + chartComponent = chartComponent, + modifier = Modifier, + ) } @Composable diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt index 7cda12cb63..bf0fdc6e5b 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/entity/YieldSupplyActiveContentUM.kt @@ -10,4 +10,5 @@ internal data class YieldSupplyActiveContentUM( val subtitle: TextReference, val subtitleLink: TextReference, val notificationUM: NotificationUM?, + val apy: TextReference? = null, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt index 4b38fbb125..ae85f3f637 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/model/YieldSupplyActiveModel.kt @@ -5,18 +5,22 @@ 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.notifications.NotificationConfig +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.yield.supply.usecase.YieldSupplyGetProtocolBalanceUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.subcomponents.active.YieldSupplyActiveComponent import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSupplyActiveContentUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch +import timber.log.Timber import javax.inject.Inject @ModelScoped @@ -24,6 +28,7 @@ internal class YieldSupplyActiveModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val yieldSupplyGetProtocolBalanceUseCase: YieldSupplyGetProtocolBalanceUseCase, + private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, ) : Model() { private val params: YieldSupplyActiveComponent.Params = paramsContainer.require() @@ -87,6 +92,8 @@ internal class YieldSupplyActiveModel @Inject constructor( null } + loadApy() + uiState.update { it.copy( notificationUM = approvalNotification, @@ -104,6 +111,21 @@ internal class YieldSupplyActiveModel @Inject constructor( .launchIn(modelScope) } + private fun loadApy() { + val cryptoCurrencyToken = cryptoCurrency as? CryptoCurrency.Token ?: return + modelScope.launch(dispatchers.default) { + yieldSupplyGetTokenStatusUseCase(cryptoCurrencyToken).onRight { tokenStatus -> + uiState.update { + it.copy( + apy = TextReference.Str("${tokenStatus.apy}%"), + ) + } + }.onLeft { + Timber.e("Error loading token status") + } + } + } + private companion object { const val AAVEV3_PREFIX = "a" } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt index 0619e6b7a0..383a4b2231 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/active/ui/YieldSupplyActiveContent.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* 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.Composable import androidx.compose.runtime.remember @@ -15,6 +16,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.LinkAnnotation import androidx.compose.ui.text.withLink @@ -25,6 +27,7 @@ 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.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -35,6 +38,7 @@ import com.tangem.features.yield.supply.impl.subcomponents.active.entity.YieldSu internal fun YieldSupplyActiveContent( state: YieldSupplyActiveContentUM, isBalanceHidden: Boolean, + chartComponent: ComposableContentComponent, modifier: Modifier = Modifier, ) { Column( @@ -62,6 +66,9 @@ internal fun YieldSupplyActiveContent( style = TangemTheme.typography.h2, color = TangemTheme.colors.text.primary1, ) + + CurrentApy(state.apy) + chartComponent.Content(Modifier.padding(bottom = 12.dp)) } YieldSupplyActiveMyFunds(state = state, isBalanceHidden = isBalanceHidden) @@ -76,6 +83,45 @@ internal fun YieldSupplyActiveContent( } } +@Composable +private fun CurrentApy(apy: TextReference?, modifier: Modifier = Modifier) { + Row(modifier = modifier.padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = modifier.weight(1.0f), + text = stringResourceSafe(R.string.yield_module_earn_sheet_current_apy_title), + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.tertiary, + ) + AnimatedContent( + targetState = apy?.resolveReference(), + label = "CurrentApy", + ) { apyText -> + if (apyText == null) { + TextShimmer( + modifier = modifier.width(56.dp), + text = "", + style = TangemTheme.typography.body1, + ) + } else { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painterResource(R.drawable.ic_arrow_up_8), + tint = TangemTheme.colors.text.accent, + contentDescription = null, + modifier = Modifier.padding(end = 8.dp), + ) + Text( + modifier = modifier, + text = apyText, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.accent, + ) + } + } + } + } +} + @Composable private fun YieldSupplyActiveMyFunds(state: YieldSupplyActiveContentUM, isBalanceHidden: Boolean) { Column( @@ -193,7 +239,11 @@ private fun YieldSupplyActiveBottomSheet_Preview( @PreviewParameter(YieldSupplyActiveBottomSheetPreviewProvider::class) params: YieldSupplyActiveContentUM, ) { TangemThemePreview { - YieldSupplyActiveContent(params, true) + YieldSupplyActiveContent( + state = params, + isBalanceHidden = true, + chartComponent = ComposableContentComponent.EMPTY, + ) } } From e511e845086a86ae76bdb3c067b6c0c3480a42e3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 14:56:51 +0500 Subject: [PATCH 02/22] Updated on 2026-08-14 --- .../ComposableListContentComponent.kt | 26 ++++ .../DefaultOnrampOperationComponent.kt | 7 +- .../selecttoken/ui/OnrampSelectToken.kt | 7 +- .../swap/DefaultSwapSelectTokensComponent.kt | 9 +- .../AvailableSwapPairsComponent.kt | 5 +- .../DefaultAvailableSwapPairsComponent.kt | 16 +- .../onramp/swap/ui/SwapSelectTokens.kt | 40 ++--- .../DefaultOnrampTokenListComponent.kt | 16 +- .../tokenlist/OnrampTokenListComponent.kt | 5 +- .../SetNothingToFoundStateTransformer.kt | 3 +- .../OnrampTokenItemStateConverterFactory.kt | 52 ++++++- .../onramp/tokenlist/ui/OnrampTokenList.kt | 144 +++++++++++++----- 12 files changed, 238 insertions(+), 92 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableListContentComponent.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableListContentComponent.kt b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableListContentComponent.kt new file mode 100644 index 0000000000..75f52c5be4 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/decompose/ComposableListContentComponent.kt @@ -0,0 +1,26 @@ +package com.tangem.core.ui.decompose + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +@Stable +interface ComposableListContentComponent { + + val uiState: StateFlow + + fun LazyListScope.content(uiState: T, modifier: Modifier) + + companion object { + val EMPTY = EmptyComposableListContentComponent + } +} + +object EmptyComposableListContentComponent : ComposableListContentComponent { + override val uiState: StateFlow = MutableStateFlow(Unit) + + override fun LazyListScope.content(uiState: Unit, modifier: Modifier) { /* no-op */ + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt index f4fe8672a8..644770a58b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/DefaultOnrampOperationComponent.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.selecttoken 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 @@ -41,11 +42,13 @@ internal class DefaultOnrampOperationComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - val state = model.state.collectAsStateWithLifecycle() + val state by model.state.collectAsStateWithLifecycle() + val onrampTokenListState by onrampTokenListComponent.uiState.collectAsStateWithLifecycle() OnrampSelectToken( - state = state.value, + state = state, onrampTokenListComponent = onrampTokenListComponent, + onrampTokenListState = onrampTokenListState, hotCryptoComponent = hotCryptoComponent, modifier = modifier, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt index 689cee2068..43ac75ccc7 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/ui/OnrampSelectToken.kt @@ -21,12 +21,14 @@ import com.tangem.features.onramp.hottokens.HotCryptoComponent import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.selecttoken.entity.OnrampOperationUM import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import com.tangem.features.onramp.tokenlist.entity.TokenListUM @OptIn(ExperimentalFoundationApi::class) @Composable internal fun OnrampSelectToken( state: OnrampOperationUM, onrampTokenListComponent: OnrampTokenListComponent, + onrampTokenListState: TokenListUM, hotCryptoComponent: HotCryptoComponent?, modifier: Modifier = Modifier, ) { @@ -50,8 +52,9 @@ internal fun OnrampSelectToken( ) } - item(key = "token_list", contentType = "token_list") { - onrampTokenListComponent.Content( + with(onrampTokenListComponent) { + content( + uiState = onrampTokenListState, modifier = Modifier .padding(top = 8.dp) .padding(horizontal = 16.dp) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt index 0f8001df67..5040c07c57 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/DefaultSwapSelectTokensComponent.kt @@ -2,6 +2,7 @@ package com.tangem.features.onramp.swap import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -54,12 +55,16 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( @Composable override fun Content(modifier: Modifier) { - val state = model.state.collectAsStateWithLifecycle() + val state by model.state.collectAsStateWithLifecycle() + val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle() + val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle() SwapSelectTokens( - state = state.value, + state = state, selectFromTokenListComponent = selectFromTokenListComponent, + selectFromTokenListState = fromTokensState, selectToTokenListComponent = selectToTokenListComponent, + selectToTokenListState = toTokensState, modifier = modifier, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt index 35f44bbb33..cb87a50dc2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/AvailableSwapPairsComponent.kt @@ -3,14 +3,15 @@ package com.tangem.features.onramp.swap.availablepairs import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.decompose.ComposableListContentComponent import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.features.onramp.tokenlist.entity.TokenListUM import kotlinx.coroutines.flow.StateFlow /** Token list component that present list of available tokens for swap */ @Stable -internal interface AvailableSwapPairsComponent : ComposableContentComponent { +internal interface AvailableSwapPairsComponent : ComposableListContentComponent { /** Component factory */ interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt index fd77ee062f..eea5740b23 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/DefaultAvailableSwapPairsComponent.kt @@ -1,17 +1,17 @@ package com.tangem.features.onramp.swap.availablepairs -import androidx.compose.runtime.Composable +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Stable -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.onramp.swap.availablepairs.model.AvailableSwapPairsModel -import com.tangem.features.onramp.tokenlist.ui.TokenList +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.ui.onrampTokenList import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.StateFlow @Stable internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( @@ -21,11 +21,11 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( private val model: AvailableSwapPairsModel = getOrCreateModel(params) - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() + override val uiState: StateFlow + get() = model.state - TokenList(state = state, modifier = modifier) + override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) { + onrampTokenList(state = uiState) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt index bf873aa6f5..346d2b498f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/SwapSelectTokens.kt @@ -14,6 +14,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.unit.dp +import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -23,6 +24,7 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen import com.tangem.features.onramp.swap.entity.ExchangeCardUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent +import com.tangem.features.onramp.tokenlist.entity.TokenListUM /** * Swap select tokens @@ -39,7 +41,9 @@ import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent internal fun SwapSelectTokens( state: SwapSelectTokensUM, selectFromTokenListComponent: OnrampTokenListComponent, + selectFromTokenListState: TokenListUM, selectToTokenListComponent: AvailableSwapPairsComponent, + selectToTokenListState: TokenListUM, modifier: Modifier = Modifier, ) { BackHandler(onBack = state.onBackClick) @@ -77,33 +81,33 @@ internal fun SwapSelectTokens( } if (state.exchangeFrom is ExchangeCardUM.Empty) { - item(key = "select_from", contentType = "select_from") { - selectFromTokenListComponent.Content( - modifier = Modifier - .padding(horizontal = 16.dp) - .animateItem(), + with(selectFromTokenListComponent) { + content( + uiState = selectFromTokenListState, + modifier = Modifier, ) } } if (state.exchangeFrom is ExchangeCardUM.Filled) { item(key = "exchange_to", contentType = "exchange_to") { - ExchangeCard( - state = state.exchangeTo, - isBalanceHidden = state.isBalanceHidden, - modifier = Modifier - .padding(horizontal = 16.dp) - .padding(bottom = 12.dp) - .animateItem(), - ) + if (selectToTokenListState.warning != NotificationUM.Warning.SwapNoAvailablePair) { + ExchangeCard( + state = state.exchangeTo, + isBalanceHidden = state.isBalanceHidden, + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 12.dp) + .animateItem(), + ) + } } if (state.exchangeTo is ExchangeCardUM.Empty) { - item(key = "select_to", contentType = "select_to") { - selectToTokenListComponent.Content( - modifier = Modifier - .padding(horizontal = 16.dp) - .animateItem(), + with(selectToTokenListComponent) { + content( + uiState = selectToTokenListState, + modifier = Modifier.padding(horizontal = 16.dp), ) } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt index c538d475cd..d35442f63a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/DefaultOnrampTokenListComponent.kt @@ -1,17 +1,17 @@ package com.tangem.features.onramp.tokenlist -import androidx.compose.runtime.Composable +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Stable -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.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.model.OnrampTokenListModel -import com.tangem.features.onramp.tokenlist.ui.TokenList +import com.tangem.features.onramp.tokenlist.ui.onrampTokenList import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.StateFlow @Stable internal class DefaultOnrampTokenListComponent @AssistedInject constructor( @@ -21,11 +21,11 @@ internal class DefaultOnrampTokenListComponent @AssistedInject constructor( private val model: OnrampTokenListModel = getOrCreateModel(params) - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() + override val uiState: StateFlow + get() = model.state - TokenList(state = state, modifier = modifier) + override fun LazyListScope.content(uiState: TokenListUM, modifier: Modifier) { + onrampTokenList(state = uiState) } @AssistedFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt index 94ba06e247..4490a37ec3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/OnrampTokenListComponent.kt @@ -3,14 +3,15 @@ package com.tangem.features.onramp.tokenlist import androidx.compose.runtime.Stable import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.core.ui.decompose.ComposableListContentComponent import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.onramp.tokenlist.entity.OnrampOperation +import com.tangem.features.onramp.tokenlist.entity.TokenListUM /** Token list component that present list of token for multi-currency wallet */ @Stable -internal interface OnrampTokenListComponent : ComposableContentComponent { +internal interface OnrampTokenListComponent : ComposableListContentComponent { /** Component factory */ interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt index e98b29bfaa..2b2c7963eb 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt @@ -26,8 +26,7 @@ internal class SetNothingToFoundStateTransformer( id = emptySearchMessageReference.hashCode(), text = emptySearchMessageReference, ).let(::add) - } - .toImmutableList(), + }.toImmutableList(), unavailableItems = persistentListOf(), isBalanceHidden = isBalanceHidden, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt index f693f02e79..67f0c13e19 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/utils/OnrampTokenItemStateConverterFactory.kt @@ -6,6 +6,7 @@ import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.getFormatte import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -21,7 +22,13 @@ internal object OnrampTokenItemStateConverterFactory { ): TokenItemStateConverter { return TokenItemStateConverter( appCurrency = appCurrency, - subtitleStateProvider = { createSubtitleState(status = it, isAvailable = true) }, + subtitleStateProvider = { + createSubtitleState( + status = it, + isAvailable = true, + text = stringReference(value = it.currency.symbol), + ) + }, subtitle2StateProvider = ::createSubtitle2State, fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true) @@ -40,7 +47,13 @@ internal object OnrampTokenItemStateConverterFactory { isAvailable = false, ) }, - subtitleStateProvider = { createSubtitleState(status = it, isAvailable = false) }, + subtitleStateProvider = { + createSubtitleState( + status = it, + text = stringReference(value = it.currency.symbol), + isAvailable = false, + ) + }, subtitle2StateProvider = ::createSubtitle2State, fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false) @@ -48,12 +61,43 @@ internal object OnrampTokenItemStateConverterFactory { ) } - private fun createSubtitleState(status: CryptoCurrencyStatus, isAvailable: Boolean): TokenItemState.SubtitleState { + fun createUnavailableItemConverterV2( + appCurrency: AppCurrency, + unavailableErrorText: TextReference, + ): TokenItemStateConverter { + return TokenItemStateConverter( + appCurrency = appCurrency, + iconStateProvider = { CryptoCurrencyToIconStateConverter(isAvailable = false).convert(it) }, + titleStateProvider = { + TokenItemState.TitleState.Content( + text = stringReference(value = it.currency.name), + isAvailable = false, + ) + }, + subtitleStateProvider = { + createSubtitleState( + status = it, + isAvailable = false, + text = unavailableErrorText, + ) + }, + subtitle2StateProvider = ::createSubtitle2State, + fiatAmountStateProvider = { + createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = false) + }, + ) + } + + private fun createSubtitleState( + status: CryptoCurrencyStatus, + isAvailable: Boolean, + text: TextReference, + ): TokenItemState.SubtitleState { return when (status.value) { CryptoCurrencyStatus.Loading -> TokenItemState.SubtitleState.Loading else -> { TokenItemState.SubtitleState.TextContent( - value = stringReference(value = status.currency.symbol), + value = text, isAvailable = isAvailable, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index 27943a21c4..8aeb293ca5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -3,27 +3,28 @@ package com.tangem.features.onramp.tokenlist.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.Composable -import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEachIndexed import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.SpacerH12 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.components.notifications.Notification +import com.tangem.core.ui.components.tokenlist.PortfolioListItem +import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem import com.tangem.core.ui.components.tokenlist.TokenListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.conditional import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BuyTokenScreenTestTags @@ -36,17 +37,21 @@ import kotlinx.collections.immutable.ImmutableList * Token list * * @param state state - * @param modifier modifier * [REDACTED_AUTHOR] */ -@Composable -internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { - Column(modifier) { - if (state.warning == null) { - SearchBar(searchBarUM = state.searchBarUM) - } else { - AnimatedContent(targetState = state.warning, label = "") { warning -> +internal fun LazyListScope.onrampTokenList(state: TokenListUM) { + val itemModifier = Modifier.padding(horizontal = 16.dp) + + if (state.warning == null) { + searchBarItem(searchBarUM = state.searchBarUM, modifier = itemModifier) + } else { + item("NotificationsKey") { + AnimatedContent( + targetState = state.warning, + label = "", + modifier = itemModifier, + ) { warning -> when (warning) { is NotificationUM.Warning.OnrampErrorNotification -> { Notification( @@ -60,31 +65,29 @@ internal fun TokenList(state: TokenListUM, modifier: Modifier = Modifier) { } } } + } - if (state.availableItems.isNotEmpty()) { - SpacerH12() - ItemsBlock(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) - } + tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) - if (state.unavailableItems.isNotEmpty()) { - SpacerH12() - ItemsBlock(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) - } + tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) +} + +private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) { + item("SearchKey") { + SearchBar( + state = searchBarUM, + colors = TangemSearchBarDefaults.secondaryTextFieldColors, + modifier = modifier, + ) } } -@Composable -private fun SearchBar(searchBarUM: SearchBarUM) { - SearchBar( - state = searchBarUM, - colors = TangemSearchBarDefaults.secondaryTextFieldColors, - ) -} - -@Composable -private fun ItemsBlock(items: ImmutableList, isBalanceHidden: Boolean) { - items.fastForEachIndexed { index, item -> - key(item.id) { +private fun LazyListScope.tokensList(items: ImmutableList, isBalanceHidden: Boolean) { + itemsIndexed( + items = items, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { index, item -> TokenListItem( state = item, isBalanceHidden = isBalanceHidden, @@ -92,13 +95,70 @@ private fun ItemsBlock(items: ImmutableList, isBalanceHidden: .roundedShapeItemDecoration( currentIndex = index, lastIndex = items.lastIndex, - addDefaultPadding = false, backgroundColor = TangemTheme.colors.background.primary, ) .testTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) .semantics { lazyListItemPosition = index }, ) - } + }, + ) +} + +internal fun LazyListScope.portfolioTokensList(portfolio: TokensListItemUM.Portfolio, isBalanceHidden: Boolean) { + val tokens = portfolio.tokens + val isExpanded = portfolio.isExpanded + + portfolioItem( + portfolio = portfolio, + modifier = Modifier.padding(top = 8.dp), + isBalanceHidden = isBalanceHidden, + ) + if (!isExpanded) return + itemsIndexed( + items = tokens, + key = { _, item -> item.id }, + contentType = { _, item -> item::class.java }, + itemContent = { tokenIndex, token -> + val indexWithHeader = tokenIndex.inc() + PortfolioTokensListItem( + state = token, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .animateItem() + .roundedShapeItemDecoration( + currentIndex = indexWithHeader, + lastIndex = tokens.lastIndex.inc(), + backgroundColor = TangemTheme.colors.background.primary, + ) + .conditional(tokenIndex == tokens.lastIndex) { + Modifier.padding(bottom = 8.dp) + }, + ) + }, + ) +} + +private fun LazyListScope.portfolioItem( + portfolio: TokensListItemUM.Portfolio, + modifier: Modifier, + isBalanceHidden: Boolean, +) { + item( + key = "account-${portfolio.id}", + contentType = "account", + ) { + PortfolioListItem( + state = portfolio, + isBalanceHidden = isBalanceHidden, + modifier = Modifier + .animateItem() + .roundedShapeItemDecoration( + currentIndex = 0, + lastIndex = portfolio.tokens.lastIndex.inc(), + backgroundColor = TangemTheme.colors.background.primary, + ) + .then(modifier), + ) } } @@ -107,12 +167,12 @@ private fun ItemsBlock(items: ImmutableList, isBalanceHidden: @Composable private fun Preview_TokenList(@PreviewParameter(PreviewTokenListUMProvider::class) state: TokenListUM) { TangemThemePreview { - TokenList( - state = state, - modifier = Modifier - .fillMaxWidth() - .background(color = TangemTheme.colors.background.secondary) - .padding(16.dp), - ) + LazyColumn( + modifier = Modifier.background(color = TangemTheme.colors.background.secondary), + ) { + onrampTokenList( + state = state, + ) + } } } \ No newline at end of file From cb6fdddcde30252115591dde8023b962c881b3ba Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 14:51:32 +0400 Subject: [PATCH 03/22] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 34e16dd458..35e4b6fd2f 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -96,7 +96,7 @@ room = "2.6.1" markdown = "0.7.2" markdownComposeView = "0.5.4" usedesk = "4.4.0" -sumsub = "1.37.1" +sumsub = "1.38.0" # endregion Other libraries # region Tools From 1ed65c48cc88f41e60803373dc73657a77941ee5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 16:02:16 +0400 Subject: [PATCH 04/22] Updated on 2026-08-14 --- .../account/converter/AccountListConverter.kt | 2 +- .../FetchWalletAccountsErrorHandler.kt | 2 +- .../DefaultMultiAccountListProducer.kt | 6 +- .../DefaultSingleAccountListProducer.kt | 11 +- .../producer/WalletAccountListFlowFactory.kt | 9 +- .../DefaultAccountsCRUDRepository.kt | 6 +- .../account/converter/AccountConverterExt.kt | 7 +- .../converter/AccountListConverterTest.kt | 6 +- .../GetWalletAccountsResponseConverterTest.kt | 6 +- ...SaveWalletAccountsResponseConverterTest.kt | 11 +- .../FetchWalletAccountsErrorHandlerTest.kt | 2 +- .../DefaultMultiAccountListProducerTest.kt | 42 +++---- .../DefaultSingleAccountListProducerTest.kt | 105 +++--------------- .../WalletAccountListFlowFactoryTest.kt | 22 +++- .../DefaultAccountsCRUDRepositoryTest.kt | 8 +- .../domain/account/models/AccountList.kt | 24 ++-- .../account/models/AccountStatusList.kt | 6 +- .../domain/account/models/AccountListTest.kt | 58 ++++------ .../usecase/AddCryptoPortfolioUseCaseTest.kt | 15 +-- .../ArchiveCryptoPortfolioUseCaseTest.kt | 11 +- .../RecoverCryptoPortfolioUseCaseTest.kt | 13 +-- .../UpdateCryptoPortfolioUseCaseTest.kt | 21 ++-- .../DefaultSingleAccountStatusListProducer.kt | 10 +- .../GetAccountCurrencyByAddressUseCase.kt | 2 +- ...aultSingleAccountStatusListProducerTest.kt | 25 +++-- .../GetAccountCurrencyByAddressUseCaseTest.kt | 4 +- .../features/account/PortfolioFetcher.kt | 2 +- .../accounts/viewmodel/AccountsViewModel.kt | 2 +- 28 files changed, 172 insertions(+), 266 deletions(-) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt index d4c993f59f..b2744a7452 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/AccountListConverter.kt @@ -28,7 +28,7 @@ internal class AccountListConverter @AssistedInject constructor( override fun convert(value: GetWalletAccountsResponse): AccountList { return AccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, accounts = value.accounts.map(cryptoPortfolioConverter::convert).toSet(), totalAccounts = value.wallet.totalAccounts, sortType = TokensSortTypeConverter.convert(value.wallet.sort), diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 2acc302f24..3c7d1af215 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -87,7 +87,7 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( } private fun createDefaultAccountDTOs(userWallet: UserWallet): List { - val accounts = AccountList.empty(userWallet).accounts + val accounts = AccountList.empty(userWallet.walletId).accounts .filterIsInstance() val converter = cryptoPortfolioCF.create(userWallet = userWallet) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt index da4e0e7853..40e4fd5f47 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultMultiAccountListProducer.kt @@ -5,6 +5,7 @@ import arrow.core.some import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.MultiAccountListProducer +import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -35,10 +36,11 @@ internal class DefaultMultiAccountListProducer @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow> { return userWalletsStore.userWallets + .map { it.map(UserWallet::walletId) } .distinctUntilChanged() - .flatMapLatest { userWallets -> + .flatMapLatest { ids -> combine( - flows = userWallets.map(walletAccountListFlowFactory::create), + flows = ids.map(walletAccountListFlowFactory::create), transform = ::listOf, ) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt index 43d0745ea2..6288d9036f 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/DefaultSingleAccountListProducer.kt @@ -2,7 +2,6 @@ package com.tangem.data.account.producer import arrow.core.Option import arrow.core.none -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -11,16 +10,13 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.mapNotNull /** * Default implementation of [SingleAccountListProducer]. * Produces a list of [AccountList] for a specific user wallet. * * @property params params containing the user wallet ID - * @property userWalletsStore store that provides user wallets * @property walletAccountListFlowFactory builder to create flows of [AccountList] for each wallet * @property dispatchers coroutine dispatchers provider * @@ -28,7 +24,6 @@ import kotlinx.coroutines.flow.mapNotNull */ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @Assisted val params: SingleAccountListProducer.Params, - private val userWalletsStore: UserWalletsStore, private val walletAccountListFlowFactory: WalletAccountListFlowFactory, private val dispatchers: CoroutineDispatcherProvider, ) : SingleAccountListProducer { @@ -37,11 +32,7 @@ internal class DefaultSingleAccountListProducer @AssistedInject constructor( @OptIn(ExperimentalCoroutinesApi::class) override fun produce(): Flow { - return userWalletsStore.userWallets - .mapNotNull { userWallets -> - userWallets.firstOrNull { it.walletId == params.userWalletId } - } - .flatMapLatest(walletAccountListFlowFactory::create) + return walletAccountListFlowFactory.create(userWalletId = params.userWalletId) .flowOn(dispatchers.default) } diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt index 765d6afcc1..44aa8713fc 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/WalletAccountListFlowFactory.kt @@ -4,9 +4,11 @@ import com.tangem.data.account.converter.AccountListConverter import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.models.wallet.requireColdWallet import kotlinx.coroutines.flow.* @@ -22,12 +24,15 @@ import javax.inject.Inject [REDACTED_AUTHOR] */ internal class WalletAccountListFlowFactory @Inject constructor( + private val userWalletsStore: UserWalletsStore, private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val accountListConverterFactory: AccountListConverter.Factory, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, ) { - fun create(userWallet: UserWallet): Flow { + fun create(userWalletId: UserWalletId): Flow { + val userWallet = userWalletsStore.getSyncStrict(userWalletId) + return if (userWallet.isMultiCurrency) { createForMultiWallet(userWallet) } else { @@ -53,6 +58,6 @@ internal class WalletAccountListFlowFactory @Inject constructor( setOf(cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet = userWallet)) } - return AccountList.empty(userWallet = userWallet, cryptoCurrencies = currencies) + return AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = currencies) } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt index a9ceb3026e..524e256767 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/repository/DefaultAccountsCRUDRepository.kt @@ -101,12 +101,12 @@ internal class DefaultAccountsCRUDRepository( } override suspend fun saveAccounts(accountList: AccountList) { - val userWalletId = accountList.userWallet.walletId + val userWallet = userWalletsStore.getSyncStrict(accountList.userWalletId) - val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = accountList.userWallet) + val converter = convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) val accountsResponse = converter.convert(value = accountList) - walletAccountsSaver.pushAndStore(userWalletId = userWalletId, response = accountsResponse) + walletAccountsSaver.pushAndStore(userWalletId = userWallet.walletId, response = accountsResponse) } override suspend fun getTotalAccountsCountSync(userWalletId: UserWalletId): Option = option { diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt index 880b20520e..12ac6dda89 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountConverterExt.kt @@ -8,7 +8,6 @@ import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId internal fun createWalletAccountDTO( @@ -72,13 +71,13 @@ internal fun createGetWalletAccountsResponse( } internal fun createAccountList( - userWallet: UserWallet, + userWalletId: UserWalletId, sortType: TokensSortType = TokensSortType.BALANCE, groupType: TokensGroupType = TokensGroupType.NETWORK, ): AccountList { return AccountList( - userWallet = userWallet, - accounts = setOf(createCryptoPortfolio(userWallet.walletId)), + userWalletId = userWalletId, + accounts = setOf(createCryptoPortfolio(userWalletId)), totalAccounts = 1, sortType = sortType, groupType = groupType, diff --git a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt index 8b32adb5e5..1a7ae063f0 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/AccountListConverterTest.kt @@ -96,7 +96,7 @@ class AccountListConverterTest { ), expected = Result.success( createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.BALANCE, groupType = TokensGroupType.NETWORK, ), @@ -110,7 +110,7 @@ class AccountListConverterTest { ), expected = Result.success( createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ), @@ -124,7 +124,7 @@ class AccountListConverterTest { ), expected = Result.success( createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ), diff --git a/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt index 766544b58a..2b09ada07f 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/GetWalletAccountsResponseConverterTest.kt @@ -46,7 +46,7 @@ class GetWalletAccountsResponseConverterTest { @Test fun `cryptoPortfolioConverter throws exception`() { // Arrange - val domain = createAccountList(userWallet = userWallet) + val domain = createAccountList(userWalletId = userWallet.walletId) val exception = IllegalStateException("Test exception") every { cryptoPortfolioConverter.convertBack(any()) } throws exception @@ -92,7 +92,7 @@ class GetWalletAccountsResponseConverterTest { return listOf( ConvertModel( value = createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.BALANCE, groupType = TokensGroupType.NETWORK, ), @@ -106,7 +106,7 @@ class GetWalletAccountsResponseConverterTest { ), ConvertModel( value = createAccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, sortType = TokensSortType.NONE, groupType = TokensGroupType.NONE, ), diff --git a/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt index 6d5ebee814..d2a6742050 100644 --- a/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/converter/SaveWalletAccountsResponseConverterTest.kt @@ -6,10 +6,7 @@ import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.domain.account.models.AccountList import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import io.mockk.every -import io.mockk.mockk import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -19,13 +16,11 @@ class SaveWalletAccountsResponseConverterTest { @Test fun convert() { // Arrange - val userWallet = mockk { - every { this@mockk.walletId } returns UserWalletId("011") - } + val userWalletId = UserWalletId("011") val accountList = AccountList( - userWallet = userWallet, - accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + userWalletId = userWalletId, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)), totalAccounts = 1, ) .getOrNull()!! diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index 422c9a7b8a..c174b4d463 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -160,7 +160,7 @@ class FetchWalletAccountsErrorHandlerTest { // Arrange val error = ApiResponseError.TimeoutException() - val accounts = AccountList.empty(userWallet).accounts + val accounts = AccountList.empty(userWalletId).accounts .filterIsInstance() val accountDTO = WalletAccountDTO( diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt index 3ca26b0198..05cb95cca1 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultMultiAccountListProducerTest.kt @@ -51,8 +51,8 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) every { userWalletsStore.userWallets } returns userWalletsFlow - val accountList = AccountList.empty(userWallet) - every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList) + val accountList = AccountList.empty(userWalletId) + every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) // Act val actual = producer.produce().let(::getEmittedValues) @@ -63,7 +63,7 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -73,11 +73,11 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) every { userWalletsStore.userWallets } returns userWalletsFlow - val accountList = AccountList.empty(userWallet) - val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE) + val accountList = AccountList.empty(userWalletId) + val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE) val factoryFlow = MutableStateFlow(null) - every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull() + every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull() // Act (first emission) factoryFlow.value = accountList @@ -95,9 +95,9 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -107,10 +107,10 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) every { userWalletsStore.userWallets } returns userWalletsFlow - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val factoryFlow = MutableStateFlow(null) - every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull() + every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull() // Act (first emission) factoryFlow.value = accountList @@ -128,9 +128,9 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -141,7 +141,7 @@ class DefaultMultiAccountListProducerTest { every { userWalletsStore.userWallets } returns userWalletsFlow val exception = RuntimeException("Converter error") - every { walletAccountListFlowFactory.create(userWallet) } throws exception + every { walletAccountListFlowFactory.create(userWalletId) } throws exception // Act val actual = producer.produceWithFallback().let(::getEmittedValues) @@ -152,7 +152,7 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -178,7 +178,7 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) every { userWalletsStore.userWallets } returns userWalletsFlow - every { walletAccountListFlowFactory.create(userWallet) } returns emptyFlow() + every { walletAccountListFlowFactory.create(userWalletId) } returns emptyFlow() // Act val actual = producer.produce().let(::getEmittedValues) @@ -188,7 +188,7 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @@ -203,9 +203,9 @@ class DefaultMultiAccountListProducerTest { val userWalletsFlow = MutableStateFlow(listOf(userWallet, userWallet2)) every { userWalletsStore.userWallets } returns userWalletsFlow - val accountList = AccountList.empty(userWallet) - every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList) - every { walletAccountListFlowFactory.create(userWallet2) } returns emptyFlow() + val accountList = AccountList.empty(userWalletId) + every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) + every { walletAccountListFlowFactory.create(userWalletId2) } returns emptyFlow() // Act val actual = producer.produce().let(::getEmittedValues) @@ -215,8 +215,8 @@ class DefaultMultiAccountListProducerTest { coVerify(ordering = Ordering.SEQUENCE) { userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) - walletAccountListFlowFactory.create(userWallet2) + walletAccountListFlowFactory.create(userWalletId) + walletAccountListFlowFactory.create(userWalletId2) } } } \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt index 846e46bfea..bc065b4578 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/DefaultSingleAccountListProducerTest.kt @@ -2,7 +2,6 @@ package com.tangem.data.account.producer import com.google.common.truth.Truth import com.tangem.common.test.utils.getEmittedValues -import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.models.TokensSortType @@ -11,7 +10,6 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -26,7 +24,6 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultSingleAccountListProducerTest { - private val userWalletsStore: UserWalletsStore = mockk() private val walletAccountListFlowFactory: WalletAccountListFlowFactory = mockk() private val userWalletId = UserWalletId("011") @@ -36,24 +33,22 @@ class DefaultSingleAccountListProducerTest { private val producer = DefaultSingleAccountListProducer( params = SingleAccountListProducer.Params(userWalletId = userWalletId), - userWalletsStore = userWalletsStore, walletAccountListFlowFactory = walletAccountListFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @AfterEach fun tearDownEach() { - clearMocks(userWalletsStore, walletAccountListFlowFactory) + clearMocks(walletAccountListFlowFactory) } @Test fun produce() = runTest { // Arrange - val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow + MutableStateFlow(listOf(userWallet)) - val accountList = AccountList.empty(userWallet) - every { walletAccountListFlowFactory.create(userWallet) } returns flowOf(accountList) + val accountList = AccountList.empty(userWalletId) + every { walletAccountListFlowFactory.create(userWalletId) } returns flowOf(accountList) // Act val actual = producer.produce().let(::getEmittedValues) @@ -63,22 +58,18 @@ class DefaultSingleAccountListProducerTest { Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) } } @Test fun `flow will updated if factoryFlow is updated`() = runTest { // Arrange - val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow - - val accountList = AccountList.empty(userWallet) - val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.NONE) + val accountList = AccountList.empty(userWalletId) + val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.NONE) val factoryFlow = MutableStateFlow(null) - every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull() + every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull() // Act (first emission) factoryFlow.value = accountList @@ -95,23 +86,18 @@ class DefaultSingleAccountListProducerTest { Truth.assertThat(secondEmission).containsExactly(updatedAccountList) coVerifyOrder { - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) + walletAccountListFlowFactory.create(userWalletId) } } @Test fun `flow is filtered the same response`() = runTest { // Arrange - val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow - - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val factoryFlow = MutableStateFlow(null) - every { walletAccountListFlowFactory.create(userWallet) } returns factoryFlow.filterNotNull() + every { walletAccountListFlowFactory.create(userWalletId) } returns factoryFlow.filterNotNull() // Act (first emission) factoryFlow.value = accountList @@ -128,71 +114,8 @@ class DefaultSingleAccountListProducerTest { Truth.assertThat(secondEmission).containsExactly(accountList) coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) + walletAccountListFlowFactory.create(userWalletId) + walletAccountListFlowFactory.create(userWalletId) } } - - @Test - fun `flow is empty if factory throws exception`() = runTest { - // Arrange - val userWalletsFlow = MutableStateFlow(value = listOf(userWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow - - val exception = RuntimeException("Converter error") - every { walletAccountListFlowFactory.create(userWallet) } throws exception - - // Act - val actual = producer.produceWithFallback().let(::getEmittedValues) - - // Assert - Truth.assertThat(actual).isEmpty() // no emissions - - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets - walletAccountListFlowFactory.create(userWallet) - } - } - - @Test - fun `flow is empty if userWalletsFlow returns empty flow`() = runTest { - // Arrange - val userWalletsFlow = emptyFlow>() - every { userWalletsStore.userWallets } returns userWalletsFlow - - // Act - val actual = producer.produce().let(::getEmittedValues) - - // Assert - Truth.assertThat(actual).isEmpty() // no emissions - - coVerify(exactly = 1) { userWalletsStore.userWallets } - coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) } - } - - @Test - fun `flow is empty if userWalletsFlow doesn't contains userWalletId from params`() = runTest { - // Arrange - val unknownId = UserWalletId("012") - val unknownWallet = mockk { - every { this@mockk.walletId } returns unknownId - } - - val userWalletsFlow = MutableStateFlow(listOf(unknownWallet)) - every { userWalletsStore.userWallets } returns userWalletsFlow - - // Act - val actual = producer.produce().let(::getEmittedValues) - - // Assert - Truth.assertThat(actual).isEmpty() // no emissions - - coVerify(ordering = Ordering.SEQUENCE) { - userWalletsStore.userWallets - } - - coVerify(inverse = true) { walletAccountListFlowFactory.create(any()) } - } } \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt index 6e13e22dad..47c2e8f6fc 100644 --- a/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/producer/WalletAccountListFlowFactoryTest.kt @@ -10,6 +10,7 @@ import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.domain.account.models.AccountList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -28,6 +29,7 @@ import org.junit.jupiter.api.TestInstance @TestInstance(TestInstance.Lifecycle.PER_CLASS) class WalletAccountListFlowFactoryTest { + private val userWalletsStore: UserWalletsStore = mockk() private val accountsResponseStoreFactory: AccountsResponseStoreFactory = mockk() private val accountsResponseStore: AccountsResponseStore = mockk() private val accountsResponseStoreFlow = MutableStateFlow(value = null) @@ -38,6 +40,7 @@ class WalletAccountListFlowFactoryTest { private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val factory = WalletAccountListFlowFactory( + userWalletsStore = userWalletsStore, accountsResponseStoreFactory = accountsResponseStoreFactory, accountListConverterFactory = accountListConverterFactory, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, @@ -49,6 +52,7 @@ class WalletAccountListFlowFactoryTest { @AfterEach fun tearDownEach() { clearMocks( + userWalletsStore, accountsResponseStoreFactory, accountsResponseStore, accountListConverterFactory, @@ -66,17 +70,19 @@ class WalletAccountListFlowFactoryTest { every { this@mockk.isMultiCurrency } returns true } + every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + val accountsResponse = createGetWalletAccountsResponse(userWalletId) every { accountsResponseStoreFactory.create(userWalletId) } returns accountsResponseStore every { accountsResponseStore.data } returns accountsResponseStoreFlow accountsResponseStoreFlow.value = accountsResponse - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) every { accountListConverterFactory.create(userWallet) } returns accountListConverter every { accountListConverter.convert(accountsResponse) } returns accountList // Act - val actual = factory.create(userWallet).let(::getEmittedValues) + val actual = factory.create(userWalletId).let(::getEmittedValues) // Assert val expected = accountList @@ -99,14 +105,16 @@ class WalletAccountListFlowFactoryTest { fun `create for single wallet`() = runTest { val userWallet = MockUserWalletFactory.create().copy(isMultiCurrency = false) + every { userWalletsStore.getSyncStrict(userWallet.walletId) } returns userWallet + val currency = cryptoCurrencyFactory.ethereum every { cardCryptoCurrencyFactory.createPrimaryCurrencyForSingleCurrencyCard(userWallet) } returns currency // Act - val actual = factory.create(userWallet).let(::getEmittedValues) + val actual = factory.create(userWallet.walletId).let(::getEmittedValues) // Assert - val expected = AccountList.empty(userWallet = userWallet, cryptoCurrencies = setOf(currency)) + val expected = AccountList.empty(userWalletId = userWallet.walletId, cryptoCurrencies = setOf(currency)) Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { @@ -126,16 +134,18 @@ class WalletAccountListFlowFactoryTest { fun `flow is created for single wallet with token`() = runTest { val nodl = MockUserWalletFactory.createSingleWalletWithToken() + every { userWalletsStore.getSyncStrict(nodl.walletId) } returns nodl + val currencies = cryptoCurrencyFactory.ethereumAndStellar.toSet() every { cardCryptoCurrencyFactory.createCurrenciesForSingleCurrencyCardWithToken(userWallet = nodl) } returns currencies.toList() // Act - val actual = factory.create(nodl).let(::getEmittedValues) + val actual = factory.create(nodl.walletId).let(::getEmittedValues) // Assert - val expected = AccountList.empty(userWallet = nodl, cryptoCurrencies = currencies) + val expected = AccountList.empty(userWalletId = nodl.walletId, cryptoCurrencies = currencies) Truth.assertThat(actual).containsExactly(expected) coVerify(ordering = Ordering.SEQUENCE) { diff --git a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt index 1e7c97fc64..b830678839 100644 --- a/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/repository/DefaultAccountsCRUDRepositoryTest.kt @@ -584,7 +584,7 @@ class DefaultAccountsCRUDRepositoryTest { every { this@mockk.walletId } returns userWalletId } - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountsResponse = mockk() accountsResponseStoreFlow.value = accountsResponse @@ -593,6 +593,8 @@ class DefaultAccountsCRUDRepositoryTest { every { this@mockk.convert(accountList) } returns accountsResponse } + every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + every { convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) } returns converter @@ -617,7 +619,7 @@ class DefaultAccountsCRUDRepositoryTest { every { this@mockk.walletId } returns userWalletId } - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountsResponse = mockk() accountsResponseStoreFlow.value = accountsResponse @@ -626,6 +628,8 @@ class DefaultAccountsCRUDRepositoryTest { every { this@mockk.convert(accountList) } returns accountsResponse } + every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet + every { convertersContainer.getWalletAccountsResponseCF.create(userWallet = userWallet) } returns converter diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt index 2f848f2f16..e305566047 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountList.kt @@ -8,14 +8,14 @@ import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.extensions.addOrReplace import kotlinx.serialization.Serializable /** * Represents a list of accounts associated with a user wallet * - * @property userWallet the user wallet associated with the account list + * @property userWalletId the user wallet id associated with the account list * @property accounts a set of accounts belonging to the user wallet * @property totalAccounts the total number of accounts * @@ -23,7 +23,7 @@ import kotlinx.serialization.Serializable */ @Serializable data class AccountList private constructor( - val userWallet: UserWallet, + val userWalletId: UserWalletId, val accounts: Set, val totalAccounts: Int, val sortType: TokensSortType, @@ -51,7 +51,7 @@ data class AccountList private constructor( val accounts = this.accounts.addOrReplace(other) { it.accountId == other.accountId } return invoke( - userWallet = this.userWallet, + userWalletId = this.userWalletId, accounts = accounts, totalAccounts = this.totalAccounts + if (isNewAccount) 1 else 0, sortType = this.sortType, @@ -73,7 +73,7 @@ data class AccountList private constructor( } return invoke( - userWallet = this.userWallet, + userWalletId = this.userWalletId, accounts = accounts, totalAccounts = this.totalAccounts - if (isExistingAccount) 1 else 0, sortType = this.sortType, @@ -134,12 +134,12 @@ data class AccountList private constructor( * Factory method to create an `AccountList` instance. * Validates the input to ensure the accounts list is not empty and contains exactly one main account. * - * @param userWallet the user wallet associated with the account list + * @param userWalletId the user wallet id associated with the account list * @param accounts a set of accounts belonging to the user wallet * @param totalAccounts the total number of accounts */ operator fun invoke( - userWallet: UserWallet, + userWalletId: UserWalletId, accounts: Set, totalAccounts: Int, sortType: TokensSortType = TokensSortType.NONE, @@ -169,7 +169,7 @@ data class AccountList private constructor( } AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = accounts, totalAccounts = totalAccounts, sortType = sortType, @@ -180,19 +180,19 @@ data class AccountList private constructor( /** * Factory method to create an empty [AccountList] with a main crypto portfolio account * - * @param userWallet the user wallet associated with the account list + * @param userWalletId the user wallet id associated with the account list */ fun empty( - userWallet: UserWallet, + userWalletId: UserWalletId, cryptoCurrencies: Set = emptySet(), sortType: TokensSortType = TokensSortType.NONE, groupType: TokensGroupType = TokensGroupType.NONE, ): AccountList { return AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf( Account.CryptoPortfolio.createMainAccount( - userWalletId = userWallet.walletId, + userWalletId = userWalletId, cryptoCurrencies = cryptoCurrencies, ), ), diff --git a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt index 9581d1a6fb..4d3b4dab0e 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/models/AccountStatusList.kt @@ -3,13 +3,13 @@ package com.tangem.domain.account.models import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable /** * Represents a list of account statuses associated with a user wallet * - * @property userWallet the user wallet to which the account statuses belong + * @property userWalletId the user wallet id to which the account statuses belong * @property accountStatuses a set of account statuses associated with the user wallet * @property totalAccounts the total number of accounts (including archived ones) * @property totalFiatBalance the total fiat balance across all accounts @@ -18,7 +18,7 @@ import kotlinx.serialization.Serializable */ @Serializable data class AccountStatusList( - val userWallet: UserWallet, + val userWalletId: UserWalletId, val accountStatuses: Set, val totalAccounts: Int, val totalFiatBalance: TotalFiatBalance, diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt index 0c43198fa0..60eea11794 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/models/AccountListTest.kt @@ -8,11 +8,7 @@ import com.tangem.domain.account.utils.createAccounts import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import io.mockk.clearMocks -import io.mockk.mockk -import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance @@ -31,7 +27,7 @@ class AccountListTest { val mainAccount = Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId) val accountList = AccountList( - userWallet = mockk(), + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ) @@ -49,13 +45,13 @@ class AccountListTest { fun canAddMoreAccounts() { // Arrange val accountList = AccountList( - userWallet = mockk(), + userWalletId = userWalletId, accounts = createAccounts(userWalletId = userWalletId, count = 2), totalAccounts = 2, ).getOrNull()!! val fullAccountList = AccountList( - userWallet = mockk(), + userWalletId = userWalletId, accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!! @@ -67,16 +63,13 @@ class AccountListTest { @Test fun empty() { - // Arrange - val userWallet = mockk(relaxed = true) - // Act - val actual = AccountList.empty(userWallet) + val actual = AccountList.empty(userWalletId) // Assert val expected = AccountList( - userWallet = userWallet, - accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWallet.walletId)), + userWalletId = userWalletId, + accounts = setOf(Account.CryptoPortfolio.createMainAccount(userWalletId = userWalletId)), totalAccounts = 1, ).getOrNull()!! @@ -87,19 +80,12 @@ class AccountListTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Create { - private val userWallet = mockk() - - @BeforeEach - fun resetMocks() { - clearMocks(userWallet) - } - @ParameterizedTest @MethodSource("provideTestModels") fun invoke(model: CreateTestModel) { // Act val actual = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = model.accounts, totalAccounts = model.accounts.size, ) @@ -131,13 +117,13 @@ class AccountListTest { createAccounts(userWalletId = userWalletId, count = 1).let { CreateTestModel( accounts = it, - expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 1), + expected = AccountList(userWalletId = userWalletId, accounts = it, totalAccounts = 1), ) }, createAccounts(userWalletId = userWalletId, count = 20).let { CreateTestModel( accounts = it, - expected = AccountList(userWallet = userWallet, accounts = it, totalAccounts = 20), + expected = AccountList(userWalletId = userWalletId, accounts = it, totalAccounts = 20), ) }, CreateTestModel( @@ -171,8 +157,6 @@ class AccountListTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Plus { - private val userWallet = mockk() - @ParameterizedTest @MethodSource("provideTestModels") fun invoke(model: PlusTestModel) { @@ -191,13 +175,13 @@ class AccountListTest { PlusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ).getOrNull()!!, toAdd = newAccount, expected = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount, newAccount), totalAccounts = 2, ), @@ -211,13 +195,13 @@ class AccountListTest { PlusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ).getOrNull()!!, toAdd = newAccount, expected = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(newAccount), totalAccounts = 1, ), @@ -226,7 +210,7 @@ class AccountListTest { // endregion PlusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!!, @@ -246,8 +230,6 @@ class AccountListTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Minus { - private val userWallet = mockk() - @ParameterizedTest @MethodSource("provideTestModels") fun invoke(model: MinusTestModel) { @@ -266,13 +248,13 @@ class AccountListTest { MinusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount, secondaryAccount), totalAccounts = 2, ).getOrNull()!!, toRemove = secondaryAccount, expected = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ), @@ -286,13 +268,13 @@ class AccountListTest { MinusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ).getOrNull()!!, toRemove = notInList, expected = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ), @@ -305,7 +287,7 @@ class AccountListTest { MinusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount), totalAccounts = 1, ).getOrNull()!!, @@ -321,7 +303,7 @@ class AccountListTest { MinusTestModel( initial = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = setOf(mainAccount, secondaryAccount), totalAccounts = 2, ).getOrNull()!!, diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt index 4808e416a1..32e113b374 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/AddCryptoPortfolioUseCaseTest.kt @@ -14,7 +14,6 @@ import com.tangem.domain.account.utils.createAccount import com.tangem.domain.account.utils.createAccounts import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest @@ -35,20 +34,16 @@ class AddCryptoPortfolioUseCaseTest { mainAccountTokensMigration = mainAccountTokensMigration, ) - private val userWallet = mockk() - @BeforeEach fun resetMocks() { - clearMocks(crudRepository, singleAccountListFetcher, mainAccountTokensMigration, userWallet) - - every { userWallet.walletId } returns userWalletId + clearMocks(crudRepository, singleAccountListFetcher, mainAccountTokensMigration) } @Test fun `invoke should add new crypto portfolio account to existing list`() = runTest { // Arrange val newAccount = createNewAccount() - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val updatedAccountList = (accountList + newAccount).getOrNull()!! coEvery { @@ -152,7 +147,7 @@ class AddCryptoPortfolioUseCaseTest { fun `invoke should return error if account list requirements not met`() = runTest { // Arrange val accountList = AccountList( - userWallet = userWallet, + userWalletId = userWalletId, accounts = createAccounts(userWalletId = userWalletId, count = 20), totalAccounts = 20, ).getOrNull()!! @@ -228,7 +223,7 @@ class AddCryptoPortfolioUseCaseTest { fun `invoke should return error if saveAccounts throws exception`() = runTest { // Arrange val newAccount = createNewAccount() - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val updatedAccountList = (accountList + newAccount).getOrNull()!! val exception = IllegalStateException("Test error") @@ -266,7 +261,7 @@ class AddCryptoPortfolioUseCaseTest { fun `invoke should return new account if migrate returns error`() = runTest { // Arrange val newAccount = createNewAccount() - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val updatedAccountList = (accountList + newAccount).getOrNull()!! val exception = Exception("Migration error") diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index fe67edc618..b23bd9bfe0 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -11,7 +11,6 @@ import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase.Error import com.tangem.domain.account.utils.createAccount import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest @@ -24,19 +23,17 @@ class ArchiveCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) private val useCase = ArchiveCryptoPortfolioUseCase(crudRepository) - private val userWallet = mockk() @BeforeEach fun resetMocks() { - clearMocks(crudRepository, userWallet) - every { userWallet.walletId } returns userWalletId + clearMocks(crudRepository) } @Test fun `invoke should archive existing crypto portfolio account`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! + val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!! val accountId = account.accountId val updatedAccountList = (accountList - account).getOrNull()!! @@ -103,7 +100,7 @@ class ArchiveCryptoPortfolioUseCaseTest { @Test fun `invoke should return error if account not found`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val accountId = AccountId.forCryptoPortfolio( userWalletId = userWalletId, derivationIndex = DerivationIndex(1).getOrNull()!!, @@ -126,7 +123,7 @@ class ArchiveCryptoPortfolioUseCaseTest { fun `invoke should return error if saveAccounts throws exception`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = (AccountList.empty(userWallet) + account).getOrNull()!! + val accountList = (AccountList.empty(userWalletId) + account).getOrNull()!! val accountId = account.accountId val updatedAccountList = (accountList - account).getOrNull()!! diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt index eb4e423c11..f5ede48207 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/RecoverCryptoPortfolioUseCaseTest.kt @@ -12,7 +12,6 @@ import com.tangem.domain.account.usecase.RecoverCryptoPortfolioUseCase.Error import com.tangem.domain.account.utils.createAccount import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest @@ -28,19 +27,17 @@ class RecoverCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) private val useCase = RecoverCryptoPortfolioUseCase(crudRepository) - private val userWallet = mockk() @BeforeEach fun resetMocks() { - clearMocks(crudRepository, userWallet) - every { userWallet.walletId } returns userWalletId + clearMocks(crudRepository) } @Test fun `invoke should recover archived crypto portfolio account`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val archivedAccount = ArchivedAccount( accountId = account.accountId, name = account.accountName, @@ -122,7 +119,7 @@ class RecoverCryptoPortfolioUseCaseTest { fun `invoke should return error if getArchivedAccount throws exception`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val exception = IllegalStateException("Test error") coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() @@ -146,7 +143,7 @@ class RecoverCryptoPortfolioUseCaseTest { fun `invoke should return error if getArchivedAccount returns null`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) coEvery { crudRepository.getAccountListSync(userWalletId) } returns accountList.toOption() coEvery { crudRepository.getArchivedAccountSync(account.accountId) } returns None @@ -169,7 +166,7 @@ class RecoverCryptoPortfolioUseCaseTest { fun `invoke should return error if saveAccounts throws exception`() = runTest { // Arrange val account = createAccount(userWalletId) - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val archivedAccount = ArchivedAccount( accountId = account.accountId, name = account.accountName, diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt index 3d904d3117..724429e3d3 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/UpdateCryptoPortfolioUseCaseTest.kt @@ -12,7 +12,6 @@ import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import io.mockk.* import kotlinx.coroutines.test.runTest @@ -29,19 +28,15 @@ class UpdateCryptoPortfolioUseCaseTest { private val crudRepository: AccountsCRUDRepository = mockk(relaxUnitFun = true) private val useCase = UpdateCryptoPortfolioUseCase(crudRepository = crudRepository) - private val userWallet = mockk() - @BeforeEach fun resetMocks() { - clearMocks(crudRepository, userWallet) - - every { userWallet.walletId } returns userWalletId + clearMocks(crudRepository) } @Test fun `invoke should update crypto portfolio account with new name`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -66,7 +61,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new icon`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountIcon = CryptoPortfolioIcon.ofCustomAccount( @@ -94,7 +89,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke should update crypto portfolio account with new name and icon`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -123,7 +118,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if name and icon are null`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId coEvery { crudRepository.getAccountListSync(userWalletId = userWalletId) } returns accountList.toOption() @@ -144,7 +139,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts throws exception`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! @@ -192,7 +187,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if getAccounts does not contain accountId`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = AccountId.forCryptoPortfolio( userWalletId = userWalletId, derivationIndex = DerivationIndex(1).getOrNull()!!, @@ -217,7 +212,7 @@ class UpdateCryptoPortfolioUseCaseTest { @Test fun `invoke if saveAccounts throws exception`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet = userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) val accountId = accountList.mainAccount.accountId val newAccountName = AccountName("New name").getOrNull()!! diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 9cc97e0760..e8c9a00296 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -6,6 +6,7 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.utils.lceContent import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokensGroupType @@ -39,6 +40,7 @@ import java.math.BigDecimal @OptIn(ExperimentalCoroutinesApi::class) internal class DefaultSingleAccountStatusListProducer @AssistedInject constructor( @Assisted private val params: SingleAccountStatusListProducer.Params, + private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListSupplier: SingleAccountListSupplier, private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory, private val dispatchers: CoroutineDispatcherProvider, @@ -58,8 +60,12 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo if (account.cryptoCurrencies.isEmpty()) { createEmptyAccountStatusFlow(account) } else { + val userWallet = userWalletsListRepository.userWalletsSync().first { + it.walletId == params.userWalletId + } + getAccountStatusFlow( - userWallet = accountList.userWallet, + userWallet = userWallet, account = account, groupType = accountList.groupType, sortType = accountList.sortType, @@ -71,7 +77,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo val balances = accountStatuses.map { it.tokenList.totalFiatBalance } AccountStatusList( - userWallet = accountList.userWallet, + userWalletId = accountList.userWalletId, accountStatuses = accountStatuses.toSet(), totalAccounts = accountList.totalAccounts, totalFiatBalance = TotalFiatBalanceCalculator.calculate(balances), diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt index f038816b91..18abf00e6c 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCase.kt @@ -117,7 +117,7 @@ class GetAccountCurrencyByAddressUseCase( .firstOrNull() return ensureNotNull(result) { - "No account found for network: $networkId in walletId: ${accountList.userWallet.walletId}" + "No account found for network: $networkId in walletId: ${accountList.userWalletId}" } } diff --git a/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt b/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt index ca151e1478..6601e4c64e 100644 --- a/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt +++ b/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt @@ -9,6 +9,7 @@ import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.core.utils.lceContent import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.StatusSource @@ -37,6 +38,7 @@ import java.math.BigDecimal @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultSingleAccountStatusListProducerTest { + private val userWalletsListRepository: UserWalletsListRepository = mockk() private val singleAccountListSupplier: SingleAccountListSupplier = mockk() private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory = mockk() @@ -47,6 +49,7 @@ class DefaultSingleAccountStatusListProducerTest { private val producer = DefaultSingleAccountStatusListProducer( params = SingleAccountStatusListProducer.Params(userWalletId), + userWalletsListRepository = userWalletsListRepository, singleAccountListSupplier = singleAccountListSupplier, cryptoCurrencyStatusesFlowFactory = cryptoCurrencyStatusesFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), @@ -60,7 +63,7 @@ class DefaultSingleAccountStatusListProducerTest { @Test fun `flow is mapped for user wallet id from params`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId = userWalletId) every { singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) @@ -71,7 +74,7 @@ class DefaultSingleAccountStatusListProducerTest { // Assert val expected = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = accountList.mainAccount, @@ -92,8 +95,8 @@ class DefaultSingleAccountStatusListProducerTest { @Test fun `flow will updated if balances are updated`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet) - val updatedAccountList = AccountList.empty(userWallet = userWallet, sortType = TokensSortType.BALANCE) + val accountList = AccountList.empty(userWalletId) + val updatedAccountList = AccountList.empty(userWalletId = userWalletId, sortType = TokensSortType.BALANCE) val accountListFlow = MutableStateFlow(value = accountList) @@ -106,7 +109,7 @@ class DefaultSingleAccountStatusListProducerTest { // Assert (first emission) val expected = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = accountList.mainAccount, @@ -125,7 +128,7 @@ class DefaultSingleAccountStatusListProducerTest { // Assert (second emission) val expected2 = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = updatedAccountList.mainAccount, @@ -147,7 +150,7 @@ class DefaultSingleAccountStatusListProducerTest { @Test fun `flow is filtered the same balance`() = runTest { // Arrange - val accountList = AccountList.empty(userWallet) + val accountList = AccountList.empty(userWalletId) val accountListFlow = MutableStateFlow(value = accountList) every { @@ -155,7 +158,7 @@ class DefaultSingleAccountStatusListProducerTest { } returns accountListFlow val expected = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = accountList.mainAccount, @@ -191,10 +194,12 @@ class DefaultSingleAccountStatusListProducerTest { // Arrange val cryptoCurrencyFactory = MockCryptoCurrencyFactory() val accountList = AccountList.empty( - userWallet = userWallet, + userWalletId = userWalletId, cryptoCurrencies = cryptoCurrencyFactory.ethereumAndStellar.toSet(), ) + coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet) + every { singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) } returns flowOf(accountList) @@ -220,7 +225,7 @@ class DefaultSingleAccountStatusListProducerTest { // Assert val expected = AccountStatusList( - userWallet = userWallet, + userWalletId = userWalletId, accountStatuses = setOf( AccountStatus.CryptoPortfolio( account = accountList.mainAccount, diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt index 829ce7b3ec..34b63f7842 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyByAddressUseCaseTest.kt @@ -218,7 +218,7 @@ class GetAccountCurrencyByAddressUseCaseTest { }, value = NetworkStatus.Unreachable(address = validNetworkAddress), ) - val accountList = AccountList.empty(multiUserWallet) + val accountList = AccountList.empty(userWalletId) every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet)) coEvery { @@ -253,7 +253,7 @@ class GetAccountCurrencyByAddressUseCaseTest { network = currency.network, value = NetworkStatus.Unreachable(address = validNetworkAddress), ) - val accountList = AccountList.empty(userWallet = multiUserWallet, cryptoCurrencies = setOf(currency)) + val accountList = AccountList.empty(userWalletId = userWalletId, cryptoCurrencies = setOf(currency)) every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(multiUserWallet)) coEvery { diff --git a/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt b/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt index 0dc73a77b7..22fae1a67e 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/PortfolioFetcher.kt @@ -28,7 +28,7 @@ interface PortfolioFetcher { val walletBalance: Lce, val accountsBalance: AccountStatusList, ) { - val userWallet: UserWallet get() = accountsBalance.userWallet + val userWalletId: UserWalletId get() = accountsBalance.userWalletId } sealed interface Mode { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt index 31be606a60..de73765db2 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt @@ -140,7 +140,7 @@ internal class AccountsViewModel @Inject constructor( // It's temporary solution to create main account for testing purposes val accountList = AccountList( - userWallet = userWallet, + userWalletId = userWallet.walletId, accounts = setOf( Account.CryptoPortfolio.createMainAccount(userWallet.walletId).copy( accountName = AccountName.invoke(value = "Main Account").getOrNull()!!, From e38f1e0a050953ab0bd591b3c787580ef14c1421 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 16:44:31 +0500 Subject: [PATCH 05/22] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 14 ++++------ .../tap/di/domain/YieldSupplyDomainModule.kt | 28 +++++++++++-------- ...ory.kt => DefaultYieldSupplyRepository.kt} | 22 +++++++++++++-- .../yield/supply/di/YieldSupplyDataModule.kt | 10 ++++--- ...Repository.kt => YieldSupplyRepository.kt} | 5 +++- .../usecase/YieldSupplyApyFlowUseCase.kt | 6 ++-- .../usecase/YieldSupplyApyUpdateUseCase.kt | 6 ++-- .../usecase/YieldSupplyGetApyUseCase.kt | 6 ++-- .../usecase/YieldSupplyGetChartUseCase.kt | 6 ++-- .../YieldSupplyGetTokenStatusUseCase.kt | 6 ++-- .../usecase/YieldSupplyIsAvailableUseCase.kt | 14 ++++++++++ .../supply/impl/main/entity/YieldSupplyUM.kt | 4 ++- .../impl/main/model/YieldSupplyModel.kt | 17 +++++++++-- ...ieldSupplyTokenStatusSuccessTransformer.kt | 2 +- .../impl/main/ui/YieldSupplyBlockContent.kt | 7 +++-- gradle/tangem_dependencies.toml | 2 +- 16 files changed, 103 insertions(+), 52 deletions(-) rename data/yield-supply/src/main/java/com/tangem/data/yield/supply/{DefaultYieldSupplyMarketRepository.kt => DefaultYieldSupplyRepository.kt} (76%) rename domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/{YieldSupplyMarketRepository.kt => YieldSupplyRepository.kt} (85%) create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyIsAvailableUseCase.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 0f7e759701..d1749cce71 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 @@ -16,7 +16,7 @@ import com.tangem.domain.wallets.legacy.UserWalletsListManager import com.tangem.domain.wallets.repository.WalletNamesMigrationRepository import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.domain.wallets.usecase.* -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.presentation.wallet.domain.IsWalletNFTEnabledSyncUseCase @@ -454,21 +454,17 @@ internal object WalletsDomainModule { @Provides @Singleton - fun provideYieldSupplyApyFlowUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, - ): YieldSupplyApyFlowUseCase { + fun provideYieldSupplyApyFlowUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyApyFlowUseCase { return YieldSupplyApyFlowUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, ) } @Provides @Singleton - fun provideYieldSupplyApyUpdateUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, - ): YieldSupplyApyUpdateUseCase { + fun provideYieldSupplyApyUpdateUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyApyUpdateUseCase { return YieldSupplyApyUpdateUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index 70ce92ea14..ccc23e94e3 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -4,7 +4,7 @@ import com.tangem.domain.blockaid.BlockAidGasEstimate import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.yield.supply.YieldSupplyErrorResolver -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.domain.yield.supply.usecase.* import dagger.Module @@ -82,30 +82,36 @@ internal object YieldSupplyDomainModule { @Provides @Singleton fun provideYieldSupplyGetTokenStatusUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, + yieldSupplyRepository: YieldSupplyRepository, ): YieldSupplyGetTokenStatusUseCase { return YieldSupplyGetTokenStatusUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, ) } @Provides @Singleton - fun provideYieldSupplyGetApyUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, - ): YieldSupplyGetApyUseCase { + fun provideYieldSupplyGetApyUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyGetApyUseCase { return YieldSupplyGetApyUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, ) } @Provides @Singleton - fun provideYieldSupplyGetChartUseCase( - yieldSupplyMarketRepository: YieldSupplyMarketRepository, - ): YieldSupplyGetChartUseCase { + fun provideYieldSupplyGetChartUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyGetChartUseCase { return YieldSupplyGetChartUseCase( - yieldSupplyMarketRepository = yieldSupplyMarketRepository, + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyIsAvailableUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyIsAvailableUseCase { + return YieldSupplyIsAvailableUseCase( + yieldSupplyRepository = yieldSupplyRepository, ) } } \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt similarity index 76% rename from data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt rename to data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index 641a04923e..304697ca8d 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyMarketRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -1,7 +1,9 @@ package com.tangem.data.yield.supply import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.yieldsupply.YieldSupplyProvider import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.local.yieldsupply.YieldMarketsStore @@ -10,7 +12,9 @@ import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.data.yield.supply.converters.YieldTokenStatusConverter import com.tangem.data.yield.supply.converters.YieldTokenChartConverter import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData @@ -20,11 +24,12 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import kotlin.collections.map -internal class DefaultYieldSupplyMarketRepository( +internal class DefaultYieldSupplyRepository( private val yieldSupplyApi: YieldSupplyApi, private val store: YieldMarketsStore, + private val walletManagersFacade: WalletManagersFacade, private val dispatchers: CoroutineDispatcherProvider, -) : YieldSupplyMarketRepository { +) : YieldSupplyRepository { override suspend fun getCachedMarkets(): List? = withContext(dispatchers.io) { val cache = store.getSyncOrNull().orEmpty() @@ -57,6 +62,17 @@ internal class DefaultYieldSupplyMarketRepository( return YieldTokenChartConverter.convert(response) } + override suspend fun isYieldSupplySupported(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean = + withContext(dispatchers.io) { + val walletManager = walletManagersFacade.getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = cryptoCurrency.network.toBlockchain(), + derivationPath = cryptoCurrency.network.derivationPath.value, + ) ?: error("Wallet manager not found") + + (walletManager as? YieldSupplyProvider)?.isSupported() ?: false + } + private fun List.enrichNetworkIds(): List { val chainIdMap = Blockchain.entries.associate { it.getChainId() to it.toNetworkId() } return this.map { token -> diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt index 8888eba918..69ce23f67e 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/di/YieldSupplyDataModule.kt @@ -1,12 +1,12 @@ package com.tangem.data.yield.supply.di -import com.tangem.data.yield.supply.DefaultYieldSupplyMarketRepository +import com.tangem.data.yield.supply.DefaultYieldSupplyRepository import com.tangem.data.yield.supply.DefaultYieldSupplyErrorResolver import com.tangem.data.yield.supply.DefaultYieldSupplyTransactionRepository import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.datasource.local.yieldsupply.YieldMarketsStore import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.YieldSupplyErrorResolver import com.tangem.domain.yield.supply.YieldSupplyTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -37,12 +37,14 @@ internal object YieldSupplyDataModule { fun provideYieldSupplyMarketRepository( yieldSupplyApi: YieldSupplyApi, store: YieldMarketsStore, + walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, - ): YieldSupplyMarketRepository { - return DefaultYieldSupplyMarketRepository( + ): YieldSupplyRepository { + return DefaultYieldSupplyRepository( yieldSupplyApi = yieldSupplyApi, store = store, dispatchers = dispatchers, + walletManagersFacade = walletManagersFacade, ) } diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt similarity index 85% rename from domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt rename to domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt index 5d26c82073..e751e75a1c 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyMarketRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt @@ -1,12 +1,13 @@ package com.tangem.domain.yield.supply import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.yield.supply.models.YieldMarketToken import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData import kotlinx.coroutines.flow.Flow -interface YieldSupplyMarketRepository { +interface YieldSupplyRepository { /** * Get cached yield markets or null if nothing cached yet. @@ -35,4 +36,6 @@ interface YieldSupplyMarketRepository { */ @Throws suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData + + suspend fun isYieldSupplySupported(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt index 9efcab1ea6..d93fcca1a1 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyFlowUseCase.kt @@ -1,6 +1,6 @@ package com.tangem.domain.yield.supply.usecase -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -12,11 +12,11 @@ import kotlinx.coroutines.flow.map * - value: APY as string */ class YieldSupplyApyFlowUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { operator fun invoke(): Flow> { - return yieldSupplyMarketRepository.getMarketsFlow() + return yieldSupplyRepository.getMarketsFlow() .map { yieldMarketTokenList -> yieldMarketTokenList.filter { it.isActive }.associate { token -> token.yieldSupplyKey to token.apy.toString() diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt index a3d9200e44..d2dff0ea7f 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyApyUpdateUseCase.kt @@ -1,7 +1,7 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import kotlin.collections.filter /** @@ -12,11 +12,11 @@ import kotlin.collections.filter * - value: APY as string */ class YieldSupplyApyUpdateUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { suspend operator fun invoke(): Either> = Either.catch { - yieldSupplyMarketRepository.updateMarkets() + yieldSupplyRepository.updateMarkets() .filter { it.isActive } .associate { it.tokenAddress to it.apy.toString() diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetApyUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetApyUseCase.kt index 6bb5070262..14f176b3ec 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetApyUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetApyUseCase.kt @@ -1,14 +1,14 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository class YieldSupplyGetApyUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { suspend operator fun invoke(tokenAddress: String): Either = Either.catch { - val apys = yieldSupplyMarketRepository.getCachedMarkets() ?: yieldSupplyMarketRepository.updateMarkets() + val apys = yieldSupplyRepository.getCachedMarkets() ?: yieldSupplyRepository.updateMarkets() apys.first { it.tokenAddress == tokenAddress }.apy.toString() } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetChartUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetChartUseCase.kt index 06cb773d1c..3d1142fb98 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetChartUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetChartUseCase.kt @@ -2,15 +2,15 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldSupplyMarketChartData class YieldSupplyGetChartUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { suspend operator fun invoke(cryptoCurrency: CryptoCurrency.Token): Either = Either.catch { - yieldSupplyMarketRepository.getTokenChart(cryptoCurrency) + yieldSupplyRepository.getTokenChart(cryptoCurrency) } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt index 3a6d689924..c76f4c8e71 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetTokenStatusUseCase.kt @@ -2,14 +2,14 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.yield.supply.YieldSupplyMarketRepository +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldMarketTokenStatus class YieldSupplyGetTokenStatusUseCase( - private val yieldSupplyMarketRepository: YieldSupplyMarketRepository, + private val yieldSupplyRepository: YieldSupplyRepository, ) { suspend operator fun invoke(token: CryptoCurrency.Token): Either = Either.catch { - yieldSupplyMarketRepository.getTokenStatus(token) + yieldSupplyRepository.getTokenStatus(token) } } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyIsAvailableUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyIsAvailableUseCase.kt new file mode 100644 index 0000000000..4b7fc6e168 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyIsAvailableUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.yield.supply.usecase + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplyIsAvailableUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + return yieldSupplyRepository.isYieldSupplySupported(userWalletId, cryptoCurrency) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt index de7a778530..192ea9551a 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/entity/YieldSupplyUM.kt @@ -6,7 +6,9 @@ import com.tangem.core.ui.extensions.TextReference @Immutable internal sealed class YieldSupplyUM { - data class Initial( + data object Initial : YieldSupplyUM() + + data class Available( val title: TextReference, val onClick: () -> Unit, ) : YieldSupplyUM() diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index 829f65d97f..fb7fbdf51f 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -17,6 +17,7 @@ import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase import com.tangem.features.yield.supply.api.YieldSupplyComponent import com.tangem.features.yield.supply.impl.main.entity.YieldSupplyUM import com.tangem.features.yield.supply.impl.main.entity.LoadingStatusMode @@ -45,12 +46,13 @@ internal class YieldSupplyModel @Inject constructor( @DelayedWork private val coroutineScope: CoroutineScope, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, + private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase, ) : Model(), YieldSupplyClickIntents { private val params = paramsContainer.require() val uiState: StateFlow - field = MutableStateFlow(YieldSupplyUM.Loading) + field = MutableStateFlow(YieldSupplyUM.Initial) val bottomSheetNavigation: SlotNavigation = SlotNavigation() @@ -69,8 +71,17 @@ internal class YieldSupplyModel @Inject constructor( field = MutableStateFlow(false) init { - subscribeOnCurrencyStatusUpdates() - subscribeOnBalanceHidden() + checkIfYieldSupplyIsAvailable() + } + + private fun checkIfYieldSupplyIsAvailable() { + modelScope.launch(dispatchers.io) { + val isAvailable = yieldSupplyIsAvailableUseCase(params.userWalletId, params.cryptoCurrency) + if (isAvailable) { + subscribeOnCurrencyStatusUpdates() + subscribeOnBalanceHidden() + } + } } private fun subscribeOnCurrencyStatusUpdates() { diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt index 42549b2060..f45986dc3c 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/transformers/YieldSupplyTokenStatusSuccessTransformer.kt @@ -20,7 +20,7 @@ internal class YieldSupplyTokenStatusSuccessTransformer( return when (mode) { LoadingStatusMode.Initial -> { - YieldSupplyUM.Initial( + YieldSupplyUM.Available( title = resourceReference( id = R.string.yield_module_token_details_earn_notification_title, formatArgs = wrappedList(tokenStatus.apy), diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt index df41b8fb28..e044b1c598 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/ui/YieldSupplyBlockContent.kt @@ -44,7 +44,7 @@ internal fun YieldSupplyBlockContent( modifier = modifier, ) { supplyUM -> when (supplyUM) { - is YieldSupplyUM.Initial -> SupplyInitial(supplyUM) + is YieldSupplyUM.Available -> SupplyAvailable(supplyUM) YieldSupplyUM.Loading -> SupplyLoading() is YieldSupplyUM.Content -> SupplyContent(supplyUM, isBalanceHidden) YieldSupplyUM.Processing.Enter -> SupplyProcessing( @@ -54,12 +54,13 @@ internal fun YieldSupplyBlockContent( resourceReference(R.string.yield_module_stop_earning), ) YieldSupplyUM.Unavailable -> SupplyUnavailable() + YieldSupplyUM.Initial -> {} } } } @Composable -private fun SupplyInitial(supplyUM: YieldSupplyUM.Initial) { +private fun SupplyAvailable(supplyUM: YieldSupplyUM.Available) { SupplyInfo( title = supplyUM.title, subtitle = resourceReference(R.string.yield_module_token_details_earn_notification_description), @@ -292,7 +293,7 @@ private fun YieldSupplyBlockContent_Preview(@PreviewParameter(PreviewProvider::c private class PreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - YieldSupplyUM.Initial( + YieldSupplyUM.Available( title = TextReference.Res( R.string.yield_module_token_details_earn_notification_title, wrappedList("5.1"), diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 289f92fb06..9dabe4209d 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-1254" +tangemBlockchainSdk = "develop-1256" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-564" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 5d21b088c49bda0640b13f05cb50db3d646ce383 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 15:30:14 +0300 Subject: [PATCH 06/22] Updated on 2026-08-14 --- .../com/tangem/common/utils/ClipboardUtils.kt | 20 ++ .../com/tangem/scenarios/BaseScenarios.kt | 9 + .../scenarios/WalletConnectScenarios.kt | 71 ++++++-- .../WalletConnectBottonSheetPageObject.kt | 2 - .../tangem/screens/WalletConnectPageObject.kt | 15 ++ .../screens/WalletConnectScanQrPageObject.kt | 23 +++ .../com/tangem/tests/WalletConnectTest.kt | 171 +++++++++++++----- .../ui/test/WalletConnectScreenTestTags.kt | 1 + .../connections/ui/WcConnectionsContent.kt | 4 +- 9 files changed, 243 insertions(+), 73 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt new file mode 100644 index 0000000000..f4648d1cc9 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/ClipboardUtils.kt @@ -0,0 +1,20 @@ +package com.tangem.common.utils + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context + +fun getClipboardText(context: Context): String? { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + return if (clipboard.hasPrimaryClip()) { + clipboard.primaryClip?.getItemAt(0)?.text?.toString() + } else { + null + } +} + +fun setClipboardText(context: Context, text: String?) { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("label", text) + clipboard.setPrimaryClip(clip) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 3212a90432..d07178f3cb 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -85,4 +85,13 @@ fun BaseTestCase.openDeviceSettingsScreen() { step("Click on 'Device settings' button") { onWalletSettingsScreen { deviceSettingsButton.clickWithAssertion() } } +} + +fun BaseTestCase.openWalletConnectScreen() { + step("Click 'More' button on TopBar") { + onTopBar { moreButton.clickWithAssertion() } + } + step("Click on 'Wallet Connect' button") { + onDetailsScreen { walletConnectButton.clickWithAssertion() } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt index 107806004a..facffbfd2d 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/WalletConnectScenarios.kt @@ -7,6 +7,7 @@ import com.tangem.screens.onWalletConnectScreen import io.qameta.allure.kotlin.Allure.step fun BaseTestCase.checkWalletConnectBottomSheet() { + waitForIdle() step("Assert 'Wallet Connect' bottom sheet title is displayed") { onWalletConnectBottomSheet { title.assertIsDisplayed() } } @@ -60,34 +61,64 @@ fun BaseTestCase.checkWalletConnectBottomSheet() { } } -fun BaseTestCase.checkWalletConnectScreen() { +fun BaseTestCase.checkWalletConnectScreen(withConnections: Boolean) { + waitForIdle() step("Assert 'Wallet Connect' title is displayed") { onWalletConnectScreen { title.assertIsDisplayed() } } - step("Assert 'More' button is displayed") { - onWalletConnectScreen { moreButton.assertIsDisplayed() } - } - step("Assert wallet name is displayed") { - onWalletConnectScreen { walletName.assertIsDisplayed() } - } - step("Assert app icon is displayed") { - onWalletConnectScreen { appIcon.assertIsDisplayed() } - } - step("Assert app name is displayed") { - onWalletConnectScreen { appName.assertIsDisplayed() } - } - step("Assert approve icon is displayed") { - onWalletConnectScreen { approveIcon.assertIsDisplayed() } - } - step("Assert app URL is displayed") { - onWalletConnectScreen { appUrl.assertIsDisplayed() } - } step("Assert 'New Connection' button is displayed") { onWalletConnectScreen { newConnectionButton.assertIsDisplayed() } } + if (withConnections) { + step("Assert 'More' button is displayed") { + onWalletConnectScreen { moreButton.assertIsDisplayed() } + } + step("Assert wallet name is displayed") { + onWalletConnectScreen { walletName.assertIsDisplayed() } + } + step("Assert app icon is displayed") { + onWalletConnectScreen { appIcon.assertIsDisplayed() } + } + step("Assert app name is displayed") { + onWalletConnectScreen { appName.assertIsDisplayed() } + } + step("Assert approve icon is displayed") { + onWalletConnectScreen { approveIcon.assertIsDisplayed() } + } + step("Assert app URL is displayed") { + onWalletConnectScreen { appUrl.assertIsDisplayed() } + } + } else { + step("Assert wallet name is not displayed") { + onWalletConnectScreen { walletName.assertIsNotDisplayed() } + } + step("Assert app icon is not displayed") { + onWalletConnectScreen { appIcon.assertIsNotDisplayed() } + } + step("Assert app name is not displayed") { + onWalletConnectScreen { appName.assertIsNotDisplayed() } + } + step("Assert approve icon is not displayed") { + onWalletConnectScreen { approveIcon.assertIsNotDisplayed() } + } + step("Assert app URL is not displayed") { + onWalletConnectScreen { appUrl.assertIsNotDisplayed() } + } + step("Assert 'Wallet Connect' image is displayed") { + onWalletConnectScreen { walletConnectImage.assertIsDisplayed() } + } + step("Assert 'No session' title is displayed") { + onWalletConnectScreen { noSessionTitle.assertIsDisplayed() } + } + step("Assert 'No session' text is displayed") { + onWalletConnectScreen { noSessionText.assertIsDisplayed() } + } + } + } fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) { + waitForIdle() step("Assert connection details title is displayed") { onWalletConnectDetailsBottomSheet { title.assertIsDisplayed() } } @@ -128,7 +159,7 @@ fun BaseTestCase.checkWalletConnectDetailsBottomSheet(dAppName: String) { onWalletConnectDetailsBottomSheet { connectedNetworkIcon.assertIsDisplayed() } } step("Assert connected dApp name: '$dAppName'") { - onWalletConnectDetailsBottomSheet { connectedNetworkName.assertTextContains(dAppName) } + onWalletConnectDetailsBottomSheet { appName.assertTextContains(dAppName) } } step("Assert connected network symbol is displayed") { onWalletConnectDetailsBottomSheet { connectedNetworkSymbol.assertIsDisplayed() } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt index c111e92406..8490c75e76 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectBottonSheetPageObject.kt @@ -9,13 +9,11 @@ import io.github.kakaocup.compose.node.element.ComposeScreen import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen import io.github.kakaocup.compose.node.element.KNode import io.github.kakaocup.kakao.common.utilities.getResourceString -import com.tangem.features.walletconnect.impl.R as WalletConnectImplR class WalletConnectBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : ComposeScreen(semanticsProvider = semanticsProvider) { val title: KNode = child { - hasText(getResourceString(WalletConnectImplR.string.wc_wallet_connect)) hasTestTag(WalletConnectBottomSheetTestTags.TITLE) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt index b9da100325..cb84e644f1 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectPageObject.kt @@ -53,6 +53,21 @@ class WalletConnectPageObject(semanticsProvider: SemanticsNodeInteractionsProvid hasText(getResourceString(R.string.wc_new_connection)) useUnmergedTree = true } + + val walletConnectImage: KNode = child { + hasTestTag(WalletConnectScreenTestTags.WALLET_CONNECT_IMAGE) + useUnmergedTree = true + } + + val noSessionTitle: KNode = child { + hasText(getResourceString(R.string.wc_no_sessions_title)) + useUnmergedTree = true + } + + val noSessionText: KNode = child { + hasText(getResourceString(R.string.wc_no_sessions_desc)) + useUnmergedTree = true + } } internal fun BaseTestCase.onWalletConnectScreen(function: WalletConnectPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt new file mode 100644 index 0000000000..d48c6e7e67 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/WalletConnectScanQrPageObject.kt @@ -0,0 +1,23 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.core.ui.R +import com.tangem.core.ui.test.BaseButtonTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class WalletConnectScanQrPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val pasteFromClipboardButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.wallet_connect_paste_from_clipboard)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onWalletConnectScanQrScreen(function: WalletConnectScanQrPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt index db5e364f20..13fa9b6a47 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/WalletConnectTest.kt @@ -2,12 +2,13 @@ package com.tangem.tests import com.tangem.common.BaseTestCase import com.tangem.common.constants.TestConstants.TOTAL_BALANCE -import com.tangem.common.extensions.SwipeDirection +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.extensions.swipeVertical import com.tangem.common.utils.getWcUri +import com.tangem.common.utils.setClipboardText import com.tangem.scenarios.* import com.tangem.screens.* +import com.tangem.wallet.BuildConfig import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId import io.qameta.allure.kotlin.junit4.DisplayName @@ -21,7 +22,7 @@ class WalletConnectTest : BaseTestCase() { @DisplayName("WC (React App): open session from deeplink on main screen") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Test - fun openWalletConnectSessionOnMainScreen() { + fun openWalletConnectSessionOnMainScreenTest() { val balance = TOTAL_BALANCE val dAppName = "React App" val deepLinkUri = getWcUri() @@ -37,19 +38,28 @@ class WalletConnectTest : BaseTestCase() { openAppByDeepLink(deepLinkUri) } step("Check 'Wallet Connect' bottom sheet") { - checkWalletConnectBottomSheet() + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectBottomSheet() + } + } + step("Assert 'Connect' button is enabled") { + onWalletConnectBottomSheet { connectButton.assertIsEnabled() } } step("Click on 'Connect' button") { + waitForIdle() onWalletConnectBottomSheet { connectButton.performClick() } } - step("Click 'More' button on TopBar") { - onTopBar { moreButton.clickWithAssertion() } + step("Assert 'Connect' button is not displayed") { + waitForIdle() + onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() } } - step("Click on 'Wallet Connect' button") { - onDetailsScreen { walletConnectButton.clickWithAssertion() } + step("Open 'Wallet Connect' screen") { + openWalletConnectScreen() } - step("Check 'Wallet Connect' screen") { - checkWalletConnectScreen() + step("Check 'Wallet Connect' screen with connections") { + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectScreen(withConnections = true) + } } step("Click on app icon") { onWalletConnectScreen { appIcon.performClick() } @@ -57,11 +67,11 @@ class WalletConnectTest : BaseTestCase() { step("Check 'Wallet Connect' details bottom sheet") { checkWalletConnectDetailsBottomSheet(dAppName) } - step("Click on 'Disconnect button' is displayed") { + step("Click on 'Disconnect' button") { onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } } - step("Assert connection is not displayed") { - onWalletConnectScreen { appName.assertIsNotDisplayed() } + step("Check 'Wallet Connect' screen without connections") { + checkWalletConnectScreen(withConnections = false) } } } @@ -70,7 +80,7 @@ class WalletConnectTest : BaseTestCase() { @DisplayName("WC (React App): open session from deeplink not on main screen") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Test - fun openWalletConnectSessionNotOnMainScreen() { + fun openWalletConnectSessionNotOnMainScreenTest() { val balance = TOTAL_BALANCE val dAppName = "React App" val deepLinkUri = getWcUri() @@ -82,41 +92,44 @@ class WalletConnectTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses(balance) } - step("Click on 'Buy' button") { - onMainScreen { buyButton.clickWithAssertion() } + step("Open 'Wallet Connect' screen") { + openWalletConnectScreen() + checkWalletConnectScreen(false) } step("Create WC session buy deeplink") { openAppByDeepLink(deepLinkUri) } step("Check 'Wallet Connect' bottom sheet") { - checkWalletConnectBottomSheet() + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectBottomSheet() + } } step("Click on 'Connect' button") { + waitForIdle() onWalletConnectBottomSheet { connectButton.performClick() } } - step("Click 'More' button on TopBar") { - onTopBar { moreButton.clickWithAssertion() } + step("Assert 'Connect' button is not displayed") { + waitForIdle() + onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() } } - step("Click on 'Wallet Connect' button") { - onDetailsScreen { walletConnectButton.clickWithAssertion() } - } - step("Assert 'Wallet Connect' bottom sheet is displayed") { - onWalletConnectBottomSheet { connectButton.clickWithAssertion() } - } - step("Check 'Wallet Connect' screen") { - checkWalletConnectScreen() + step("Check 'Wallet Connect' screen with connections") { + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectScreen(withConnections = true) + } } step("Click on app icon") { onWalletConnectScreen { appIcon.performClick() } } step("Check 'Wallet Connect' details bottom sheet") { - checkWalletConnectDetailsBottomSheet(dAppName) + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectDetailsBottomSheet(dAppName) + } } - step("Click on 'Disconnect button' is displayed") { + step("Click on 'Disconnect' button") { onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } } - step("Assert connection is not displayed") { - onWalletConnectScreen { appName.assertIsNotDisplayed() } + step("Check 'Wallet Connect' screen without connections") { + checkWalletConnectScreen(withConnections = false) } } } @@ -125,9 +138,10 @@ class WalletConnectTest : BaseTestCase() { @DisplayName("WC (React App): open session from deeplink ") @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") @Test - fun openWalletConnectSession() { + fun openWalletConnectSessionTest() { val balance = TOTAL_BALANCE val dAppName = "React App" + val packageName = BuildConfig.APPLICATION_ID val deepLinkUri = getWcUri() setupHooks().run { @@ -137,32 +151,28 @@ class WalletConnectTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses(balance) } - step("Open recent apps") { - device.uiDevice.pressRecentApps() - } - step("Stop app by swipe") { - swipeVertical(SwipeDirection.UP, startHeightRatio = 0.8f) + step("Kill app") { + device.apps.kill(packageName) } step("Create WC session buy deeplink") { openAppByDeepLink(deepLinkUri) } - step("Open 'Main Screen'") { - openMainScreen() - } step("Check 'Wallet Connect' bottom sheet") { - checkWalletConnectBottomSheet() + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectBottomSheet() + } } step("Click on 'Connect' button") { onWalletConnectBottomSheet { connectButton.performClick() } } - step("Click 'More' button on TopBar") { - onTopBar { moreButton.clickWithAssertion() } + step("Assert 'Connect' button is not displayed") { + onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() } } - step("Click on 'Wallet Connect' button") { - onDetailsScreen { walletConnectButton.clickWithAssertion() } + step("Open 'Wallet Connect' screen") { + openWalletConnectScreen() } - step("Check 'Wallet Connect' screen") { - checkWalletConnectScreen() + step("Check 'Wallet Connect' screen with connections") { + checkWalletConnectScreen(withConnections = true) } step("Click on app icon") { onWalletConnectScreen { appIcon.performClick() } @@ -170,11 +180,72 @@ class WalletConnectTest : BaseTestCase() { step("Check 'Wallet Connect' details bottom sheet") { checkWalletConnectDetailsBottomSheet(dAppName) } - step("Click on 'Disconnect button' is displayed") { + step("Click on 'Disconnect' button") { onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } } - step("Assert connection is not displayed") { - onWalletConnectScreen { appName.assertIsNotDisplayed() } + step("Check 'Wallet Connect' screen without connections") { + checkWalletConnectScreen(withConnections = false) + } + } + } + + @AllureId("887") + @DisplayName("WC: open session by 'Paste from clipboard' button") + @Ignore("TODO [REDACTED_JIRA] React app deeplink doesn't work") + @Test + fun openWalletConnectSessionByClipboardLinkTest() { + val balance = TOTAL_BALANCE + val dAppName = "React App" + val context = device.context + val deepLinkUri = getWcUri() + + setupHooks().run { + step("Set URI to clipboard") { + setClipboardText(context, deepLinkUri) + } + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses(balance) + } + step("Open 'Wallet Connect' screen") { + openWalletConnectScreen() + } + step("Click 'New connection' button") { + onWalletConnectScreen { newConnectionButton.performClick() } + } + step("CLick 'Paste from clipboard' button") { + onWalletConnectScanQrScreen { pasteFromClipboardButton.clickWithAssertion() } + } + step("Check 'Wallet Connect' bottom sheet") { + waitForIdle() + flakySafely(WAIT_UNTIL_TIMEOUT) { + checkWalletConnectBottomSheet() + } + } + step("Click on 'Connect' button") { + waitForIdle() + onWalletConnectBottomSheet { connectButton.performClick() } + } + step("Assert 'Connect' button is not displayed") { + waitForIdle() + onWalletConnectBottomSheet { connectButton.assertIsNotDisplayed() } + } + step("Check 'Wallet Connect' screen with connections") { + checkWalletConnectScreen(withConnections = true) + } + step("Click on app icon") { + onWalletConnectScreen { appIcon.performClick() } + } + step("Check 'Wallet Connect' details bottom sheet") { + checkWalletConnectDetailsBottomSheet(dAppName) + } + step("Click on 'Disconnect' button") { + onWalletConnectDetailsBottomSheet { disconnectButton.performClick() } + } + step("Check 'Wallet Connect' screen without connections") { + checkWalletConnectScreen(withConnections = false) } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt index 8280fc5021..53400f7588 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/WalletConnectScreenTestTags.kt @@ -7,4 +7,5 @@ object WalletConnectScreenTestTags { const val APP_NAME = "WALLET_CONNECT_SCREEN_APP_NAME" const val APPROVE_ICON = "WALLET_CONNECT_SCREEN_APPROVE_ICON" const val APP_URL = "WALLET_CONNECT_SCREEN_APP_URL" + const val WALLET_CONNECT_IMAGE = "WALLET_CONNECT_SCREEN_WALLET_CONNECT_IMAGE" } \ 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 f079b379c0..8afd70342c 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 @@ -110,7 +110,9 @@ private fun EmptyConnectionsBlock(onNewConnectionClick: () -> Unit, modifier: Mo Image( painter = painterResource(R.drawable.img_wallet_connect_76), contentDescription = "Wallet Connect", - modifier = Modifier.size(76.dp), + modifier = Modifier + .size(76.dp) + .testTag(WalletConnectScreenTestTags.WALLET_CONNECT_IMAGE), ) Text( modifier = Modifier.padding(top = TangemTheme.dimens.spacing24), From 16156c559a66ed6434d85b17e13378b0a42c781e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 19:39:10 +0500 Subject: [PATCH 07/22] Updated on 2026-08-14 --- .../impl/common/entity/YieldSupplyFeeUM.kt | 2 ++ .../model/YieldSupplyStartEarningModel.kt | 25 ++++++++++++++----- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt index 14baa04a9a..d00eede2f9 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/entity/YieldSupplyFeeUM.kt @@ -5,6 +5,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList +import java.math.BigDecimal @Immutable internal sealed class YieldSupplyFeeUM { @@ -27,4 +28,5 @@ internal data class YieldSupplyActionUM( val yieldSupplyFeeUM: YieldSupplyFeeUM, val isPrimaryButtonEnabled: Boolean, val isTransactionSending: Boolean, + val maxFee: BigDecimal = BigDecimal.ZERO, ) \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index a82ff670c8..e052894b7f 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -8,8 +8,10 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.utils.parseToBigDecimal import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.FetchCurrencyStatusUseCase @@ -18,6 +20,7 @@ import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory @@ -55,6 +58,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, + private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStartEarningComponent.Params = paramsContainer.require() @@ -106,13 +110,26 @@ internal class YieldSupplyStartEarningModel @Inject constructor( } } + private suspend fun getMaxFee(): BigDecimal? { + if (uiState.value.maxFee != BigDecimal.ZERO) return uiState.value.maxFee + val yieldTokenStatus = yieldSupplyGetTokenStatusUseCase(cryptoCurrency as CryptoCurrency.Token) + .getOrNull() + return yieldTokenStatus?.maxFeeNative?.parseToBigDecimal(cryptoCurrency.decimals) + } + private suspend fun onLoadFee() { if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading || uiState.value.isTransactionSending) return + val maxFee = if (uiState.value.maxFee == BigDecimal.ZERO) { + getMaxFee() + } else { + uiState.value.maxFee + } ?: return + val transactionListData = yieldSupplyStartEarningUseCase( userWalletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus, - maxNetworkFee = MAX_NETWORK_FEE, + maxNetworkFee = maxFee, ).getOrNull() ?: return uiState.update { @@ -146,7 +163,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( appCurrency = appCurrency, updatedTransactionList = updatedTransactionList, feeValue = feeSum, - maxNetworkFee = MAX_NETWORK_FEE, + maxNetworkFee = maxFee, ), ) yieldSupplyNotificationsUpdateTrigger.triggerUpdate( @@ -274,8 +291,4 @@ internal class YieldSupplyStartEarningModel @Inject constructor( popBack = params.callback::onBackClick, ) } - - private companion object { - val MAX_NETWORK_FEE: BigDecimal = BigDecimal.TEN // TODO replace with value from api - } } \ No newline at end of file From bbd0c7927dfd13bf1bbca236672f1f230460607c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 15 Sep 2025 18:46:49 +0500 Subject: [PATCH 08/22] Updated on 2026-08-14 --- features/walletconnect/impl/build.gradle.kts | 11 ++-- .../entity/approve/WcSpendAllowanceUM.kt | 1 + .../model/WcSendTransactionModel.kt | 19 +++++- .../ui/approve/WcCustomAllowanceContent.kt | 1 + .../blockaid/TransactionCheckResultsItem.kt | 60 ++++++++++++++++--- .../WcEstimatedWalletChangeUMConverter.kt | 3 +- .../WcSendAndReceiveBlockAidUiConverter.kt | 19 ++++-- .../blockaid/WcSpendAllowanceUMConverter.kt | 20 ++++--- .../send/WcSendTransactionModalBottomSheet.kt | 31 ++++++++++ 9 files changed, 133 insertions(+), 32 deletions(-) diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index 0bf16b6415..1d3a97b3a6 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -16,13 +16,16 @@ dependencies { implementation(projects.features.walletconnect.api) implementation(projects.features.sendV2.api) - /** Core */ - implementation(projects.core.configToggles) - implementation(projects.core.decompose) - implementation(projects.core.ui) + /** Common */ implementation(projects.common.routing) implementation(projects.common.ui) + + /** Core */ implementation(projects.core.analytics) + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.ui) /** Domain models */ implementation(projects.domain.appCurrency.models) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt index 0bdad0e725..57aef929c4 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/entity/approve/WcSpendAllowanceUM.kt @@ -11,4 +11,5 @@ internal data class WcSpendAllowanceUM( val tokenSymbol: String, val tokenImageUrl: String?, val networkIconRes: Int?, + val onLearnMoreClicked: () -> Unit, ) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt index 8d36bbe2c6..8b201362c2 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSendTransactionModel.kt @@ -16,6 +16,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.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2.Icon.Type @@ -55,6 +56,7 @@ import com.tangem.features.walletconnect.transaction.entity.send.WcSendTransacti import com.tangem.features.walletconnect.transaction.routes.WcTransactionRoutes import com.tangem.features.walletconnect.transaction.ui.blockaid.WcSendAndReceiveBlockAidUiConverter import com.tangem.features.walletconnect.utils.WcNotificationsFactory +import com.tangem.utils.SupportedLanguages import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -74,11 +76,11 @@ internal class WcSendTransactionModel @Inject constructor( private val clipboardManager: ClipboardManager, private val useCaseFactory: WcRequestUseCaseFactory, private val converter: WcSendTransactionUMConverter, - private val blockAidUiConverter: WcSendAndReceiveBlockAidUiConverter, private val getFeeUseCase: GetFeeUseCase, private val getNetworkCoinUseCase: GetNetworkCoinStatusUseCase, private val notificationsFactory: WcNotificationsFactory, private val analytics: AnalyticsEventHandler, + private val urlOpener: UrlOpener, ) : Model(), WcCommonTransactionModel, FeeSelectorModelCallback { private val params = paramsContainer.require() @@ -94,6 +96,7 @@ internal class WcSendTransactionModel @Inject constructor( private var signState: WcSignState<*> by Delegates.notNull() private var wcApproval: WcApproval? = null private var sign: () -> Unit = {} + private val blockAidUiConverter = WcSendAndReceiveBlockAidUiConverter() private val feeReloadState = MutableStateFlow(false) private val signatureReceivedAnalyticsSendState = MutableStateFlow(false) private val securityStatusState = @@ -243,8 +246,9 @@ internal class WcSendTransactionModel @Inject constructor( val blockAidState = when (securityCheck) { is Lce.Content -> blockAidUiConverter.convert( WcSendAndReceiveBlockAidUiConverter.Input( - securityCheck.content.result, - if (isApproval) wcApproval?.getAmount() else null, + result = securityCheck.content.result, + approvedAmount = if (isApproval) wcApproval?.getAmount() else null, + onApproveLearnMoreClick = ::onApproveLearnMoreClick, ), ) is Lce.Error -> WcSendReceiveTransactionCheckResultsUM(isLoading = false) @@ -291,6 +295,15 @@ internal class WcSendTransactionModel @Inject constructor( stackNavigation.pop() } + private fun onApproveLearnMoreClick() { + val code = SupportedLanguages.getCurrentSupportedLanguageCode() + .takeIf { it == SupportedLanguages.RUSSIAN } + ?: SupportedLanguages.ENGLISH + + val url = "https://tangem.com/$code/blog/post/give-revoke-permission/" + urlOpener.openUrl(url) + } + private fun isMultipleSignRequired(useCase: WcSignUseCase<*>): Boolean { return if (useCase is SignRequirements) { useCase.isMultipleSignRequired() diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt index 60c4fc70c2..a4e431c1a1 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/approve/WcCustomAllowanceContent.kt @@ -255,6 +255,7 @@ private class WcCustomAllowanceStateProvider : CollectionPreviewParameterProvide amountValue = BigDecimal("100"), tokenSymbol = "ETH", isUnlimited = false, + onLearnMoreClicked = {}, ), ), ) \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt index 2b7aca4340..92dbcdd661 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/TransactionCheckResultsItem.kt @@ -3,25 +3,29 @@ package com.tangem.features.walletconnect.transaction.ui.blockaid import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withLink 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.PreviewParameterProvider import androidx.compose.ui.unit.dp -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.isNullOrEmpty -import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.walletconnect.impl.R +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.blockaid.BlockAidNotificationUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcSendReceiveTransactionCheckResultsUM import com.tangem.features.walletconnect.transaction.ui.approve.WcSpendAllowanceItem import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal @Composable internal fun TransactionCheckResultsItem( @@ -29,11 +33,7 @@ internal fun TransactionCheckResultsItem( onClickAllowToSpend: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - modifier = modifier - .fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { + Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp)) { if (item.isLoading) { WcEstimatedWalletChangesLoadingItem() } else { @@ -44,6 +44,10 @@ internal fun TransactionCheckResultsItem( WcEstimatedWalletChangesItem(item.estimatedWalletChanges) } else if (item.spendAllowance != null) { WcSpendAllowanceItem(item.spendAllowance, onClickAllowToSpend) + ApproveDescription( + modifier = Modifier.padding(bottom = 6.dp, start = 12.dp, end = 12.dp), + onLearnMoreClick = item.spendAllowance.onLearnMoreClicked, + ) } else if (!item.additionalNotification.isNullOrEmpty()) { WcEstimatedWalletChangesNotificationItem(description = item.additionalNotification) } else { @@ -53,6 +57,29 @@ internal fun TransactionCheckResultsItem( } } +@Composable +private fun ApproveDescription(modifier: Modifier = Modifier, onLearnMoreClick: () -> Unit) { + val linkText = stringResourceSafe(R.string.common_learn_more) + val fullString = stringResourceSafe(R.string.wc_approve_description) + val defaultColor = TangemTheme.colors.text.tertiary + val linkColor = TangemTheme.colors.text.accent + Text( + modifier = modifier, + style = TangemTheme.typography.caption2, + text = buildAnnotatedString { + appendColored(fullString, defaultColor) + appendSpace() + withLink( + link = LinkAnnotation.Clickable( + tag = "WC_APPROVE_LEARN_MORE_TAG", + linkInteractionListener = { onLearnMoreClick() }, + ), + block = { appendColored(text = linkText, color = linkColor) }, + ) + }, + ) +} + @Composable @Preview(showBackground = true, device = Devices.PIXEL_7_PRO) @Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -95,5 +122,22 @@ private class TransactionCheckResultsItemProvider : PreviewParameterProvider { override fun convert(value: Input): WcEstimatedWalletChangeUM { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt index 3ef6afbf4c..e8777d39f3 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSendAndReceiveBlockAidUiConverter.kt @@ -20,15 +20,16 @@ import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal -import javax.inject.Inject private const val DECIMALS_AMOUNT = 2 @Suppress("CyclomaticComplexMethod", "LongMethod") -internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( - private val estimatedWalletChangeUMConverter: WcEstimatedWalletChangeUMConverter, - private val spendAllowanceUMConverter: WcSpendAllowanceUMConverter, -) : Converter { +internal class WcSendAndReceiveBlockAidUiConverter : + Converter { + + private val estimatedWalletChangeUMConverter = WcEstimatedWalletChangeUMConverter() + private val spendAllowanceUMConverter = WcSpendAllowanceUMConverter() + override fun convert(value: Input): WcSendReceiveTransactionCheckResultsUM { val description = value.result.description?.let { if (it.isNotEmpty()) TextReference.Str(it) else null } val simulation = value.result.simulation @@ -118,7 +119,12 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( when (data) { is SimulationData.SendAndReceive, SimulationData.NoWalletChangesDetected -> null is SimulationData.Approve -> value.approvedAmount?.let { - spendAllowanceUMConverter.convert(it) + spendAllowanceUMConverter.convert( + WcSpendAllowanceUMConverter.Input( + approvedAmount = it, + onLearnMoreClick = value.onApproveLearnMoreClick, + ), + ) } } }, @@ -128,6 +134,7 @@ internal class WcSendAndReceiveBlockAidUiConverter @Inject constructor( data class Input( val result: CheckTransactionResult, val approvedAmount: WcApprovedAmount?, + val onApproveLearnMoreClick: () -> Unit, ) } diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt index 4f7bf34268..e79abdf12d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/blockaid/WcSpendAllowanceUMConverter.kt @@ -6,24 +6,26 @@ import com.tangem.domain.walletconnect.model.WcApprovedAmount import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.utils.converter.Converter -import javax.inject.Inject -internal class WcSpendAllowanceUMConverter @Inject constructor() : Converter { +internal class WcSpendAllowanceUMConverter : Converter { - override fun convert(value: WcApprovedAmount): WcSpendAllowanceUM { - val amount = value.amount?.value ?: 0.0.toBigDecimal() - val isUnlimited = value.amount?.value == null + override fun convert(value: Input): WcSpendAllowanceUM { + val amount = value.approvedAmount.amount?.value ?: 0.0.toBigDecimal() + val isUnlimited = value.approvedAmount.amount?.value == null return WcSpendAllowanceUM( amountValue = amount, - amountText = if (value.amount?.value == null) { + amountText = if (value.approvedAmount.amount?.value == null) { TextReference.Res(R.string.wc_common_unlimited) } else { TextReference.Str(amount.amountText()) }, isUnlimited = isUnlimited, - tokenSymbol = value.amount?.currencySymbol ?: "", - tokenImageUrl = value.logoUrl, - networkIconRes = value.chainId?.toString()?.let { getActiveIconRes(it) }, + tokenSymbol = value.approvedAmount.amount?.currencySymbol ?: "", + tokenImageUrl = value.approvedAmount.logoUrl, + networkIconRes = value.approvedAmount.chainId?.toString()?.let { getActiveIconRes(it) }, + onLearnMoreClicked = value.onLearnMoreClick, ) } + + data class Input(val approvedAmount: WcApprovedAmount, val onLearnMoreClick: () -> Unit) } \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt index a1d574e005..1f02216d53 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/ui/send/WcSendTransactionModalBottomSheet.kt @@ -37,6 +37,7 @@ import com.tangem.features.walletconnect.connections.entity.VerifiedDAppState import com.tangem.features.walletconnect.connections.ui.WcAppInfoItem import com.tangem.features.walletconnect.impl.R import com.tangem.features.walletconnect.transaction.components.PreviewFeeSelectorBlockComponent +import com.tangem.features.walletconnect.transaction.entity.approve.WcSpendAllowanceUM import com.tangem.features.walletconnect.transaction.entity.blockaid.BlockAidNotificationUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangeUM import com.tangem.features.walletconnect.transaction.entity.blockaid.WcEstimatedWalletChangesUM @@ -51,6 +52,7 @@ import com.tangem.features.walletconnect.transaction.ui.common.WcSmallTitleItem import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestButtons import com.tangem.features.walletconnect.transaction.ui.common.WcTransactionRequestItem import kotlinx.collections.immutable.persistentListOf +import java.math.BigDecimal @Suppress("LongParameterList", "LongMethod") @Composable @@ -316,5 +318,34 @@ private class WcSendTransactionStateProvider : CollectionPreviewParameterProvide ), transactionValidationResult = ValidationResult.SAFE, ), + WcSendTransactionItemUM( + onDismiss = {}, + onSend = {}, + appInfo = WcTransactionAppInfoContentUM( + appName = "React App", + appIcon = "", + verifiedState = VerifiedDAppState.Verified {}, + appSubtitle = "react-app.walletconnect.com", + ), + estimatedWalletChanges = WcSendReceiveTransactionCheckResultsUM( + isLoading = false, + spendAllowance = WcSpendAllowanceUM( + amountValue = BigDecimal.ZERO, + isUnlimited = false, + amountText = stringReference("0.00 WPOL"), + tokenSymbol = "", + tokenImageUrl = "", + networkIconRes = 0, + onLearnMoreClicked = {}, + ), + ), + walletName = "Tangem 2.0", + networkInfo = WcNetworkInfoUM(name = "Ethereum", iconRes = R.drawable.img_eth_22), + feeState = WcTransactionFeeState.None, + address = "0xdac17f958d2ee523a2206206994597c13d831ec7", + sendEnabled = true, + feeErrorNotification = null, + transactionValidationResult = ValidationResult.SAFE, + ), ), ) \ No newline at end of file From 4534ceb54b1e53527fcc4addde2fdeb7147432da Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 17:41:56 +0200 Subject: [PATCH 09/22] Updated on 2026-08-14 --- .../moonpay/MoonpayBlockchainMapping.kt | 1 + .../core/ui/extensions/BlockchainIcons.kt | 3 +++ core/ui/src/main/res/drawable/ic_linea_22.xml | 15 +++++++++++ .../ui/src/main/res/drawable/img_linea_22.xml | 26 +++++++++++++++++++ .../data/common/network/NetworkFactory.kt | 1 + .../legacy/MercuryoBlockchainMapping.kt | 1 + .../domain/card/configs/Wallet2CardConfig.kt | 2 ++ .../card/configs/Wallet2CardConfigTest.kt | 2 ++ gradle/tangem_dependencies.toml | 2 +- .../tangem/blockchainsdk/utils/Blockchain.kt | 5 ++++ 10 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 core/ui/src/main/res/drawable/ic_linea_22.xml create mode 100644 core/ui/src/main/res/drawable/img_linea_22.xml diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt index 3a83c00641..06a188ca28 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonpayBlockchainMapping.kt @@ -160,4 +160,5 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? Pepecoin, PepecoinTestnet -> null Hyperliquid, HyperliquidTestnet -> null Quai, QuaiTestnet -> null + Linea, LineaTestnet -> null } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt index e106f177ab..d0a89131e0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt @@ -96,6 +96,7 @@ fun getActiveIconRes(blockchainId: String): Int { "pepecoin", "pepecoin/test" -> R.drawable.img_pepecoin_22 "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 "quai", "quai/test" -> R.drawable.img_quai_22 + "linea", "linea/test" -> R.drawable.img_linea_22 else -> R.drawable.ic_alert_24 } } @@ -190,6 +191,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "pepecoin-network", "pepecoin-network/test" -> R.drawable.img_pepecoin_22 "hyperliquid", "hyperliquid/test" -> R.drawable.img_hyperliquid_22 "quai", "quai/test" -> R.drawable.img_quai_22 + "linea", "linea/test" -> R.drawable.img_linea_22 else -> R.drawable.ic_alert_24 } } @@ -287,6 +289,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "pepecoin", "pepecoin/test" -> R.drawable.ic_pepecoin_22 "hyperliquid", "hyperliquid/test" -> R.drawable.ic_hyperliquid_22 "quai", "quai/test" -> R.drawable.ic_quai_22 + "linea", "linea/test" -> R.drawable.ic_linea_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_linea_22.xml b/core/ui/src/main/res/drawable/ic_linea_22.xml new file mode 100644 index 0000000000..32b14b57c5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_linea_22.xml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/img_linea_22.xml b/core/ui/src/main/res/drawable/img_linea_22.xml new file mode 100644 index 0000000000..5c2542faaa --- /dev/null +++ b/core/ui/src/main/res/drawable/img_linea_22.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index c7aff7add9..07463e771f 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -326,6 +326,7 @@ class NetworkFactory @Inject constructor( Blockchain.Pepecoin, Blockchain.PepecoinTestnet, Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet, Blockchain.Quai, Blockchain.QuaiTestnet, + Blockchain.Linea, Blockchain.LineaTestnet, -> Network.TransactionExtrasType.NONE // endregion } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt index fe654114a6..2a67f6acfe 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoBlockchainMapping.kt @@ -160,5 +160,6 @@ public val Blockchain.mercuryoNetwork: String? Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> null Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> null Blockchain.Quai, Blockchain.QuaiTestnet -> null + Blockchain.Linea, Blockchain.LineaTestnet -> null } } \ No newline at end of file diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt index 0d983f457f..1d3fa41ced 100644 --- a/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt +++ b/domain/card/src/main/kotlin/com/tangem/domain/card/configs/Wallet2CardConfig.kt @@ -212,6 +212,8 @@ data object Wallet2CardConfig : CardConfig { Blockchain.HyperliquidTestnet -> EllipticCurve.Secp256k1 Blockchain.Quai -> EllipticCurve.Secp256k1 Blockchain.QuaiTestnet -> EllipticCurve.Secp256k1 + Blockchain.Linea -> EllipticCurve.Secp256k1 + Blockchain.LineaTestnet -> EllipticCurve.Secp256k1 } } } \ No newline at end of file diff --git a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt index 8f9f3f26ff..2dd73a1cb4 100644 --- a/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt +++ b/domain/card/src/test/java/com/tangem/domain/card/configs/Wallet2CardConfigTest.kt @@ -168,6 +168,8 @@ class Wallet2CardConfigTest { Blockchain.HyperliquidTestnet to EllipticCurve.Secp256k1, Blockchain.Quai to EllipticCurve.Secp256k1, Blockchain.QuaiTestnet to EllipticCurve.Secp256k1, + Blockchain.Linea to EllipticCurve.Secp256k1, + Blockchain.LineaTestnet to EllipticCurve.Secp256k1, ) @Test diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 9dabe4209d..0c32661884 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-1256" +tangemBlockchainSdk = "develop-1257" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-564" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt index d1d5a501c6..381452eefa 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/Blockchain.kt @@ -169,6 +169,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "hyperevm/test" -> Blockchain.HyperliquidTestnet "quai-network" -> Blockchain.Quai "quai-network/test" -> Blockchain.QuaiTestnet + "linea" -> Blockchain.Linea + "linea/test" -> Blockchain.LineaTestnet else -> null } } @@ -335,6 +337,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.HyperliquidTestnet -> "hyperevm/test" Blockchain.Quai -> "quai-network" Blockchain.QuaiTestnet -> "quai-network/test" + Blockchain.Linea -> "linea" + Blockchain.LineaTestnet -> "linea/test" } } @@ -440,6 +444,7 @@ fun Blockchain.toCoinId(): String { Blockchain.Pepecoin, Blockchain.PepecoinTestnet -> "pepecoin-network" Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet -> "hyperliquid" Blockchain.Quai, Blockchain.QuaiTestnet -> "quai-network" + Blockchain.Linea, Blockchain.LineaTestnet -> "linea" } } From 480bfdd412d95d5a4b190c0c1fc51e0f147c305b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 19:47:17 +0500 Subject: [PATCH 10/22] Updated on 2026-08-14 --- .../tap/di/domain/YieldSupplyDomainModule.kt | 18 +++++++++++++ .../supply/DefaultYieldSupplyRepository.kt | 25 +++++++++++++++++++ .../yield/supply/YieldSupplyRepository.kt | 19 ++++++++++++++ .../usecase/YieldSupplyActivateUseCase.kt | 14 +++++++++++ .../usecase/YieldSupplyDeactivateUseCase.kt | 14 +++++++++++ .../impl/main/model/YieldSupplyModel.kt | 16 ++++++++++++ .../model/YieldSupplyStartEarningModel.kt | 3 +++ .../model/YieldSupplyStopEarningModel.kt | 3 +++ 8 files changed, 112 insertions(+) create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyActivateUseCase.kt create mode 100644 domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyDeactivateUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt index ccc23e94e3..839504ffbe 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/YieldSupplyDomainModule.kt @@ -114,4 +114,22 @@ internal object YieldSupplyDomainModule { yieldSupplyRepository = yieldSupplyRepository, ) } + + @Provides + @Singleton + fun provideYieldSupplyActivateUseCase(yieldSupplyRepository: YieldSupplyRepository): YieldSupplyActivateUseCase { + return YieldSupplyActivateUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } + + @Provides + @Singleton + fun provideYieldSupplyDeactivateUseCase( + yieldSupplyRepository: YieldSupplyRepository, + ): YieldSupplyDeactivateUseCase { + return YieldSupplyDeactivateUseCase( + yieldSupplyRepository = yieldSupplyRepository, + ) + } } \ No newline at end of file diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index 304697ca8d..6a7c33ebbb 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -11,6 +11,7 @@ import com.tangem.data.yield.supply.converters.YieldMarketTokenConverter import com.tangem.datasource.api.tangemTech.YieldSupplyApi import com.tangem.data.yield.supply.converters.YieldTokenStatusConverter import com.tangem.data.yield.supply.converters.YieldTokenChartConverter +import com.tangem.datasource.api.tangemTech.models.YieldSupplyChangeTokenStatusBody import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletmanager.WalletManagersFacade @@ -73,6 +74,30 @@ internal class DefaultYieldSupplyRepository( (walletManager as? YieldSupplyProvider)?.isSupported() ?: false } + override suspend fun activateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean = + withContext(dispatchers.io) { + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + ?: error("Chain id is required for evm's") + yieldSupplyApi.activateYieldModule( + YieldSupplyChangeTokenStatusBody( + tokenAddress = cryptoCurrencyToken.contractAddress, + chainId = chainId, + ), + ).getOrThrow().isActive + } + + override suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean = + withContext(dispatchers.io) { + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + ?: error("Chain id is required for evm's") + yieldSupplyApi.deactivateYieldModule( + YieldSupplyChangeTokenStatusBody( + tokenAddress = cryptoCurrencyToken.contractAddress, + chainId = chainId, + ), + ).getOrThrow().isActive + } + private fun List.enrichNetworkIds(): List { val chainIdMap = Blockchain.entries.associate { it.getChainId() to it.toNetworkId() } return this.map { token -> diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt index e751e75a1c..eceba08082 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/YieldSupplyRepository.kt @@ -38,4 +38,23 @@ interface YieldSupplyRepository { suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData suspend fun isYieldSupplySupported(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean + + /** + * Activate yield protocol for the specified token. + * + * Returns whether the token is active after the operation completes. + * May throw on network/backend errors or if required chain id cannot be resolved. + */ + @Throws + suspend fun activateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean + + /** + * Deactivate yield protocol for the specified token. + * + * Returns whether the token is active after the operation completes + * (expected to be false when deactivation succeeds). May throw on + * network/backend errors or if required chain id cannot be resolved. + */ + @Throws + suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token): Boolean } \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyActivateUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyActivateUseCase.kt new file mode 100644 index 0000000000..92b5787e7d --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyActivateUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplyActivateUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(cryptoCurrencyToken: CryptoCurrency.Token): Either = Either.catch { + yieldSupplyRepository.activateProtocol(cryptoCurrencyToken) + } +} \ No newline at end of file diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyDeactivateUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyDeactivateUseCase.kt new file mode 100644 index 0000000000..6449187071 --- /dev/null +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyDeactivateUseCase.kt @@ -0,0 +1,14 @@ +package com.tangem.domain.yield.supply.usecase + +import arrow.core.Either +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.yield.supply.YieldSupplyRepository + +class YieldSupplyDeactivateUseCase( + private val yieldSupplyRepository: YieldSupplyRepository, +) { + + suspend operator fun invoke(cryptoCurrencyToken: CryptoCurrency.Token): Either = Either.catch { + yieldSupplyRepository.deactivateProtocol(cryptoCurrencyToken) + } +} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt index fb7fbdf51f..ad5f40b8cf 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/main/model/YieldSupplyModel.kt @@ -16,6 +16,8 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.FetchCurrencyStatusUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyIsAvailableUseCase import com.tangem.features.yield.supply.api.YieldSupplyComponent @@ -47,6 +49,8 @@ internal class YieldSupplyModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, private val yieldSupplyIsAvailableUseCase: YieldSupplyIsAvailableUseCase, + private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, + private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, ) : Model(), YieldSupplyClickIntents { private val params = paramsContainer.require() @@ -162,6 +166,7 @@ internal class YieldSupplyModel @Inject constructor( val yieldTransaction = cryptoCurrencyStatus.value.pendingTransactions.firstOrNull { it.type is TxInfo.TransactionType.YieldSupply }?.type as? TxInfo.TransactionType.YieldSupply + sendInfoAboutProtocolStatus(yieldSupplyStatus?.isActive == true) val yieldSupplyUM = when { hasActiveTransaction && yieldTransaction != null -> { @@ -193,6 +198,17 @@ internal class YieldSupplyModel @Inject constructor( } } + private fun sendInfoAboutProtocolStatus(isActivated: Boolean) { + val token = cryptoCurrency as? CryptoCurrency.Token ?: return + modelScope.launch(dispatchers.default) { + if (isActivated) { + yieldSupplyActivateUseCase(token) + } else { + yieldSupplyDeactivateUseCase(token) + } + } + } + private companion object { const val PROCESSING_UPDATE_DELAY = 10_000L } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt index e052894b7f..991aa736fa 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/startearning/model/YieldSupplyStartEarningModel.kt @@ -19,6 +19,7 @@ import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyActivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyEstimateEnterFeeUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyGetTokenStatusUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyStartEarningUseCase @@ -58,6 +59,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, + private val yieldSupplyActivateUseCase: YieldSupplyActivateUseCase, private val yieldSupplyGetTokenStatusUseCase: YieldSupplyGetTokenStatusUseCase, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { @@ -206,6 +208,7 @@ internal class YieldSupplyStartEarningModel @Inject constructor( }, ifRight = { fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, cryptoCurrency.id) + yieldSupplyActivateUseCase(cryptoCurrency as CryptoCurrency.Token) modelScope.launch { params.callback.onTransactionSent() } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt index b1bba83d10..4204a8bc55 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/stopearning/model/YieldSupplyStopEarningModel.kt @@ -15,6 +15,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.transaction.usecase.GetFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.yield.supply.usecase.YieldSupplyDeactivateUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyStopEarningUseCase import com.tangem.features.yield.supply.impl.R import com.tangem.features.yield.supply.impl.common.YieldSupplyAlertFactory @@ -49,6 +50,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( private val urlOpener: UrlOpener, private val yieldSupplyNotificationsUpdateTrigger: YieldSupplyNotificationsUpdateTrigger, private val yieldSupplyAlertFactory: YieldSupplyAlertFactory, + private val yieldSupplyDeactivateUseCase: YieldSupplyDeactivateUseCase, ) : Model(), YieldSupplyNotificationsComponent.ModelCallback { private val params: YieldSupplyStopEarningComponent.Params = paramsContainer.require() @@ -134,6 +136,7 @@ internal class YieldSupplyStopEarningModel @Inject constructor( ) }, ifRight = { + yieldSupplyDeactivateUseCase(cryptoCurrency as CryptoCurrency.Token) params.callback.onTransactionSent() }, ) From 971c4196231804a3eb3a2a49ad7b1abc5bf878a6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 21:21:36 +0500 Subject: [PATCH 11/22] Updated on 2026-08-14 --- .../com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt | 2 +- .../tangem/data/yield/supply/DefaultYieldSupplyRepository.kt | 3 ++- gradle/tangem_dependencies.toml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt index 20aae40e90..2158ef7ede 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/YieldSupplyApi.kt @@ -15,7 +15,7 @@ import retrofit2.http.Query interface YieldSupplyApi { @GET("api/v1/yield/markets") - suspend fun getYieldMarkets(@Query("chainId") chainId: Int? = null): ApiResponse + suspend fun getYieldMarkets(@Query("chainId") chainId: String? = null): ApiResponse @GET("api/v1/yield/token/{chainId}/{tokenAddress}") suspend fun getYieldTokenStatus( diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index 6a7c33ebbb..8d96150927 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -39,7 +39,8 @@ internal class DefaultYieldSupplyRepository( } override suspend fun updateMarkets(): List = withContext(dispatchers.io) { - val response = yieldSupplyApi.getYieldMarkets().getOrThrow() + val chains = Blockchain.yieldSupplySupportedBlockchains().map { it.getChainId() }.joinToString(",") + val response = yieldSupplyApi.getYieldMarkets(chainId = chains).getOrThrow() val domain = response.marketDtos.map(YieldMarketTokenConverter::convert) store.store(response.marketDtos) domain diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 0c32661884..1d38e0c3a2 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-1257" +tangemBlockchainSdk = "develop-1259" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-564" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 231c97e371d8baced21d2e86b2ea448e3df5fbe4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Oct 2025 11:49:42 +0500 Subject: [PATCH 12/22] Updated on 2026-08-14 --- .../ui/components/tokenlist/TokenListItem.kt | 18 +- .../tokenlist/state/TokensListItemUM.kt | 1 + features/onramp/impl/build.gradle.kts | 1 + .../LoadingAccountTokenItemConverter.kt | 29 +++ .../SetNoAvailablePairsTransformerV2.kt | 61 ++++++ .../model/AvailableSwapPairsModel.kt | 203 ++++++++++++++++-- .../swap/entity/AccountAvailabilityTokenUM.kt | 14 ++ .../onramp/tokenlist/entity/TokenListUM.kt | 15 +- .../tokenlist/entity/TokenListUMController.kt | 1 + .../SetLoadingAccountTokenListTransformer.kt | 45 ++++ .../SetNothingToFoundStateTransformer.kt | 2 + .../SetNothingToFoundStateTransformerV2.kt | 29 +++ .../UpdateAccountTokenItemConverter.kt | 45 ++++ .../UpdateAccountTokenListTransformer.kt | 64 ++++++ .../tokenlist/model/OnrampTokenListModel.kt | 186 ++++++++++++++-- .../onramp/tokenlist/ui/OnrampTokenList.kt | 17 ++ .../ui/preview/PreviewTokenListUMProvider.kt | 2 + .../WalletCurrencyActionsClickIntents.kt | 37 ++-- .../common/preview/WalletScreenPreviewData.kt | 2 + .../converter/TokenListStateConverter.kt | 1 + 20 files changed, 717 insertions(+), 56 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/AccountAvailabilityTokenUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index ff3b869c1f..989637b8eb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -60,7 +60,7 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M @Composable fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { if (state.isExpanded) { - ExpandedPortfolioHeader(state.state, modifier) + ExpandedPortfolioHeader(state = state.state, isCollapsable = state.isCollapsable, modifier = modifier) } else { TokenItem( state = state.state, @@ -83,7 +83,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B } @Composable -private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier = Modifier) { +private fun ExpandedPortfolioHeader(state: TokenItemState, isCollapsable: Boolean, modifier: Modifier = Modifier) { Row( verticalAlignment = Alignment.CenterVertically, modifier = modifier @@ -130,11 +130,13 @@ private fun ExpandedPortfolioHeader(state: TokenItemState, modifier: Modifier = ) } - Icon( - modifier = Modifier.size(TangemTheme.dimens.size16), - painter = painterResource(id = R.drawable.ic_minimize_24), - tint = TangemTheme.colors.icon.inactive, - contentDescription = null, - ) + if (isCollapsable) { + Icon( + modifier = Modifier.size(TangemTheme.dimens.size16), + painter = painterResource(id = R.drawable.ic_minimize_24), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + } } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt index 24da6e9d28..23b2ef9da6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -43,6 +43,7 @@ sealed interface TokensListItemUM { data class Portfolio( val state: TokenItemState, val isExpanded: Boolean, + val isCollapsable: Boolean, val tokens: List, ) : TokensListItemUM { override val id: String = state.id diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 8280707bae..e1d7b97469 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -46,6 +46,7 @@ dependencies { implementation(projects.domain.wallets.models) implementation(projects.domain.settings) implementation(projects.domain.transaction.models) + implementation(projects.domain.account.status) /** DI */ implementation(deps.hilt.android) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt new file mode 100644 index 0000000000..8d9c2042a9 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt @@ -0,0 +1,29 @@ +package com.tangem.features.onramp.swap.availablepairs.entity.converters + +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountStatus +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class LoadingAccountTokenItemConverter( + private val appCurrency: AppCurrency, +) : Converter { + + override fun convert(value: AccountStatus.CryptoPortfolio): TokensListItemUM.Portfolio { + val (account, currencies) = value + + return TokensListItemUM.Portfolio( + state = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = account, + onItemClick = null, + ).convert(TotalFiatBalance.Failed), + isExpanded = true, + isCollapsable = false, + tokens = currencies.flattenCurrencies().map(LoadingTokenListItemConverter::convert).toPersistentList(), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt new file mode 100644 index 0000000000..669604724c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt @@ -0,0 +1,61 @@ +package com.tangem.features.onramp.swap.availablepairs.entity.transformers + +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class SetNoAvailablePairsTransformerV2( + private val appCurrency: AppCurrency, + private val accountList: Map>, + private val isBalanceHidden: Boolean, + private val isAccountsMode: Boolean, + private val unavailableErrorText: TextReference, +) : TokenListUMTransformer { + private val unavailableConverter = OnrampTokenItemStateConverterFactory + .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) + + override fun transform(prevState: TokenListUM): TokenListUM { + return prevState.copy( + availableItems = persistentListOf(), + unavailableItems = persistentListOf(), + tokensListData = if (isAccountsMode) { + TokenListUMData.AccountList( + tokensList = accountList.map { (account, cryptoCurrencies) -> + TokensListItemUM.Portfolio( + state = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = account, + onItemClick = null, + ).convert(TotalFiatBalance.Failed), + isExpanded = true, + isCollapsable = false, + tokens = unavailableConverter.convertList(cryptoCurrencies) + .map(TokensListItemUM::Token) + .toPersistentList(), + ) + }.toPersistentList(), + ) + } else { + TokenListUMData.TokenList( + tokensList = accountList.flatMap { (_, cryptoCurrencies) -> + unavailableConverter.convertList(cryptoCurrencies) + .map(TokensListItemUM::Token) + }.toPersistentList(), + ) + }, + isBalanceHidden = isBalanceHidden, + warning = NotificationUM.Warning.SwapNoAvailablePair, + ) + } +} \ No newline at end of file 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 d62cce58ae..8e19216bf3 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 @@ -1,12 +1,15 @@ package com.tangem.features.onramp.swap.availablepairs.model -import arrow.core.getOrElse import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -15,6 +18,8 @@ import com.tangem.domain.core.utils.getOrElse 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.models.account.Account +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList @@ -28,11 +33,13 @@ import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponen import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetLoadingTokenItemsTransformer import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformer +import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetNoAvailablePairsTransformerV2 +import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM +import com.tangem.features.onramp.swap.entity.AccountCurrencyUM import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer -import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer -import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.* import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -43,7 +50,7 @@ import javax.inject.Inject private typealias AvailablePairsState = Lce> -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class AvailableSwapPairsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, @@ -53,7 +60,10 @@ internal class AvailableSwapPairsModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getAvailablePairsUseCase: GetAvailablePairsUseCase, - private val getWalletsUseCase: GetWalletsUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val accountsFeatureToggles: AccountsFeatureToggles, + getWalletsUseCase: GetWalletsUseCase, ) : Model() { val state: StateFlow = tokenListUMController.state @@ -62,13 +72,17 @@ internal class AvailableSwapPairsModel @Inject constructor( private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } private val tokenListFlow = getTokenListUseCaseFlow() - + private val accountListFlow = getAccountListUseCaseFlow() private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) init { - initializeSearchBarCallbacks() + if (accountsFeatureToggles.isFeatureEnabled) { + subscribeOnUpdateStateV2() + } else { + subscribeOnUpdateState() + } - subscribeOnUpdateState() + initializeSearchBarCallbacks() subscribeOnAvailablePairsUpdates() } @@ -79,12 +93,20 @@ internal class AvailableSwapPairsModel @Inject constructor( maybeTokenList.getOrElse( ifLoading = { it ?: TokenList.Empty }, ifError = { TokenList.Empty }, - ) - .flattenCurrencies() + ).flattenCurrencies() } .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) } + private fun getAccountListUseCaseFlow(): SharedFlow> { + return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId)) + .distinctUntilChanged() + .map { accountStatusList -> + accountStatusList.accountStatuses.toList() + }.flowOn(dispatchers.default) + .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) + } + private fun initializeSearchBarCallbacks() { tokenListUMController.update( transformer = UpdateSearchBarCallbacksTransformer( @@ -130,6 +152,53 @@ internal class AvailableSwapPairsModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeOnUpdateStateV2() { + combine( + flow = getAccountsAndModeFlow(), + flow2 = getAppCurrencyAndBalanceHidingFlow(), + flow3 = params.selectedStatus, + flow4 = searchManager.query, + flow5 = availablePairsByNetworkFlow + .map { it[params.selectedStatus.value?.toLeastTokenInfo()] } + .distinctUntilChanged(), + ) { accountListAndMode, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState -> + val (accountList, isAccountsMode) = accountListAndMode + availablePairsState?.fold( + ifLoading = { + SetLoadingAccountTokenListTransformer( + appCurrency = appCurrencyAndBalanceHiding.first, + accountList = accountList, + isAccountsMode = isAccountsMode, + ) + }, + ifContent = { pairs -> + handleContentStateV2( + appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding, + accountList = accountList, + selectedStatus = selectedStatus, + query = query, + availablePairs = pairs, + isAccountsMode = isAccountsMode, + ) + }, + ifError = { + handleErrorStateV2( + cause = it, + networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), + accountList = accountList, + ) + }, + ) ?: SetLoadingAccountTokenListTransformer( + appCurrency = appCurrencyAndBalanceHiding.first, + accountList = accountList, + isAccountsMode = isAccountsMode, + ) + } + .onEach(tokenListUMController::update) + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + private fun handleContentState( appCurrencyAndBalanceHiding: Pair, currencies: List, @@ -176,6 +245,53 @@ internal class AvailableSwapPairsModel @Inject constructor( } } + private fun handleContentStateV2( + appCurrencyAndBalanceHiding: Pair, + accountList: List, + selectedStatus: CryptoCurrencyStatus?, + query: String, + availablePairs: List, + isAccountsMode: Boolean, + ): TokenListUMTransformer { + val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding + + val filterByQueryAccountList = accountList.associate { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.account to accountStatus.tokenList.flattenCurrencies() + .filter { it.currency != selectedStatus?.currency } + .filterByQuery(query = query) + } + } + + if (availablePairs.isEmpty()) { + return SetNoAvailablePairsTransformerV2( + appCurrency = appCurrency, + accountList = filterByQueryAccountList, + unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), + isBalanceHidden = isBalanceHidden, + isAccountsMode = isAccountsMode, + ) + } + + return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) { + SetNothingToFoundStateTransformerV2( + isBalanceHidden = isBalanceHidden, + emptySearchMessageReference = resourceReference( + id = R.string.action_buttons_swap_empty_search_message, + ), + ) + } else { + UpdateAccountTokenListTransformer( + appCurrency = appCurrency, + onItemClick = params.onTokenClick, + accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs), + isBalanceHidden = isBalanceHidden, + unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), + isAccountsMode = isAccountsMode, + ) + } + } + private fun handleErrorState( cause: Throwable, networkInfo: LeastTokenInfo?, @@ -193,6 +309,26 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } + private fun handleErrorStateV2( + cause: Throwable, + networkInfo: LeastTokenInfo?, + accountList: List, + ): SetErrorWarningTransformer { + return SetErrorWarningTransformer( + cause = cause, + onRefresh = { + modelScope.launch { + if (networkInfo != null) { + accountList.filterIsInstance() + .forEach { (_, currencies) -> + updateAvailablePairs(networkInfo, currencies.flattenCurrencies()) + } + } + } + }, + ) + } + private fun subscribeOnAvailablePairsUpdates() { modelScope.launch { params.selectedStatus @@ -203,9 +339,19 @@ internal class AvailableSwapPairsModel @Inject constructor( val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true if (isAlreadyLoaded) return@collectLatest - val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest - - updateAvailablePairs(networkInfo = networkInfo, statuses = statuses) + if (accountsFeatureToggles.isFeatureEnabled) { + val accountList = accountListFlow.firstOrNull() ?: return@collectLatest + updateAvailablePairs( + networkInfo = networkInfo, + statuses = accountList.filterIsInstance() + .flatMap { accountStatus -> + accountStatus.flattenCurrencies() + }.toSet().toList(), + ) + } else { + val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest + updateAvailablePairs(networkInfo = networkInfo, statuses = statuses) + } } } } @@ -247,6 +393,14 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } + private fun getAccountsAndModeFlow(): Flow, Boolean>> { + return combine( + flow = accountListFlow.distinctUntilChanged(), + flow2 = isAccountsModeEnabledUseCase().distinctUntilChanged(), + transform = ::Pair, + ) + } + private fun onSearchQueryChange(newQuery: String) { if (state.value.searchBarUM.query == newQuery) return @@ -286,6 +440,29 @@ internal class AvailableSwapPairsModel @Inject constructor( } } + private fun Map>.filterByAvailability( + availablePairs: List, + ): List { + return map { (account, currencies) -> + AccountAvailabilityUM( + account = account, + currencyList = currencies.map { status -> + val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo()) + + val isAvailableToSwap = isAvailable && + status.value !is CryptoCurrencyStatus.MissedDerivation && + status.value !is CryptoCurrencyStatus.Unreachable && + !status.currency.isCustom + + AccountCurrencyUM( + cryptoCurrencyStatus = status, + isAvailable = isAvailableToSwap, + ) + }, + ) + } + } + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/AccountAvailabilityTokenUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/AccountAvailabilityTokenUM.kt new file mode 100644 index 0000000000..f0856222ba --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/AccountAvailabilityTokenUM.kt @@ -0,0 +1,14 @@ +package com.tangem.features.onramp.swap.entity + +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrencyStatus + +internal data class AccountAvailabilityUM( + val account: Account.CryptoPortfolio, + val currencyList: List, +) + +internal data class AccountCurrencyUM( + val isAvailable: Boolean, + val cryptoCurrencyStatus: CryptoCurrencyStatus, +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt index 615823f4e4..3f88743a05 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUM.kt @@ -19,6 +19,19 @@ internal data class TokenListUM( val searchBarUM: SearchBarUM, val availableItems: ImmutableList, val unavailableItems: ImmutableList, + val tokensListData: TokenListUMData, val isBalanceHidden: Boolean, val warning: NotificationUM? = null, -) \ No newline at end of file +) + +internal sealed interface TokenListUMData { + data class AccountList( + val tokensList: ImmutableList, + ) : TokenListUMData + + data class TokenList( + val tokensList: ImmutableList, + ) : TokenListUMData + + data object EmptyList : TokenListUMData +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt index 80bb1d02f0..583697cca2 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/TokenListUMController.kt @@ -31,6 +31,7 @@ internal class TokenListUMController @Inject constructor() { ), availableItems = persistentListOf(), unavailableItems = persistentListOf(), + tokensListData = TokenListUMData.EmptyList, isBalanceHidden = false, ), ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt new file mode 100644 index 0000000000..48d7b7331d --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetLoadingAccountTokenListTransformer.kt @@ -0,0 +1,45 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.account.AccountStatus +import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingAccountTokenItemConverter +import com.tangem.features.onramp.swap.availablepairs.entity.converters.LoadingTokenListItemConverter +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class SetLoadingAccountTokenListTransformer( + appCurrency: AppCurrency, + private val accountList: List, + private val isAccountsMode: Boolean, +) : TokenListUMTransformer { + + private val accountListItemConverter = LoadingAccountTokenItemConverter(appCurrency) + + override fun transform(prevState: TokenListUM): TokenListUM { + return prevState.copy( + availableItems = persistentListOf(), + unavailableItems = persistentListOf(), + tokensListData = if (isAccountsMode) { + TokenListUMData.AccountList( + tokensList = accountListItemConverter.convertList( + accountList.filterIsInstance(), + ).toPersistentList(), + ) + } else { + TokenListUMData.TokenList( + tokensList = accountList.flatMap { account -> + when (account) { + is AccountStatus.CryptoPortfolio -> LoadingTokenListItemConverter.convertList( + account.tokenList.flattenCurrencies(), + ) + } + }.toPersistentList(), + ) + }, + warning = null, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt index 2b2c7963eb..fcaa9d6147 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -28,6 +29,7 @@ internal class SetNothingToFoundStateTransformer( ).let(::add) }.toImmutableList(), unavailableItems = persistentListOf(), + tokensListData = TokenListUMData.EmptyList, isBalanceHidden = isBalanceHidden, ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt new file mode 100644 index 0000000000..dafa2ac842 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt @@ -0,0 +1,29 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class SetNothingToFoundStateTransformerV2( + private val isBalanceHidden: Boolean, + private val emptySearchMessageReference: TextReference, +) : TokenListUMTransformer { + + override fun transform(prevState: TokenListUM): TokenListUM { + return prevState.copy( + availableItems = persistentListOf(), + unavailableItems = persistentListOf(), + tokensListData = TokenListUMData.TokenList(tokensList = buildList { + TokensListItemUM.Text( + id = emptySearchMessageReference.hashCode(), + text = emptySearchMessageReference, + ).let(::add) + }.toImmutableList()), + isBalanceHidden = isBalanceHidden, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt new file mode 100644 index 0000000000..2389d1aec9 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt @@ -0,0 +1,45 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList + +internal class UpdateAccountTokenItemConverter( + private val appCurrency: AppCurrency, + private val unavailableErrorText: TextReference, + onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, +) : Converter { + + private val availableConverter = OnrampTokenItemStateConverterFactory + .createAvailableItemConverter(appCurrency, onItemClick) + + private val unavailableConverter = OnrampTokenItemStateConverterFactory + .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) + + override fun convert(value: AccountAvailabilityUM): TokensListItemUM.Portfolio { + return TokensListItemUM.Portfolio( + state = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = value.account, + onItemClick = null, + ).convert(TotalFiatBalance.Failed), + isExpanded = true, + isCollapsable = false, + tokens = value.currencyList.asSequence().map { (isAvailable, status) -> + if (isAvailable) { + availableConverter.convert(status) + } else { + unavailableConverter.convert(status) + } + }.map(TokensListItemUM::Token).toPersistentList(), + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt new file mode 100644 index 0000000000..5b13ba643c --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenListTransformer.kt @@ -0,0 +1,64 @@ +package com.tangem.features.onramp.tokenlist.entity.transformer + +import com.tangem.common.ui.notifications.NotificationUM +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData +import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList + +internal class UpdateAccountTokenListTransformer( + private val appCurrency: AppCurrency, + private val onItemClick: (TokenItemState, CryptoCurrencyStatus) -> Unit, + private val accountList: List, + private val isBalanceHidden: Boolean, + private val unavailableErrorText: TextReference, + private val warning: NotificationUM? = null, + private val isAccountsMode: Boolean, +) : TokenListUMTransformer { + + private val accountListItemConverter = UpdateAccountTokenItemConverter( + appCurrency = appCurrency, + onItemClick = onItemClick, + unavailableErrorText = unavailableErrorText, + ) + + private val availableConverter = OnrampTokenItemStateConverterFactory + .createAvailableItemConverter(appCurrency, onItemClick) + + private val unavailableConverter = OnrampTokenItemStateConverterFactory + .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) + + override fun transform(prevState: TokenListUM): TokenListUM { + return prevState.copy( + availableItems = persistentListOf(), + unavailableItems = persistentListOf(), + tokensListData = if (isAccountsMode) { + TokenListUMData.AccountList( + tokensList = accountListItemConverter.convertList(accountList).toPersistentList(), + ) + } else { + TokenListUMData.TokenList( + tokensList = accountList.flatMap { (_, currencyList) -> + currencyList.asSequence().map { (isAvailable, status) -> + if (isAvailable) { + availableConverter.convert(status) + } else { + unavailableConverter.convert(status) + } + }.map(TokensListItemUM::Token).toPersistentList() + }.toPersistentList(), + ) + }, + isBalanceHidden = isBalanceHidden, + warning = warning, + ) + } +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt index ae9b3f2e32..a61e5837a8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/model/OnrampTokenListModel.kt @@ -6,6 +6,11 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -13,6 +18,8 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.settings.usercountry.GetUserCountryUseCase @@ -23,13 +30,11 @@ import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM +import com.tangem.features.onramp.swap.entity.AccountCurrencyUM import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent -import com.tangem.features.onramp.tokenlist.entity.OnrampOperation -import com.tangem.features.onramp.tokenlist.entity.TokenListUM -import com.tangem.features.onramp.tokenlist.entity.TokenListUMController -import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer -import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer -import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateTokenItemsTransformer +import com.tangem.features.onramp.tokenlist.entity.* +import com.tangem.features.onramp.tokenlist.entity.transformer.* import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -42,7 +47,9 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject -@Suppress("LongParameterList") +typealias AccountCryptoList = Map> + +@Suppress("LargeClass", "LongParameterList") internal class OnrampTokenListModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, @@ -55,6 +62,9 @@ internal class OnrampTokenListModel @Inject constructor( private val rampStateManager: RampStateManager, private val getUserCountryUseCase: GetUserCountryUseCase, private val getAssetRequirementsUseCase: GetAssetRequirementsUseCase, + private val accountsFeatureToggles: AccountsFeatureToggles, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, ) : Model() { val state: StateFlow = tokenListUMController.state @@ -71,8 +81,11 @@ internal class OnrampTokenListModel @Inject constructor( onActiveChange = ::onSearchBarActiveChange, ), ) - - subscribeOnUpdateState() + if (accountsFeatureToggles.isFeatureEnabled) { + subscribeOnUpdateStateV2() + } else { + subscribeOnUpdateState() + } } private fun subscribeOnUpdateState() { @@ -95,12 +108,7 @@ internal class OnrampTokenListModel @Inject constructor( if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) { SetNothingToFoundStateTransformer( isBalanceHidden = isBalanceHidden, - emptySearchMessageReference = when (params.filterOperation) { - OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message - OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message - OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message - } - .let(::resourceReference), + emptySearchMessageReference = getEmptySearchMessageReference(), ) } else { val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) { @@ -134,6 +142,62 @@ internal class OnrampTokenListModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeOnUpdateStateV2() { + combine( + flow = singleAccountStatusListSupplier( + SingleAccountStatusListProducer.Params(params.userWalletId), + ).distinctUntilChanged(), + flow2 = getAppCurrencyAndBalanceHidingFlow(), + flow3 = isAccountsModeEnabledUseCase(), + flow4 = searchManager.query, + flow5 = hasRestrictionForSellFlow(), + ) { accountList, appCurrencyAndBalanceHiding, isAccountsMode, query, hasRestrictionForSell -> + val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding + val filterByQueryAccountList = accountList.filterAccountsByQuery(query) + + if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) { + updateTokenListUM( + SetNothingToFoundStateTransformerV2( + isBalanceHidden = isBalanceHidden, + emptySearchMessageReference = getEmptySearchMessageReference(), + ), + ) + } else { + updateTokenListUM( + SetLoadingAccountTokenListTransformer( + appCurrency = appCurrency, + accountList = accountList.accountStatuses.toList(), + isAccountsMode = isAccountsMode, + ), + ) + updateTokenListUM( + UpdateAccountTokenListTransformer( + appCurrency = appCurrency, + onItemClick = params.onTokenClick, + accountList = filterByQueryAccountList.filterByAvailability(), + isBalanceHidden = isBalanceHidden, + unavailableErrorText = getUnavailableTokensHeaderReference(), + warning = getSellWarning( + hasRestrictionForSell = hasRestrictionForSell, + isInsufficientBalanceForSell = accountList.isInsufficientBalanceForSell(), + ), + isAccountsMode = isAccountsMode, + ), + ) + } + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + + private fun getAppCurrencyAndBalanceHidingFlow(): Flow> { + return combine( + flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), + flow2 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), + transform = ::Pair, + ) + } + private fun hasRestrictionForSellFlow(): Flow { return if (params.filterOperation == OnrampOperation.SELL) { getUserCountryUseCase().map { maybe -> @@ -154,25 +218,52 @@ internal class OnrampTokenListModel @Inject constructor( } } + private fun AccountStatusList.isInsufficientBalanceForSell(): Boolean { + return if (params.filterOperation == OnrampOperation.SELL) { + (totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true + } else { + false + } + } + private fun getUnavailableTokensHeaderReference() = when (params.filterOperation) { OnrampOperation.BUY -> R.string.tokens_list_unavailable_to_purchase_header OnrampOperation.SELL -> R.string.tokens_list_unavailable_to_sell_header OnrampOperation.SWAP -> R.string.tokens_list_unavailable_to_swap_source_header }.let(::resourceReference) + private fun getEmptySearchMessageReference() = when (params.filterOperation) { + OnrampOperation.BUY -> R.string.action_buttons_buy_empty_search_message + OnrampOperation.SELL -> R.string.action_buttons_sell_empty_search_message + OnrampOperation.SWAP -> R.string.action_buttons_swap_empty_search_message + }.let(::resourceReference) + private fun updateTokenListUM(transformer: TokenListUMTransformer) { - tokenListUMController.update { prevState -> - transformer.transform(prevState).apply { - if (isFirstInitialization(prevState = prevState, newState = this)) { - params.onTokenListInitialized() + modelScope.launch { + tokenListUMController.update { prevState -> + transformer.transform(prevState).apply { + if (isFirstInitialization(prevState = prevState, newState = this)) { + params.onTokenListInitialized() + } } } } } + private fun getSellWarning(hasRestrictionForSell: Boolean, isInsufficientBalanceForSell: Boolean) = when { + hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction + isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling + else -> null + } + private fun isFirstInitialization(prevState: TokenListUM, newState: TokenListUM): Boolean { - return prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() && - (newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty()) + return if (accountsFeatureToggles.isFeatureEnabled) { + prevState.tokensListData == TokenListUMData.EmptyList && + newState.tokensListData != TokenListUMData.EmptyList + } else { + prevState.availableItems.isEmpty() && prevState.unavailableItems.isEmpty() && + (newState.availableItems.isNotEmpty() || newState.unavailableItems.isNotEmpty()) + } } private fun onSearchQueryChange(newQuery: String) { @@ -195,6 +286,16 @@ internal class OnrampTokenListModel @Inject constructor( ) } + private fun AccountStatusList.filterAccountsByQuery(query: String) = accountStatuses.asSequence() + .associate { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> { + val filteredList = accountStatus.tokenList.flattenCurrencies().filterByQuery(query = query) + accountStatus.account to filteredList + } + } + }.filter { (_, value) -> value.isNotEmpty() } + private fun List.filterByQuery(query: String): List { return filter { it.currency.name.contains(other = query, ignoreCase = true) || @@ -237,6 +338,49 @@ internal class OnrampTokenListModel @Inject constructor( } } + private suspend fun AccountCryptoList.filterByAvailability(): List { + return coroutineScope { + map { (account, currencies) -> + async { + AccountAvailabilityUM( + account = account, + currencyList = currencies.map { status -> + val isOperationAvailable = checkAvailabilityByOperation(status = status) + val isNotMissedDerivation = status.value !is CryptoCurrencyStatus.MissedDerivation + val isNotLoading = status.value !is CryptoCurrencyStatus.Loading + + val requirements = getAssetRequirementsUseCase( + userWalletId = userWallet.walletId, + currency = status.currency, + ).getOrNull() + + val isAvailableForBuy = rampStateManager.checkAssetRequirements(requirements) + val isNotUnreachable = status.value !is CryptoCurrencyStatus.Unreachable + + val isAvailable = when (params.filterOperation) { + OnrampOperation.BUY -> { + isAvailableForBuy + } // unreachable state is available for Buy operation + OnrampOperation.SELL -> isNotUnreachable + OnrampOperation.SWAP -> { + isNotUnreachable && isAvailableForBuy + } + } + + val isTotalAvailable = + isOperationAvailable && isNotMissedDerivation && isNotLoading && isAvailable + + AccountCurrencyUM( + cryptoCurrencyStatus = status, + isAvailable = isTotalAvailable, + ) + }, + ) + } + }.awaitAll() + } + } + private suspend fun checkAvailabilityByOperation(status: CryptoCurrencyStatus): Boolean { return when (params.filterOperation) { OnrampOperation.BUY -> { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt index 8aeb293ca5..aefd164597 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/OnrampTokenList.kt @@ -30,6 +30,7 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.BuyTokenScreenTestTags import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import com.tangem.features.onramp.tokenlist.ui.preview.PreviewTokenListUMProvider import kotlinx.collections.immutable.ImmutableList @@ -70,6 +71,22 @@ internal fun LazyListScope.onrampTokenList(state: TokenListUM) { tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) tokensList(items = state.unavailableItems, isBalanceHidden = state.isBalanceHidden) + + when (val list = state.tokensListData) { + is TokenListUMData.AccountList -> list.tokensList.forEach { item -> + portfolioTokensList( + portfolio = item, + isBalanceHidden = state.isBalanceHidden, + ) + } + is TokenListUMData.TokenList -> { + tokensList( + items = list.tokensList, + isBalanceHidden = state.isBalanceHidden, + ) + } + TokenListUMData.EmptyList -> Unit + } } private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modifier = Modifier) { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt index 684e6d0cc3..2f7e48ad75 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/ui/preview/PreviewTokenListUMProvider.kt @@ -10,6 +10,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.tokenlist.entity.TokenListUM +import com.tangem.features.onramp.tokenlist.entity.TokenListUMData import kotlinx.collections.immutable.persistentListOf internal class PreviewTokenListUMProvider : PreviewParameterProvider { @@ -42,6 +43,7 @@ internal class PreviewTokenListUMProvider : PreviewParameterProvider checkSwapCryptoAvailability( + tokenCount = tokenListState.items.count { it is TokensListItemUM.Token }, ) - - return + is WalletTokensListState.ContentState.PortfolioContent -> checkSwapCryptoAvailability( + tokenCount = tokenListState.items.sumOf { it.tokens.count { it is TokensListItemUM.Token } }, + ) + WalletTokensListState.ContentState.Loading, + WalletTokensListState.ContentState.Locked, + WalletTokensListState.Empty, + -> return } modelScope.launch { @@ -808,4 +808,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) } } + + private fun checkSwapCryptoAvailability(tokenCount: Int) { + if (tokenCount < 2) { + handleError( + alertState = WalletAlertState.InsufficientTokensCountForSwapping, + eventCreator = MainScreenAnalyticsEvent::ButtonSwap, + ) + + return + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index cd5f108efd..37319208bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -88,12 +88,14 @@ internal object WalletScreenPreviewData { TokensListItemUM.Portfolio( tokens = textContentTokensState.items.filterIsInstance(), isExpanded = false, + isCollapsable = true, state = AccountItemPreviewData.accountItem .copy(iconState = AccountItemPreviewData.accountLetterIcon), ), TokensListItemUM.Portfolio( tokens = textContentTokensState.items.filterIsInstance(), isExpanded = true, + isCollapsable = true, state = AccountItemPreviewData.accountItem, ), ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 931921c425..07afba9a94 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -114,6 +114,7 @@ internal class TokenListStateConverter( return TokensListItemUM.Portfolio( state = accountItem, isExpanded = isExtend, + isCollapsable = true, tokens = items.filterIsInstance(), ) } From 373880691ea23060d08992a2517641f929405ce9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 17:12:39 +0400 Subject: [PATCH 13/22] Updated on 2026-08-14 --- .../account/SaveWalletAccountsResponse.kt | 33 +++++++++++- .../com/tangem/datasource/di/MoshiModule.kt | 2 + .../tangem/datasource/utils/SerializeNulls.kt | 5 ++ .../datasource/utils/SerializeNullsFactory.kt | 25 +++++++++ .../utils/SerializeNullsFactoryTest.kt | 51 +++++++++++++++++++ .../SaveWalletAccountsResponseConverter.kt | 5 +- .../fetcher/DefaultWalletAccountsFetcher.kt | 6 +-- ...SaveWalletAccountsResponseConverterTest.kt | 3 +- .../tester/presentation/TesterActivity.kt | 4 +- .../accounts/entity/AccountsUM.kt | 3 +- .../accounts/ui/AccountsScreen.kt | 17 ++----- ...iewModel.kt => TesterAccountsViewModel.kt} | 36 ++----------- 12 files changed, 129 insertions(+), 61 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNulls.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNullsFactory.kt create mode 100644 core/datasource/src/test/kotlin/com/tangem/datasource/utils/SerializeNullsFactoryTest.kt rename features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/{AccountsViewModel.kt => TesterAccountsViewModel.kt} (79%) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt index 3f36276519..4157c42c41 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/account/SaveWalletAccountsResponse.kt @@ -2,8 +2,37 @@ package com.tangem.datasource.api.tangemTech.models.account import com.squareup.moshi.Json import com.squareup.moshi.JsonClass +import com.tangem.datasource.utils.SerializeNulls @JsonClass(generateAdapter = true) data class SaveWalletAccountsResponse( - @Json(name = "accounts") val accounts: List, -) \ No newline at end of file + @Json(name = "accounts") val accounts: List, +) { + + @SerializeNulls + @JsonClass(generateAdapter = true) + data class AccountDTO( + @Json(name = "id") val id: String, + @Json(name = "name") val name: String?, + @Json(name = "derivation") val derivationIndex: Int, + @Json(name = "icon") val icon: String, + @Json(name = "iconColor") val iconColor: String, + ) + + companion object { + + operator fun invoke(accounts: List): SaveWalletAccountsResponse { + return SaveWalletAccountsResponse( + accounts = accounts.map { accountDto -> + AccountDTO( + id = accountDto.id, + name = accountDto.name, + derivationIndex = accountDto.derivationIndex, + icon = accountDto.icon, + iconColor = accountDto.iconColor, + ) + }, + ) + } + } +} \ 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 37ea5507c5..1da93e6b44 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 @@ -9,6 +9,7 @@ import com.tangem.common.json.MoshiJsonConverter 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.datasource.utils.SerializeNullsFactory import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus @@ -28,6 +29,7 @@ class MoshiModule { @NetworkMoshi fun provideNetworkMoshi(): Moshi { return Moshi.Builder() + .add(SerializeNullsFactory) .add( PolymorphicJsonAdapterFactory.of(ProviderModel::class.java, "type") .withSubtype(ProviderModel.Public::class.java, "public") diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNulls.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNulls.kt new file mode 100644 index 0000000000..8361150150 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNulls.kt @@ -0,0 +1,5 @@ +package com.tangem.datasource.utils + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +annotation class SerializeNulls \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNullsFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNullsFactory.kt new file mode 100644 index 0000000000..ac77f01f7c --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/SerializeNullsFactory.kt @@ -0,0 +1,25 @@ +package com.tangem.datasource.utils + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import java.lang.reflect.Type + +/** + * Factory to serialize nulls in Moshi if the class is annotated with [SerializeNulls]. + * +[REDACTED_AUTHOR] + */ +internal object SerializeNullsFactory : JsonAdapter.Factory { + + override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? { + val rawType = Types.getRawType(type) + if (!rawType.isAnnotationPresent(SerializeNulls::class.java)) { + return null + } + + val nextAdapter: JsonAdapter = moshi.nextAdapter(this, type, annotations) + + return nextAdapter.serializeNulls() + } +} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/utils/SerializeNullsFactoryTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/utils/SerializeNullsFactoryTest.kt new file mode 100644 index 0000000000..9dafd03360 --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/utils/SerializeNullsFactoryTest.kt @@ -0,0 +1,51 @@ +package com.tangem.datasource.utils + +import com.google.common.truth.Truth.assertThat +import com.squareup.moshi.JsonClass +import com.squareup.moshi.Moshi +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +// --- DTO --- +@SerializeNulls +@JsonClass(generateAdapter = true) +data class UserWithNulls(val id: String?, val name: String?) + +@JsonClass(generateAdapter = true) +data class UserWithoutNulls(val id: String?, val name: String?) + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class SerializeNullsFactoryTest { + + private val moshi = Moshi.Builder() + .add(SerializeNullsFactory) + .build() + + @Test + fun `should serialize nulls for annotated class`() { + val adapter = moshi.adapter(UserWithNulls::class.java) + + val json = adapter.toJson(UserWithNulls(id = null, name = "John")) + + assertThat(json).isEqualTo("""{"id":null,"name":"John"}""") + } + + @Test + fun `should skip nulls for non-annotated class`() { + val adapter = moshi.adapter(UserWithoutNulls::class.java) + + val json = adapter.toJson(UserWithoutNulls(id = null, name = "John")) + + assertThat(json).isEqualTo("""{"name":"John"}""") + } + + @Test + fun `should deserialize annotated class correctly`() { + val adapter = moshi.adapter(UserWithNulls::class.java) + + val json = """{"id":null,"name":"Jane"}""" + val result = adapter.fromJson(json) + + assertThat(result).isEqualTo(UserWithNulls(id = null, name = "Jane")) + } +} \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt index 07c76c7c24..8f26a20de5 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/converter/SaveWalletAccountsResponseConverter.kt @@ -1,7 +1,6 @@ package com.tangem.data.account.converter import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse -import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.domain.account.models.AccountList import com.tangem.domain.models.account.Account import com.tangem.utils.converter.Converter @@ -21,8 +20,8 @@ internal object SaveWalletAccountsResponseConverter : Converter().apply { + val viewModel = hiltViewModel().apply { setupNavigation(innerTesterRouter) } val state by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt index 168afd1186..64cf095f9c 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/entity/AccountsUM.kt @@ -9,9 +9,8 @@ internal data class AccountsUM( val onBackClick: () -> Unit, val walletSelector: WalletSelector, val accountListBottomSheetConfig: AccountListBottomSheetConfig, - val onAccountsClick: () -> Unit, + val onAccountsClick: () -> Boolean, val onFetchAccountsClick: () -> Unit, - val onCreateMainAccountClick: () -> Unit, val onClearETagClick: () -> Unit, ) { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt index 66202d3842..8ee0fd82be 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/ui/AccountsScreen.kt @@ -62,8 +62,9 @@ internal fun AccountsScreen(state: AccountsUM, modifier: Modifier = Modifier) { ManageAccountsButtons( state = state, onAccountsClick = { context -> - if (state.accountListBottomSheetConfig.accounts.isNotEmpty()) { - state.onAccountsClick() + val isEmpty = state.onAccountsClick() + + if (!isEmpty) { isAccountListShown = true } else { Toast.makeText(context, "No accounts found", Toast.LENGTH_SHORT).show() @@ -242,16 +243,4 @@ private fun LazyListScope.ManageAccountsButtons(state: AccountsUM, onAccountsCli .fillMaxWidth(), ) } - - if (state.accountListBottomSheetConfig.accounts.none { it.isMainAccount }) { - item { - PrimaryButton( - text = "Create Main account", - onClick = state.onCreateMainAccountClick, - modifier = Modifier - .padding(horizontal = 16.dp, vertical = 8.dp) - .fillMaxWidth(), - ) - } - } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt similarity index 79% rename from features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt rename to features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt index de73765db2..0824ae8aaa 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/AccountsViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/accounts/viewmodel/TesterAccountsViewModel.kt @@ -6,13 +6,9 @@ import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.producer.SingleAccountListProducer -import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountName import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.tester.presentation.accounts.entity.AccountsUM @@ -28,11 +24,10 @@ import javax.inject.Inject @OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel -internal class AccountsViewModel @Inject constructor( +internal class TesterAccountsViewModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListFetcher: SingleAccountListFetcher, private val singleAccountListSupplier: SingleAccountListSupplier, - private val accountsCRUDRepository: AccountsCRUDRepository, private val eTagsStore: ETagsStore, private val dispatchers: CoroutineDispatcherProvider, ) : ViewModel() { @@ -88,7 +83,6 @@ internal class AccountsViewModel @Inject constructor( ), onAccountsClick = ::updateAccountsList, onFetchAccountsClick = ::fetchAccounts, - onCreateMainAccountClick = ::createMainAccount, onClearETagClick = ::clearETag, ) } @@ -107,8 +101,8 @@ internal class AccountsViewModel @Inject constructor( } } - private fun updateAccountsList() { - val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return + private fun updateAccountsList(): Boolean { + val userWalletId = uiState.value.walletSelector.selected?.walletId ?: return false val accounts = walletAccounts.value[userWalletId]?.accounts ?.filterIsInstance() @@ -122,6 +116,8 @@ internal class AccountsViewModel @Inject constructor( ), ) } + + return accounts.isEmpty() } private fun fetchAccounts() { @@ -134,28 +130,6 @@ internal class AccountsViewModel @Inject constructor( } } - private fun createMainAccount() { - viewModelScope.launch { - val userWallet = uiState.value.walletSelector.selected ?: return@launch - - // It's temporary solution to create main account for testing purposes - val accountList = AccountList( - userWalletId = userWallet.walletId, - accounts = setOf( - Account.CryptoPortfolio.createMainAccount(userWallet.walletId).copy( - accountName = AccountName.invoke(value = "Main Account").getOrNull()!!, - ), - ), - totalAccounts = 1, - sortType = TokensSortType.NONE, - groupType = TokensGroupType.NONE, - ) - .getOrNull()!! - - accountsCRUDRepository.saveAccounts(accountList) - } - } - private fun clearETag() { viewModelScope.launch { val userWallet = uiState.value.walletSelector.selected ?: return@launch From cafd4d074bbe460f3ab3b48c4e398928f040f77f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 22:37:04 +0400 Subject: [PATCH 14/22] Updated on 2026-08-14 --- data/account/build.gradle.kts | 1 + .../fetcher/DefaultWalletAccountsFetcher.kt | 26 +- .../FetchWalletAccountsErrorHandler.kt | 82 ++---- .../DefaultWalletAccountsResponseFactory.kt | 68 +++++ .../DefaultWalletAccountsFetcherTest.kt | 13 + .../FetchWalletAccountsErrorHandlerTest.kt | 74 ++---- ...efaultWalletAccountsResponseFactoryTest.kt | 240 ++++++++++++++++++ 7 files changed, 378 insertions(+), 126 deletions(-) create mode 100644 data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt create mode 100644 data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt diff --git a/data/account/build.gradle.kts b/data/account/build.gradle.kts index d5c297866a..58b5628a0d 100644 --- a/data/account/build.gradle.kts +++ b/data/account/build.gradle.kts @@ -24,6 +24,7 @@ dependencies { // region Project - Domain api(projects.domain.account) api(projects.domain.card) + api(projects.domain.common) api(projects.domain.models) // endregion diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt index a4fe4cd17d..5740e30ccb 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcher.kt @@ -2,6 +2,7 @@ package com.tangem.data.account.fetcher import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory +import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.account.utils.assignTokens import com.tangem.data.account.utils.toUserTokensResponse import com.tangem.data.common.account.WalletAccountsFetcher @@ -14,6 +15,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError.HttpException. import com.tangem.datasource.api.common.response.ETAG_HEADER import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.SaveWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO @@ -31,17 +33,20 @@ import javax.inject.Singleton * @property accountsResponseStoreFactory factory to create [AccountsResponseStore] * @property userTokensSaver saves user tokens to the database * @property fetchWalletAccountsErrorHandler handles errors during fetching wallet accounts + * @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse] * @property eTagsStore store for ETags to manage caching * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") @Singleton internal class DefaultWalletAccountsFetcher @Inject constructor( private val tangemTechApi: TangemTechApi, private val accountsResponseStoreFactory: AccountsResponseStoreFactory, private val userTokensSaver: UserTokensSaver, private val fetchWalletAccountsErrorHandler: FetchWalletAccountsErrorHandler, + private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory, private val eTagsStore: ETagsStore, private val dispatchers: CoroutineDispatcherProvider, ) : WalletAccountsFetcher, WalletAccountsSaver { @@ -49,9 +54,11 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( override suspend fun fetch(userWalletId: UserWalletId) { val savedAccountsResponse = getAccountsResponseStore(userWalletId = userWalletId).getSyncOrNull() val accountsResponse = fetchWalletAccounts(userWalletId, savedAccountsResponse) - val unassignedTokens = accountsResponse?.unassignedTokens + ?: return - if (!unassignedTokens.isNullOrEmpty()) { + if (accountsResponse.accounts.isEmpty()) { + initializeAccounts(userWalletId, accountsResponse) + } else if (accountsResponse.unassignedTokens.isNotEmpty()) { assignTokens(userWalletId, accountsResponse) } } @@ -131,12 +138,23 @@ internal class DefaultWalletAccountsFetcher @Inject constructor( pushWalletAccounts = ::push, storeWalletAccounts = ::store, ) - - null }, ) } + private suspend fun initializeAccounts(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) { + val response = defaultWalletAccountsResponseFactory.create( + userWalletId = userWalletId, + userTokensResponse = UserTokensResponse( + group = accountsResponse.wallet.group, + sort = accountsResponse.wallet.sort, + tokens = accountsResponse.unassignedTokens, + ), + ) + + pushAndStore(userWalletId, response) + } + private suspend fun assignTokens(userWalletId: UserWalletId, accountsResponse: GetWalletAccountsResponse) { val accountsResponseWithTokens = accountsResponse.assignTokens(userWalletId) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt index 3c7d1af215..6f284edf5b 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandler.kt @@ -1,10 +1,8 @@ package com.tangem.data.account.fetcher -import com.tangem.data.account.converter.CryptoPortfolioConverter -import com.tangem.data.account.utils.assignTokens +import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.account.utils.toUserTokensResponse -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.data.common.currency.UserTokensResponseAccountIdEnricher import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code @@ -13,10 +11,6 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.account.models.AccountList -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import timber.log.Timber import javax.inject.Inject @@ -24,12 +18,9 @@ import javax.inject.Inject /** * Handles errors that occur during the fetching of wallet accounts * - * @property userTokensSaver saves user tokens to the storage - * @property userWalletsStore provides access to user wallet data - * @property userTokensResponseStore provides access to user token responses. - * @property cryptoPortfolioCF factory for converting crypto portfolios - * @property userTokensResponseFactory factory for creating user token responses - * @property cardCryptoCurrencyFactory factory for creating default cryptocurrencies for multi-currency wallets + * @property userTokensSaver saves user tokens to the storage + * @property userTokensResponseStore provides access to user token responses. + * @property defaultWalletAccountsResponseFactory creates [GetWalletAccountsResponse] from [UserTokensResponse] * * @see DefaultWalletAccountsFetcher * @@ -37,11 +28,8 @@ import javax.inject.Inject */ internal class FetchWalletAccountsErrorHandler @Inject constructor( private val userTokensSaver: UserTokensSaver, - private val userWalletsStore: UserWalletsStore, private val userTokensResponseStore: UserTokensResponseStore, - private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, - private val userTokensResponseFactory: UserTokensResponseFactory, - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, + private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory, ) { /** @@ -61,20 +49,19 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( savedAccountsResponse: GetWalletAccountsResponse?, pushWalletAccounts: suspend (userWalletId: UserWalletId, accounts: List) -> Unit, storeWalletAccounts: suspend (userWalletId: UserWalletId, response: GetWalletAccountsResponse) -> Unit, - ) { + ): GetWalletAccountsResponse? { val isResponseUpToDate = error.isNetworkError(code = Code.NOT_MODIFIED) if (isResponseUpToDate) { Timber.e("ETag is up to date, no need to update accounts for wallet: $userWalletId") - return + return savedAccountsResponse } - val (accountDTOs, userTokensResponse) = if (savedAccountsResponse == null) { - val userWallet = userWalletsStore.getSyncStrict(key = userWalletId) + val response = savedAccountsResponse ?: defaultWalletAccountsResponseFactory.create( + userWalletId = userWalletId, + userTokensResponse = getFromLegacyStore(userWalletId), + ) - createDefaultAccountDTOs(userWallet) to getFromLegacyStore(userWalletId).orDefault(userWallet) - } else { - savedAccountsResponse.accounts to savedAccountsResponse.toUserTokensResponse() - } + val (accountDTOs, userTokensResponse) = response.accounts to response.toUserTokensResponse() val isNotFoundError = error.isNetworkError(code = Code.NOT_FOUND) if (isNotFoundError) { @@ -82,49 +69,18 @@ internal class FetchWalletAccountsErrorHandler @Inject constructor( userTokensSaver.push(userWalletId = userWalletId, response = userTokensResponse) } - val response = savedAccountsResponse.orDefault(userWalletId, accountDTOs, userTokensResponse) storeWalletAccounts(userWalletId, response) - } - private fun createDefaultAccountDTOs(userWallet: UserWallet): List { - val accounts = AccountList.empty(userWallet.walletId).accounts - .filterIsInstance() - - val converter = cryptoPortfolioCF.create(userWallet = userWallet) - - return converter.convertListBack(input = accounts) + return response } private suspend fun getFromLegacyStore(userWalletId: UserWalletId): UserTokensResponse? { return userTokensResponseStore.getSyncOrNull(userWalletId) + ?.let { + it.copy( + tokens = UserTokensResponseAccountIdEnricher(userWalletId = userWalletId, tokens = it.tokens), + ) + } .also { userTokensResponseStore.clear(userWalletId) } } - - private fun UserTokensResponse?.orDefault(userWallet: UserWallet): UserTokensResponse { - if (this != null) return this - - return userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet = userWallet), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - } - - private fun GetWalletAccountsResponse?.orDefault( - userWalletId: UserWalletId, - accountDTOs: List, - userTokensResponse: UserTokensResponse, - ): GetWalletAccountsResponse { - if (this != null) return this - - return GetWalletAccountsResponse( - wallet = GetWalletAccountsResponse.Wallet( - group = userTokensResponse.group, - sort = userTokensResponse.sort, - totalAccounts = accountDTOs.size, - ), - accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = userTokensResponse.tokens), - unassignedTokens = emptyList(), - ) - } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt new file mode 100644 index 0000000000..8dad68c1de --- /dev/null +++ b/data/account/src/main/kotlin/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactory.kt @@ -0,0 +1,68 @@ +package com.tangem.data.account.utils + +import com.tangem.data.account.converter.CryptoPortfolioConverter +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import javax.inject.Inject + +/** + * Factory to create default [GetWalletAccountsResponse]. + * + * @property userWalletsListRepository repository to get user wallet information + * @property cryptoPortfolioCF converter factory to convert crypto portfolio accounts + * @property userTokensResponseFactory factory to create [UserTokensResponse] + * @property cardCryptoCurrencyFactory factory to get default coins for multi-currency wallet + * +[REDACTED_AUTHOR] + */ +internal class DefaultWalletAccountsResponseFactory @Inject constructor( + private val userWalletsListRepository: UserWalletsListRepository, + private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory, + private val userTokensResponseFactory: UserTokensResponseFactory, + private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, +) { + + suspend fun create(userWalletId: UserWalletId, userTokensResponse: UserTokensResponse?): GetWalletAccountsResponse { + val userWallet = userWalletsListRepository.userWalletsSync().firstOrNull { it.walletId == userWalletId } + + val accountDTOs = userWallet?.let(::createDefaultAccountDTOs).orEmpty() + val response = userTokensResponse.orDefault(userWallet = userWallet) + + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = response.group, + sort = response.sort, + totalAccounts = accountDTOs.size, + ), + accounts = accountDTOs.assignTokens(userWalletId = userWalletId, tokens = response.tokens), + unassignedTokens = emptyList(), + ) + } + + private fun createDefaultAccountDTOs(userWallet: UserWallet): List { + val accounts = AccountList.empty(userWallet.walletId).accounts + .filterIsInstance() + + val converter = cryptoPortfolioCF.create(userWallet = userWallet) + + return converter.convertListBack(input = accounts) + } + + private fun UserTokensResponse?.orDefault(userWallet: UserWallet?): UserTokensResponse { + if (this != null) return this + + return userTokensResponseFactory.createUserTokensResponse( + currencies = userWallet?.let(cardCryptoCurrencyFactory::createDefaultCoinsForMultiCurrencyWallet).orEmpty(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } +} \ No newline at end of file diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt index ac012e76fb..4494f5af61 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/DefaultWalletAccountsFetcherTest.kt @@ -5,6 +5,7 @@ import com.tangem.data.account.converter.createGetWalletAccountsResponse import com.tangem.data.account.converter.createWalletAccountDTO import com.tangem.data.account.store.AccountsResponseStore import com.tangem.data.account.store.AccountsResponseStoreFactory +import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.common.cache.etag.ETagsStore import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponse @@ -36,6 +37,7 @@ class DefaultWalletAccountsFetcherTest { private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) private val fetchWalletAccountsErrorHandler: FetchWalletAccountsErrorHandler = mockk(relaxUnitFun = true) + private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk() private val eTagsStore: ETagsStore = mockk(relaxUnitFun = true) private val fetcher: DefaultWalletAccountsFetcher = DefaultWalletAccountsFetcher( @@ -43,6 +45,7 @@ class DefaultWalletAccountsFetcherTest { accountsResponseStoreFactory = accountsResponseStoreFactory, userTokensSaver = userTokensSaver, fetchWalletAccountsErrorHandler = fetchWalletAccountsErrorHandler, + defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, eTagsStore = eTagsStore, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -205,6 +208,16 @@ class DefaultWalletAccountsFetcherTest { tangemTechApi.getWalletAccounts(walletId = userWalletId.stringValue, eTag = eTag) } returns apiError as ApiResponse + coEvery { + fetchWalletAccountsErrorHandler.handle( + error = apiError.cause, + userWalletId = userWalletId, + savedAccountsResponse = null, + pushWalletAccounts = any(), + storeWalletAccounts = any(), + ) + } returns savedAccountsResponse + // Act fetcher.fetch(userWalletId) diff --git a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt index c174b4d463..e8a3341536 100644 --- a/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt +++ b/data/account/src/test/java/com/tangem/data/account/fetcher/FetchWalletAccountsErrorHandlerTest.kt @@ -1,9 +1,7 @@ package com.tangem.data.account.fetcher -import com.tangem.data.account.converter.CryptoPortfolioConverter +import com.tangem.data.account.utils.DefaultWalletAccountsResponseFactory import com.tangem.data.account.utils.toUserTokensResponse -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory import com.tangem.data.common.currency.UserTokensSaver import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.ApiResponseError.HttpException.Code @@ -11,12 +9,11 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.domain.account.models.AccountList -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import io.mockk.* +import io.mockk.clearMocks +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -29,35 +26,21 @@ import org.junit.jupiter.api.TestInstance class FetchWalletAccountsErrorHandlerTest { private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) - private val userWalletsStore: UserWalletsStore = mockk() private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) - private val cryptoPortfolioCF: CryptoPortfolioConverter.Factory = mockk() - private val cryptoPortfolioConverter = mockk() - private val userTokensResponseFactory: UserTokensResponseFactory = mockk() - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() + private val defaultWalletAccountsResponseFactory: DefaultWalletAccountsResponseFactory = mockk() private val handler = FetchWalletAccountsErrorHandler( userTokensSaver = userTokensSaver, - userWalletsStore = userWalletsStore, userTokensResponseStore = userTokensResponseStore, - cryptoPortfolioCF = cryptoPortfolioCF, - userTokensResponseFactory = userTokensResponseFactory, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + defaultWalletAccountsResponseFactory = defaultWalletAccountsResponseFactory, ) - private val userWallet = mockk { - every { this@mockk.walletId } returns userWalletId - } - @BeforeEach fun setupEach() { clearMocks( userTokensSaver, - userWalletsStore, userTokensResponseStore, - cryptoPortfolioCF, - cryptoPortfolioConverter, - cardCryptoCurrencyFactory, + defaultWalletAccountsResponseFactory, ) } @@ -84,12 +67,8 @@ class FetchWalletAccountsErrorHandlerTest { // Assert coVerify(inverse = true) { - userWalletsStore.getSyncStrict(key = any()) userTokensResponseStore.getSyncOrNull(userWalletId = any()) - userTokensResponseFactory.createUserTokensResponse(any(), any(), any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - cryptoPortfolioCF.create(any()) - cryptoPortfolioConverter.convertListBack(any()) + defaultWalletAccountsResponseFactory.create(userWalletId = any(), userTokensResponse = any()) pushWalletAccounts(any(), any()) userTokensSaver.push(userWalletId = any(), response = any()) storeWalletAccounts(any(), any()) @@ -146,12 +125,8 @@ class FetchWalletAccountsErrorHandlerTest { } coVerify(inverse = true) { - userWalletsStore.getSyncStrict(key = any()) userTokensResponseStore.getSyncOrNull(userWalletId = any()) - userTokensResponseFactory.createUserTokensResponse(any(), any(), any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - cryptoPortfolioCF.create(any()) - cryptoPortfolioConverter.convertListBack(any()) + defaultWalletAccountsResponseFactory.create(userWalletId = any(), userTokensResponse = any()) } } @@ -160,9 +135,6 @@ class FetchWalletAccountsErrorHandlerTest { // Arrange val error = ApiResponseError.TimeoutException() - val accounts = AccountList.empty(userWalletId).accounts - .filterIsInstance() - val accountDTO = WalletAccountDTO( id = "nibh", name = "Michael Dotson", @@ -186,18 +158,10 @@ class FetchWalletAccountsErrorHandlerTest { val userTokensResponse = savedAccountsResponse.toUserTokensResponse() - every { userWalletsStore.getSyncStrict(userWalletId) } returns userWallet - every { cryptoPortfolioCF.create(userWallet) } returns cryptoPortfolioConverter - every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountDTO) - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns null - every { - userTokensResponseFactory.createUserTokensResponse( - currencies = emptyList(), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - } returns userTokensResponse - every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList() + coEvery { userTokensResponseStore.getSyncOrNull(userWalletId) } returns userTokensResponse + coEvery { + defaultWalletAccountsResponseFactory.create(userWalletId, userTokensResponse) + } returns savedAccountsResponse val pushWalletAccounts: suspend (UserWalletId, List) -> Unit = mockk(relaxed = true) val storeWalletAccounts: suspend (UserWalletId, GetWalletAccountsResponse) -> Unit = mockk(relaxed = true) @@ -213,16 +177,8 @@ class FetchWalletAccountsErrorHandlerTest { // Assert coVerify { - userWalletsStore.getSyncStrict(userWalletId) - cryptoPortfolioCF.create(userWallet) - cryptoPortfolioConverter.convertListBack(accounts) userTokensResponseStore.getSyncOrNull(userWalletId) - userTokensResponseFactory.createUserTokensResponse( - currencies = emptyList(), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) + defaultWalletAccountsResponseFactory.create(userWalletId, userTokensResponse) storeWalletAccounts(userWalletId, any()) } diff --git a/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt new file mode 100644 index 0000000000..e270dacc98 --- /dev/null +++ b/data/account/src/test/java/com/tangem/data/account/utils/DefaultWalletAccountsResponseFactoryTest.kt @@ -0,0 +1,240 @@ +package com.tangem.data.account.utils + +import com.google.common.truth.Truth +import com.tangem.data.account.converter.CryptoPortfolioConverter +import com.tangem.data.account.converter.createWalletAccountDTO +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.UserTokensResponseFactory +import com.tangem.datasource.api.tangemTech.models.UserTokensResponse +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DefaultWalletAccountsResponseFactoryTest { + + private val userWalletsListRepository = mockk() + private val cryptoPortfolioCF = mockk() + private val cryptoPortfolioConverter = mockk() + private val userTokensResponseFactory = mockk() + private val cardCryptoCurrencyFactory = mockk() + + private val factory = DefaultWalletAccountsResponseFactory( + userWalletsListRepository = userWalletsListRepository, + cryptoPortfolioCF = cryptoPortfolioCF, + userTokensResponseFactory = userTokensResponseFactory, + cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, + ) + + private val userWalletId = UserWalletId("011") + + @BeforeEach + fun setUpEach() { + every { cryptoPortfolioCF.create(any()) } returns cryptoPortfolioConverter + } + + @AfterEach + fun tearDownEach() { + clearMocks( + userWalletsListRepository, + cryptoPortfolioCF, + cryptoPortfolioConverter, + userTokensResponseFactory, + cardCryptoCurrencyFactory, + ) + } + + @Test + fun `create returns empty accounts when user wallet not found`() = runTest { + // Arrange + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = emptyList(), + ) + + coEvery { userWalletsListRepository.userWalletsSync() } returns emptyList() + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = emptyList(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns userTokensResponse + + // Act + val actual = factory.create(userWalletId = userWalletId, userTokensResponse = null) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + totalAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + userWalletsListRepository.userWalletsSync() + userTokensResponseFactory.createUserTokensResponse( + currencies = emptyList(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } + } + + @Test + fun `create returns response with default tokens when userTokensResponse is null`() = runTest { + // Arrange + val userWallet = mockk(relaxed = true) { + every { walletId } returns userWalletId + } + + val defaultCoins = listOf(mockk()) + coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet) + every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns defaultCoins + + val defaultResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = listOf(mockk(relaxed = true)), + ) + + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = defaultCoins, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns defaultResponse + + val accounts = AccountList.empty(userWallet.walletId).accounts + .filterIsInstance() + + val accountsDTO = createWalletAccountDTO(userWalletId) + every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO) + + // Act + val actual = factory.create(userWalletId, null) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = defaultResponse.group, + sort = defaultResponse.sort, + totalAccounts = 1, + ), + accounts = listOf(accountsDTO), + unassignedTokens = emptyList(), + ) + + Truth.assertThat(actual).isEqualTo(expected) + + coVerifyOrder { + userWalletsListRepository.userWalletsSync() + cryptoPortfolioConverter.convertListBack(accounts) + cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) + userTokensResponseFactory.createUserTokensResponse( + currencies = defaultCoins, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } + } + + @Test + fun `create returns response with default tokens when userTokensResponse is null and no default coins`() = runTest { + // Arrange + val userWallet = mockk(relaxed = true) { + every { walletId } returns userWalletId + } + coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet) + every { cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet) } returns emptyList() + val defaultResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = emptyList(), + ) + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = emptyList(), + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns defaultResponse + val accounts = AccountList.empty(userWallet.walletId).accounts + .filterIsInstance() + every { cryptoPortfolioConverter.convertListBack(accounts) } returns emptyList() + + // Act + val actual = factory.create(userWalletId, null) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = defaultResponse.group, + sort = defaultResponse.sort, + totalAccounts = 0, + ), + accounts = emptyList(), + unassignedTokens = emptyList(), + ) + Truth.assertThat(actual).isEqualTo(expected) + } + + @Test + fun `create returns response with assigned tokens`() = runTest { + // Arrange + val userWallet = mockk(relaxed = true) { + every { walletId } returns userWalletId + } + val assignedTokens = listOf(mockk(), mockk()) + coEvery { userWalletsListRepository.userWalletsSync() } returns listOf(userWallet) + val userTokensResponse = UserTokensResponse( + group = UserTokensResponse.GroupType.NETWORK, + sort = UserTokensResponse.SortType.BALANCE, + tokens = listOf(mockk(relaxed = true)), + ) + every { + userTokensResponseFactory.createUserTokensResponse( + currencies = assignedTokens, + isGroupedByNetwork = false, + isSortedByBalance = false, + ) + } returns userTokensResponse + + val accounts = AccountList.empty(userWallet.walletId).accounts + .filterIsInstance() + val accountsDTO = createWalletAccountDTO(userWalletId) + every { cryptoPortfolioConverter.convertListBack(accounts) } returns listOf(accountsDTO) + + // Act + val actual = factory.create(userWalletId, userTokensResponse) + + // Assert + val expected = GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + group = userTokensResponse.group, + sort = userTokensResponse.sort, + totalAccounts = 1, + ), + accounts = listOf(accountsDTO), + unassignedTokens = emptyList(), + ) + Truth.assertThat(actual).isEqualTo(expected) + } +} \ No newline at end of file From f1f9dff5ce92507ed60616c7c3afadd9fa3dd15d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Oct 2025 12:11:36 +0300 Subject: [PATCH 15/22] Updated on 2026-08-14 --- .../domain/wallets/hot/HotWalletPasswordRequester.kt | 3 ++- .../accesscoderequest/HotAccessCodeRequestModel.kt | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt index c3f3df8d95..ce09a009a3 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/hot/HotWalletPasswordRequester.kt @@ -36,7 +36,8 @@ interface HotWalletPasswordRequester { * @param hotWalletId The ID of the hot wallet to authenticate with. * @param authMode Indicates whether the request is for authentication mode. * In auth mode user can be deleted after failed attempts. - * @param hasBiometry Indicates whether to show biometric authentication option. + * @param hasBiometry Indicates whether to show biometric authentication option to the user. + * Will be ignored if the device does not support biometry at the moment of the request. */ data class AttemptRequest( val hotWalletId: HotWalletId, diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index 5c9a7148de..ad3e784dd7 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -8,6 +8,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS @@ -31,6 +32,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository, private val userWalletsListRepository: UserWalletsListRepository, + private val canUseBiometryUseCase: CanUseBiometryUseCase, ) : Model() { private val result = MutableStateFlow(null) @@ -60,7 +62,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( it.copy( isShown = true, accessCode = "", - useBiometricVisible = attemptRequest.hasBiometry, + useBiometricVisible = attemptRequest.isBiometryButtonVisible(), onAccessCodeChange = ::onAccessCodeChange, ) } @@ -83,7 +85,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( it.copy( accessCodeColor = PinTextColor.WrongCode, onAccessCodeChange = {}, - useBiometricVisible = currentRequest.hasBiometry, + useBiometricVisible = currentRequest.isBiometryButtonVisible(), ) } delay(timeMillis = 500) // Delay to show the wrong access code state @@ -212,6 +214,9 @@ internal class HotAccessCodeRequestModel @Inject constructor( dismiss() } + private suspend fun HotWalletPasswordRequester.AttemptRequest.isBiometryButtonVisible(): Boolean = + hasBiometry && canUseBiometryUseCase() + private fun dismissState() { uiState.update { it.copy(isShown = false) From aefaf636e87dc8bd715748896af970db0095663e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 7 Oct 2025 19:37:13 +0500 Subject: [PATCH 16/22] Updated on 2026-08-14 --- .../DefaultTangemPayTxHistoryRepository.kt | 2 +- .../PreviewTangemPayTxHistoryComponent.kt | 1 + .../TangemPayEmptyTransactionHistoryState.kt | 29 ++++++++ .../model/TangemPayTxHistoryModel.kt | 17 ++++- .../tangempay/ui/TangemPayDetailsScreen.kt | 50 +++++++------- .../tangempay/ui/TangemPayTxHistoryUi.kt | 66 ++++++++++++++++++- .../utils/TangemPayTxHistoryListManager.kt | 2 + .../utils/TangemPayTxHistoryState.kt | 1 + 8 files changed, 142 insertions(+), 26 deletions(-) create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt index be43b2bd72..7e197fea52 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayTxHistoryRepository.kt @@ -86,6 +86,6 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( }.result val items = TangemPayTxHistoryItemConverter.convertList(result.transactions).filterNotNull() txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items) - } + }.onLeft { error(it.toString()) } } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt index b430960c14..ba9bbc588f 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/PreviewTangemPayTxHistoryComponent.kt @@ -21,6 +21,7 @@ internal class PreviewTangemPayTxHistoryComponent(txHistoryUM: TangemPayTxHistor companion object { val loadingUM = TangemPayTxHistoryUM.Loading(isBalanceHidden = true) val emptyUM = TangemPayTxHistoryUM.Empty(isBalanceHidden = true) + val errorUM = TangemPayTxHistoryUM.Error(isBalanceHidden = true, onReload = {}) val contentUM = TangemPayTxHistoryUM.Content( isBalanceHidden = false, loadMore = { false }, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt new file mode 100644 index 0000000000..50816ac2a7 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEmptyTransactionHistoryState.kt @@ -0,0 +1,29 @@ +package com.tangem.features.tangempay.entity + +import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.tangempay.details.impl.R + +internal sealed class TangemPayEmptyTransactionHistoryState { + + abstract val iconRes: Int + abstract val text: TextReference + + data class FailedToLoad( + private val onReload: () -> Unit, + ) : TangemPayEmptyTransactionHistoryState() { + override val iconRes: Int = R.drawable.ic_alert_history_64 + override val text: TextReference = resourceReference(R.string.transaction_history_error_failed_to_load) + val actionButtonConfig = ActionButtonConfig( + text = resourceReference(R.string.common_reload), + iconResId = R.drawable.ic_refresh_24, + onClick = onReload, + ) + } + + data object Empty : TangemPayEmptyTransactionHistoryState() { + override val iconRes: Int = R.drawable.ic_empty_token_64 + override val text: TextReference = resourceReference(R.string.transaction_history_empty_transactions) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt index 1b3778feb0..ac67e1d5b4 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayTxHistoryModel.kt @@ -54,11 +54,16 @@ internal class TangemPayTxHistoryModel @Inject constructor( .onEach(::updateState) .launchIn(modelScope) listManager.paginationStatus - .onEach { paginationStatus -> handlePaginationStatus(paginationStatus) } + .onEach(::handlePaginationStatus) + .launchIn(modelScope) + listManager.emptyStatus + .onEach(::handleEmptyState) .launchIn(modelScope) } private fun updateState(items: ImmutableList) { + if (items.isEmpty()) return // fast exit. If items is empty, no need to update ui items + uiState.update { state -> if (state is TangemPayTxHistoryUM.Content) { state.copy(items = items) @@ -72,6 +77,12 @@ internal class TangemPayTxHistoryModel @Inject constructor( } } + private fun handleEmptyState(isEmpty: Boolean) { + if (isEmpty) { + uiState.update { getEmptyState(it.isBalanceHidden) } + } + } + private fun handlePaginationStatus(status: PaginationStatus<*>) { uiState.update { state -> when (status) { @@ -108,6 +119,10 @@ internal class TangemPayTxHistoryModel @Inject constructor( Timber.d("onTransactionClick: $item") } + private fun getEmptyState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Empty { + return TangemPayTxHistoryUM.Empty(isBalanceHidden = isBalanceHidden) + } + private fun getErrorState(isBalanceHidden: Boolean): TangemPayTxHistoryUM.Error { return TangemPayTxHistoryUM.Error(isBalanceHidden = isBalanceHidden, onReload = ::reload) } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index c2e6cc25f7..bd9cc78a83 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -2,12 +2,7 @@ package com.tangem.features.tangempay.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.LinearEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.rememberInfiniteTransition -import androidx.compose.animation.core.tween +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -15,12 +10,8 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -44,21 +35,14 @@ import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu import com.tangem.core.ui.components.snackbar.TangemSnackbarHost import com.tangem.core.ui.components.text.applyBladeBrush -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.orMaskWithStars -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.TokenDetailsTopBarTestTags import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent import com.tangem.features.tangempay.details.impl.R -import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM -import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState -import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig -import com.tangem.features.tangempay.entity.TangemPayDetailsUM +import com.tangem.features.tangempay.entity.* import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf @@ -415,8 +399,8 @@ private fun TangemPayDetailsTopAppBar(config: TangemPayDetailsTopBarConfig, modi ) } -@Preview(device = Devices.PIXEL_7_PRO, group = "day") -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO, group = "night") +@Preview(device = Devices.PIXEL_7_PRO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, device = Devices.PIXEL_7_PRO) @Composable private fun TangemPayDetailsScreenPreview( @PreviewParameter(TangemPayDetailsUMProvider::class) state: TangemPayDetailsUM, @@ -473,4 +457,26 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider( + collection = listOf( + PreviewTangemPayTxHistoryComponent.loadingUM, + PreviewTangemPayTxHistoryComponent.contentUM, + PreviewTangemPayTxHistoryComponent.emptyUM, + PreviewTangemPayTxHistoryComponent.errorUM, + ), ) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt index b7c07cbc03..466c8de420 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayTxHistoryUi.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ChainStyle import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension @@ -27,6 +28,7 @@ import coil.compose.rememberAsyncImagePainter import com.tangem.core.ui.R import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.buttons.actions.ActionButton import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.components.transactions.TxHistoryGroupTitle import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -34,7 +36,9 @@ import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.test.EmptyTransactionBlockTestTags import com.tangem.core.ui.test.TransactionHistoryBlockTestTags +import com.tangem.features.tangempay.entity.TangemPayEmptyTransactionHistoryState import com.tangem.features.tangempay.entity.TangemPayTransactionState import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM @@ -43,12 +47,25 @@ private const val LOAD_ITEMS_BUFFER = 20 internal fun LazyListScope.tangemPayTxHistoryItems(listState: LazyListState, state: TangemPayTxHistoryUM) { when (state) { is TangemPayTxHistoryUM.Content -> contentItems(listState = listState, state = state) - is TangemPayTxHistoryUM.Empty -> TODO("[REDACTED_JIRA]") - is TangemPayTxHistoryUM.Error -> TODO("[REDACTED_JIRA]") + is TangemPayTxHistoryUM.Empty -> nonContentItem(state = TangemPayEmptyTransactionHistoryState.Empty) + is TangemPayTxHistoryUM.Error -> nonContentItem( + state = TangemPayEmptyTransactionHistoryState.FailedToLoad(onReload = state.onReload), + ) is TangemPayTxHistoryUM.Loading -> loadingItems(state = state) } } +private fun LazyListScope.nonContentItem(state: TangemPayEmptyTransactionHistoryState, modifier: Modifier = Modifier) { + item(key = state::class.java, contentType = state::class.java) { + TangemPayEmptyTransactionBlock( + state = state, + modifier = modifier + .padding(horizontal = TangemTheme.dimens.spacing16, vertical = TangemTheme.dimens.spacing12) + .fillMaxWidth(), + ) + } +} + private fun LazyListScope.contentItems(listState: LazyListState, state: TangemPayTxHistoryUM.Content) { itemsIndexed( items = state.items, @@ -370,4 +387,49 @@ private fun Timestamp(state: TangemPayTransactionState, modifier: Modifier = Mod ) } } +} + +@Composable +private fun TangemPayEmptyTransactionBlock( + state: TangemPayEmptyTransactionHistoryState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(color = TangemTheme.colors.background.primary) + .padding(vertical = TangemTheme.dimens.spacing24) + .testTag(EmptyTransactionBlockTestTags.BLOCK), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing24), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier + .size(TangemTheme.dimens.size64) + .testTag(EmptyTransactionBlockTestTags.ICON), + painter = painterResource(id = state.iconRes), + tint = TangemTheme.colors.icon.inactive, + contentDescription = null, + ) + + Text( + modifier = Modifier + .padding(horizontal = TangemTheme.dimens.spacing32) + .testTag(EmptyTransactionBlockTestTags.TEXT), + textAlign = TextAlign.Center, + text = state.text.resolveReference(), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + ) + + when (state) { + is TangemPayEmptyTransactionHistoryState.Empty -> Unit + is TangemPayEmptyTransactionHistoryState.FailedToLoad -> ActionButton( + modifier = Modifier + .padding(horizontal = 24.dp) + .fillMaxWidth(), + config = state.actionButtonConfig, + ) + } + } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt index 3ccf9cf202..40cd31c7da 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryListManager.kt @@ -33,6 +33,7 @@ internal class TangemPayTxHistoryListManager( private val uiManager = TangemPayTxHistoryUiManager(state = state, txHistoryUiActions = txHistoryUiActions) val uiItems: Flow> = uiManager.items + val emptyStatus: Flow = state.map { it.isEmpty }.distinctUntilChanged() val paginationStatus: Flow> = state.map { it.status }.distinctUntilChanged() suspend fun launchPagination() = coroutineScope { @@ -80,6 +81,7 @@ internal class TangemPayTxHistoryListManager( newCurrencyBatches = batchListState.data, clearUiBatches = clearUiBatches, ), + isEmpty = batchListState.status is PaginationStatus.EndOfPagination && batchListState.data.isEmpty(), ) } } diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt index 5717238dda..5df7a11936 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayTxHistoryState.kt @@ -7,4 +7,5 @@ import com.tangem.pagination.PaginationStatus internal data class TangemPayTxHistoryState( val status: PaginationStatus<*> = PaginationStatus.None, val uiBatches: List>> = listOf(), + val isEmpty: Boolean = false, ) \ No newline at end of file From 9ff657252a12b81fc9c0d55af66c9fe655dd4f82 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Oct 2025 17:35:41 +0400 Subject: [PATCH 17/22] Updated on 2026-08-14 --- domain/account/status/build.gradle.kts | 3 + .../status/di/AccountStatusUseCaseModule.kt | 7 +- .../GetAccountCurrencyStatusUseCase.kt | 102 +++++++++++- ...faultMultiAccountStatusListProducerTest.kt | 0 ...aultSingleAccountStatusListProducerTest.kt | 0 .../GetAccountCurrencyStatusUseCaseTest.kt | 157 ++++++++++++++++++ .../CryptoCurrencyStatusesFlowFactoryTest.kt | 0 .../derivation/AccountNodeRecognizer.kt | 16 +- 8 files changed, 272 insertions(+), 13 deletions(-) rename domain/account/status/src/test/{java => kotlin}/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt (100%) rename domain/account/status/src/test/{java => kotlin}/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt (100%) create mode 100644 domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt rename domain/account/status/src/test/{java => kotlin}/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt (100%) diff --git a/domain/account/status/build.gradle.kts b/domain/account/status/build.gradle.kts index 1bc12d67f3..564baa96b8 100644 --- a/domain/account/status/build.gradle.kts +++ b/domain/account/status/build.gradle.kts @@ -25,12 +25,15 @@ dependencies { api(projects.domain.staking) api(projects.domain.tokens) + implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) implementation(deps.kotlin.datetime) implementation(deps.kotlin.serialization) implementation(deps.timber) + implementation(tangemDeps.blockchain) + // region DI implementation(deps.hilt.android) kapt(deps.hilt.kapt) diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt index 2e1d0ce365..f7cab3c413 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/di/AccountStatusUseCaseModule.kt @@ -1,5 +1,6 @@ package com.tangem.domain.account.status.di +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCase import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.supplier.SingleAccountListSupplier @@ -31,7 +32,9 @@ internal object AccountStatusUseCaseModule { @Provides @Singleton - fun provideGetAccountCurrencyStatusUseCase(): GetAccountCurrencyStatusUseCase { - return GetAccountCurrencyStatusUseCase() + fun provideGetAccountCurrencyStatusUseCase( + singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + ): GetAccountCurrencyStatusUseCase { + return GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = singleAccountStatusListSupplier) } } \ No newline at end of file diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt index be08772f57..1846999f3d 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCase.kt @@ -2,24 +2,110 @@ package com.tangem.domain.account.status.usecase import arrow.core.Option import arrow.core.none +import arrow.core.toOption +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.lib.crypto.derivation.AccountNodeRecognizer /** * Use case to retrieve the status of a specific cryptocurrency associated with an account. * + * @property singleAccountStatusListSupplier supplier to get the list of account statuses. + * [REDACTED_AUTHOR] */ -// TODO: Implement [REDACTED_JIRA] -class GetAccountCurrencyStatusUseCase { +class GetAccountCurrencyStatusUseCase( + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, +) { /** - * Invokes the use case to get the [AccountCryptoCurrencyStatus] for the given [currencyId]. + * Invokes the use case to get the status of a specific cryptocurrency for a given user wallet. * - * @param currencyId The ID of the cryptocurrency to look up. - * - * @return An [Option] containing the [AccountCryptoCurrencyStatus] if found, - * or [arrow.core.None] if not found or if any validation fails. + * @param userWalletId the ID of the user wallet. + * @param currency the cryptocurrency for which the status is to be retrieved. + * @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None. */ - suspend operator fun invoke(currencyId: CryptoCurrency.ID): Option = none() + suspend operator fun invoke( + userWalletId: UserWalletId, + currency: CryptoCurrency, + ): Option { + return invoke(userWalletId = userWalletId, currencyId = currency.id, network = currency.network) + } + + /** + * Invokes the use case to get the status of a specific cryptocurrency by its ID for a given user wallet and network. + * If the [network] is null, it searches across all accounts for the cryptocurrency. + * + * @param userWalletId the ID of the user wallet. + * @param currencyId the ID of the cryptocurrency. + * @param network the network associated with the cryptocurrency, can be null. + * @return an [Option] containing [AccountCryptoCurrencyStatus] if found, otherwise None. + */ + suspend operator fun invoke( + userWalletId: UserWalletId, + currencyId: CryptoCurrency.ID, + network: Network?, + ): Option { + val accountStatusList = singleAccountStatusListSupplier.getSyncOrNull( + params = SingleAccountStatusListProducer.Params(userWalletId), + ) ?: return none() + + return accountStatusList.getExpectedAccountStatuses(network) + .asSequence() + .filterIsInstance() + .mapNotNull { accountStatus -> + val status = accountStatus.flattenCurrencies().firstOrNull { it.currency.id == currencyId } + ?: return@mapNotNull null + + AccountCryptoCurrencyStatus(account = accountStatus.account, status = status) + } + .firstOrNull() + .toOption() + } + + /** + * Retrieves the expected account statuses based on the provided [network]. + * If the [network] is null, all account statuses are returned. + * If the network has a specific derivation index, it filters the accounts accordingly. + * + * @param network the network to filter accounts by, can be null. + * @return a set of [AccountStatus] that match the expected criteria. + */ + private fun AccountStatusList.getExpectedAccountStatuses(network: Network?): Set { + val possibleAccountIndex = network?.getAccountIndexOrNull() + + return when (possibleAccountIndex) { + // currency can be in any account + null -> accountStatuses + // currency only in the main account + DerivationIndex.Main.value -> setOf(mainAccount) + // currency only in the account with specific derivation index or in the main account + else -> { + val accountStatus = accountStatuses.firstOrNull { + val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@firstOrNull false + + cryptoPortfolio.derivationIndex.value == possibleAccountIndex + } + + setOfNotNull(accountStatus, mainAccount) + } + } + } + + private fun Network.getAccountIndexOrNull(): Int? { + val blockchain = Blockchain.fromNetworkId(networkId = rawId) ?: return null + val recognizer = AccountNodeRecognizer(blockchain) + + return recognizer.recognize(derivationPath)?.toInt() + } } \ No newline at end of file diff --git a/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt similarity index 100% rename from domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultMultiAccountStatusListProducerTest.kt diff --git a/domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt similarity index 100% rename from domain/account/status/src/test/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt new file mode 100644 index 0000000000..b8614cc704 --- /dev/null +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/GetAccountCurrencyStatusUseCaseTest.kt @@ -0,0 +1,157 @@ +package com.tangem.domain.account.status.usecase + +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.common.test.utils.assertNone +import com.tangem.common.test.utils.assertSome +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus +import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.core.utils.lceLoading +import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWalletId +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetAccountCurrencyStatusUseCaseTest { + + private val supplier = mockk() + private val useCase = GetAccountCurrencyStatusUseCase(singleAccountStatusListSupplier = supplier) + + private val userWalletId = UserWalletId("011") + private val supplierParams = SingleAccountStatusListProducer.Params(userWalletId) + private val currency = MockCryptoCurrencyFactory().ethereum.let { + val derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/1") + + it.copy( + network = it.network.copy( + id = Network.ID(value = "ethereum", derivationPath = derivationPath), + derivationPath = derivationPath, + ), + ) + } + + @BeforeEach + fun setUp() { + clearMocks(supplier) + } + + @Test + fun `invoke returns None when supplier returns null`() = runTest { + // Arrange + coEvery { supplier.getSyncOrNull(supplierParams) } returns null + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + assertNone(actual) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invoke returns None when AccountList does not contain required currency id`() = runTest { + // Arrange + val accountStatus = AccountStatus.CryptoPortfolio( + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + tokenList = TokenList.Empty, + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(accountStatus) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + assertNone(actual) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invoke returns Some if network is not null`() = runTest { + // Arrange + val mainAccountStatus = AccountStatus.CryptoPortfolio( + account = Account.CryptoPortfolio.createMainAccount(userWalletId), + tokenList = TokenList.Empty, + priceChangeLce = lceLoading(), + ) + + val account = mockk(relaxed = true) { + every { this@mockk.derivationIndex } returns DerivationIndex(1).getOrNull()!! + every { this@mockk.cryptoCurrencies } returns setOf(currency) + } + val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) + val accountStatus = AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Loading, + sortedBy = TokensSortType.NONE, + currencies = listOf(currencyStatus), + ), + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(mainAccountStatus, accountStatus, mockk()) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = currency.network) + + // Assert + val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) + assertSome(actual, expected) + + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } + + @Test + fun `invoke returns Some if network is null`() = runTest { + // Arrange + val account = mockk(relaxed = true) { + every { this@mockk.cryptoCurrencies } returns setOf(currency) + } + val currencyStatus = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading) + val accountStatus = AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenList.Ungrouped( + totalFiatBalance = TotalFiatBalance.Loading, + sortedBy = TokensSortType.NONE, + currencies = listOf(currencyStatus), + ), + priceChangeLce = lceLoading(), + ) + + val accountStatusList = mockk(relaxed = true) { + every { this@mockk.accountStatuses } returns setOf(accountStatus) + } + + coEvery { supplier.getSyncOrNull(supplierParams) } returns accountStatusList + + // Act + val actual = useCase(userWalletId = userWalletId, currencyId = currency.id, network = null) + + // Assert + val expected = AccountCryptoCurrencyStatus(account = accountStatus.account, status = currencyStatus) + assertSome(actual, expected) + coVerifyOrder { supplier.getSyncOrNull(supplierParams) } + } +} \ No newline at end of file diff --git a/domain/account/status/src/test/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt similarity index 100% rename from domain/account/status/src/test/java/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/utils/CryptoCurrencyStatusesFlowFactoryTest.kt diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt index 6008e6ea58..1312cb0581 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/derivation/AccountNodeRecognizer.kt @@ -3,6 +3,7 @@ package com.tangem.lib.crypto.derivation import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.isUTXO import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.network.Network /** * Utility class to recognize the account node in a derivation path based on the blockchain type. @@ -21,12 +22,21 @@ class AccountNodeRecognizer(blockchain: Blockchain) { NON_UTXO_BLOCKCHAIN_NODE_INDEX } + /** Recognizes the account node value from the given [derivationPath] */ + fun recognize(derivationPath: Network.DerivationPath): Long? { + val derivationPathValue = derivationPath.value ?: return null + + return recognize(derivationPathValue = derivationPathValue) + } + /** Recognizes the account node value from the given derivation path string [derivationPathValue] */ fun recognize(derivationPathValue: String): Long? { + if (derivationPathValue.isBlank()) return null + return runCatching { - recognize(derivationPath = DerivationPath(rawPath = derivationPathValue)) - } - .getOrNull() + val cardSdkDerivationPath = DerivationPath(rawPath = derivationPathValue) + recognize(derivationPath = cardSdkDerivationPath) + }.getOrNull() } /** Recognizes the account node value from the given [derivationPath] */ From f9ae0d5188752e6b7ba13cdfa069e2a386630393 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Oct 2025 20:03:08 +0500 Subject: [PATCH 18/22] Updated on 2026-08-14 --- .../ui/amountScreen/AmountScreenContent.kt | 40 ++--- .../converters/AmountCurrencyTransformer.kt | 1 - .../converters/AmountReduceByTransformer.kt | 7 +- .../converters/AmountReduceToTransformer.kt | 7 +- .../converters/AmountStateConverter.kt | 110 +----------- .../field/AmountBoundaryUpdateTransformer.kt | 12 +- .../converters/field/AmountFieldConverter.kt | 67 +------- .../AmountFieldSetMaxAmountTransformer.kt | 2 +- .../models/AmountSegmentedButtonsConfig.kt | 21 --- .../ui/amountScreen/models/AmountState.kt | 19 +-- .../preview/AmountStatePreviewData.kt | 27 +-- .../common/ui/amountScreen/ui/AmountBlock.kt | 14 +- .../ui/amountScreen/ui/AmountButtons.kt | 126 -------------- .../common/ui/amountScreen/ui/AmountField.kt | 160 ------------------ .../amountScreen/ui/AmountFieldContainer.kt | 135 +++++---------- .../NavigationButtonsBlock.kt | 1 - .../NavigationButtonsState.kt | 1 - .../preview/NavigationButtonsPreview.kt | 12 -- .../features/send/v2/send/model/SendModel.kt | 2 +- .../amount/SendAmountComponent.kt | 2 - .../amount/model/SendAmountModel.kt | 2 +- .../amount/ui/SendAmountContent.kt | 3 - ...firmationNotificationsTransformerV2Test.kt | 5 - .../analytics/utils/StakingAnalyticSender.kt | 4 +- .../state/StakingStateController.kt | 2 +- .../presentation/state/StakingStateRouter.kt | 2 + .../impl/presentation/state/StakingUiState.kt | 1 + .../transformers/SetAmountDataTransformer.kt | 5 +- .../SetButtonsStateTransformer.kt | 50 ++---- ...etConfirmationStateCompletedTransformer.kt | 2 + .../SetInitialDataStateTransformer.kt | 5 +- .../state/transformers/SetTitleTransformer.kt | 3 +- .../ui/StakingConfirmationContent.kt | 6 +- .../impl/presentation/ui/StakingScreen.kt | 17 +- .../presentation/ui/StakingSuccessContent.kt | 86 ++++++++++ .../v2/impl/amount/entity/SwapAmountUM.kt | 10 +- .../converter/SwapAmountFieldConverter.kt | 4 +- .../SwapAmountBalanceHiddenTransformer.kt | 2 - .../impl/amount/ui/SwapAmountBlockContent.kt | 1 - .../ui/preview/SwapAmountContentPreview.kt | 5 +- 40 files changed, 225 insertions(+), 756 deletions(-) delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingSuccessContent.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt index fdd122631d..f0fa604881 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/AmountScreenContent.kt @@ -13,21 +13,17 @@ import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountScreenClickIntentsStub import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData -import com.tangem.common.ui.amountScreen.ui.amountField import com.tangem.common.ui.amountScreen.ui.amountFieldV2 -import com.tangem.common.ui.amountScreen.ui.buttons import com.tangem.core.ui.res.TangemThemePreview /** * Amount screen with field * @param amountState amount state - * @param isBalanceHidden flag hidden balances * @param clickIntents amount screen clicks */ @Composable fun AmountScreenContent( amountState: AmountState, - isBalanceHidden: Boolean, clickIntents: AmountScreenClickIntents, modifier: Modifier = Modifier, extraContent: (@Composable () -> Unit)? = null, @@ -38,32 +34,17 @@ fun AmountScreenContent( .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp), ) { - if (amountState.isRedesignEnabled) { - amountFieldV2( - amountState = amountState, - onValueChange = clickIntents::onAmountValueChange, - onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, - onCurrencyChange = clickIntents::onCurrencyChangeClick, - onMaxAmountClick = clickIntents::onMaxValueClick, - ) - if (extraContent != null) { - item("EXTRA_CONTENT_KEY") { - extraContent() - } + amountFieldV2( + amountState = amountState, + onValueChange = clickIntents::onAmountValueChange, + onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, + onCurrencyChange = clickIntents::onCurrencyChangeClick, + onMaxAmountClick = clickIntents::onMaxValueClick, + ) + if (extraContent != null) { + item("EXTRA_CONTENT_KEY") { + extraContent() } - } else if (amountState is AmountState.Data) { - amountField( - amountState = amountState, - isBalanceHidden = isBalanceHidden, - onValueChange = clickIntents::onAmountValueChange, - onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, - ) - buttons( - segmentedButtonConfig = amountState.segmentedButtonConfig, - clickIntents = clickIntents, - isSegmentedButtonsEnabled = amountState.isSegmentedButtonsEnabled, - selectedButton = amountState.selectedButton, - ) } } } @@ -78,7 +59,6 @@ private fun SendAmountContentPreview( TangemThemePreview { AmountScreenContent( amountState = amountState, - isBalanceHidden = false, clickIntents = AmountScreenClickIntentsStub, ) } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt index 7406013c29..0faf2844b8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountCurrencyTransformer.kt @@ -38,7 +38,6 @@ class AmountCurrencyTransformer( keyboardType = KeyboardType.Number, ), ), - selectedButton = prevState.segmentedButtonConfig.indexOfFirst { it.isFiat == value }, ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt index f96fa69bd0..783682376d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceByTransformer.kt @@ -70,10 +70,9 @@ class AmountReduceByTransformer( error = when { isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isLessThanMinimumIfProvided -> { - val minimumAmount = minimumTransactionAmount - ?.amount - ?.format { crypto(cryptoCurrencyStatus.currency) } - .orEmpty() + val minimumAmount = minimumTransactionAmount.amount.format { + crypto(cryptoCurrencyStatus.currency) + } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt index 0afd5d8bc5..864db79015 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountReduceToTransformer.kt @@ -64,10 +64,9 @@ class AmountReduceToTransformer( error = when { isExceedBalance -> resourceReference(R.string.send_validation_amount_exceeds_balance) isLessThanMinimumIfProvided -> { - val minimumAmount = minimumTransactionAmount - ?.amount - ?.format { crypto(cryptoCurrencyStatus.currency) } - .orEmpty() + val minimumAmount = minimumTransactionAmount.amount.format { + crypto(cryptoCurrencyStatus.currency) + } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt index 36f37a67ab..cdcd0771cc 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/AmountStateConverter.kt @@ -1,104 +1,35 @@ package com.tangem.common.ui.amountScreen.converters -import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverter -import com.tangem.common.ui.amountScreen.converters.field.AmountFieldConverterV2 import com.tangem.common.ui.amountScreen.models.AmountParameters -import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DOT import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import kotlinx.collections.immutable.persistentListOf - -/** - * Converts initial [String] to [AmountState] - * - * @property clickIntents amount screen clicks - * @property appCurrencyProvider selected app currency provider - * @property maxEnterAmount max enter amount data - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider - * @property iconStateConverter currency icon converter - */ -@Deprecated("Use AmountStateConverterV2") -class AmountStateConverter( - private val clickIntents: AmountScreenClickIntents, - private val appCurrencyProvider: Provider, - private val cryptoCurrencyStatusProvider: Provider, - private val maxEnterAmount: EnterAmountBoundary, - private val iconStateConverter: CryptoCurrencyToIconStateConverter, -) : Converter { - - private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { - AmountFieldConverter( - clickIntents = clickIntents, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, - ) - } - - override fun convert(value: AmountParameters): AmountState { - val appCurrency = appCurrencyProvider() - val status = cryptoCurrencyStatusProvider() - val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } - val crypto = maxEnterAmount.amount.format { crypto(status.currency) } - val hasNoFeeRate = status.value.fiatRate.isNullOrZero() - - return AmountState.Data( - title = value.title, - availableBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)), - availableBalanceCrypto = stringReference(crypto), - availableBalanceFiat = stringReference(fiat), - tokenName = stringReference(status.currency.name), - tokenIconState = iconStateConverter.convert(status), - amountTextField = amountFieldConverter.convert(value.value), - isPrimaryButtonEnabled = false, - appCurrency = appCurrency, - segmentedButtonConfig = persistentListOf( - AmountSegmentedButtonsConfig( - title = stringReference(status.currency.symbol), - iconState = iconStateConverter.convertCustom( - value = status, - forceGrayscale = hasNoFeeRate, - showCustomTokenBadge = false, - ), - isFiat = false, - ), - AmountSegmentedButtonsConfig( - title = stringReference(appCurrency.code), - iconUrl = appCurrency.iconSmallUrl, - isFiat = true, - ), - ), - isSegmentedButtonsEnabled = !hasNoFeeRate, - selectedButton = 0, - isRedesignEnabled = false, - ) - } -} /** * Converts initial [String] to [AmountState] * * @property clickIntents amount screen clicks * @property appCurrency selected app currency - * @property maxEnterAmount max enter amount data * @property cryptoCurrencyStatus current cryptocurrency status + * @property maxEnterAmount max enter amount data * @property iconStateConverter currency icon converter * @property isBalanceHidden is balance hidden status */ @Suppress("LongParameterList") -class AmountStateConverterV2( +class AmountStateConverter( private val clickIntents: AmountScreenClickIntents, private val appCurrency: AppCurrency, private val cryptoCurrencyStatus: CryptoCurrencyStatus, @@ -108,7 +39,7 @@ class AmountStateConverterV2( ) : Converter { private val amountFieldConverter by lazy(LazyThreadSafetyMode.NONE) { - AmountFieldConverterV2( + AmountFieldConverter( clickIntents = clickIntents, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrency = appCurrency, @@ -118,19 +49,13 @@ class AmountStateConverterV2( override fun convert(value: AmountParameters): AmountState { val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } - val noFeeRate = cryptoCurrencyStatus.value.fiatRate.isNullOrZero() if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.Loading) { - return AmountState.Empty(isRedesignEnabled = true) + return AmountState.Empty } return AmountState.Data( title = value.title, - availableBalance = combinedReference( - stringReference(crypto), - stringReference(" $DOT "), - stringReference(fiat), - ).orMaskWithStars(isBalanceHidden), availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden), availableBalanceFiat = if (isBalanceHidden) { TextReference.EMPTY @@ -145,25 +70,6 @@ class AmountStateConverterV2( amountTextField = amountFieldConverter.convert(value.value), isPrimaryButtonEnabled = false, appCurrency = appCurrency, - segmentedButtonConfig = persistentListOf( - AmountSegmentedButtonsConfig( - title = stringReference(cryptoCurrencyStatus.currency.symbol), - iconState = iconStateConverter.convertCustom( - value = cryptoCurrencyStatus, - forceGrayscale = noFeeRate, - showCustomTokenBadge = false, - ), - isFiat = false, - ), - AmountSegmentedButtonsConfig( - title = stringReference(appCurrency.code), - iconUrl = appCurrency.iconSmallUrl, - isFiat = true, - ), - ), - isSegmentedButtonsEnabled = !noFeeRate, - selectedButton = 0, - isRedesignEnabled = true, ) } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt index 53bdef9c57..21a6f4e7f5 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountBoundaryUpdateTransformer.kt @@ -2,7 +2,10 @@ package com.tangem.common.ui.amountScreen.converters.field import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format @@ -32,14 +35,7 @@ class AmountBoundaryUpdateTransformer( val fiat = maxEnterAmount.fiatAmount.format { fiat(appCurrency.code, appCurrency.symbol) } val crypto = maxEnterAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } - val availableBalance = combinedReference( - stringReference(crypto), - stringReference(" $DOT "), - stringReference(fiat), - ) - return prevState.copy( - availableBalance = availableBalance.orMaskWithStars(isBalanceHidden), availableBalanceCrypto = stringReference(crypto).orMaskWithStars(isBalanceHidden), availableBalanceFiat = if (isBalanceHidden) { TextReference.EMPTY diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt index f0baf3a1e9..411de67feb 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldConverter.kt @@ -13,75 +13,10 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType import com.tangem.domain.tokens.model.convertToAmount -import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import java.math.BigDecimal -/** - * Converts initial [String] to [AmountFieldModel] - * - * @property clickIntents amount screen clicks - * @property appCurrencyProvider selected app currency provider - * @property cryptoCurrencyStatusProvider current cryptocurrency status provider - */ -@Deprecated("Use AmountFieldConverterV2") -class AmountFieldConverter( - private val clickIntents: AmountScreenClickIntents, - private val cryptoCurrencyStatusProvider: Provider, - private val appCurrencyProvider: Provider, -) : Converter { - - override fun convert(value: String): AmountFieldModel { - val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() - val cryptoDecimal = value.toBigDecimalOrNull() ?: BigDecimal.ZERO - val cryptoAmount = cryptoDecimal.convertToAmount(cryptoCurrencyStatus.currency) - val fiatRate = cryptoCurrencyStatus.value.fiatRate - val (fiatValue, fiatDecimal) = when { - fiatRate.isNullOrZero() -> "" to null - value.isEmpty() -> "" to BigDecimal.ZERO - else -> { - val fiatDecimal = fiatRate?.multiply(cryptoDecimal) - val fiatValue = fiatDecimal?.parseBigDecimal(FIAT_DECIMALS).orEmpty() - fiatValue to fiatDecimal - } - } - val isDoneActionEnabled = !cryptoDecimal.isNullOrZero() - return AmountFieldModel( - value = value, - fiatValue = fiatValue, - onValueChange = clickIntents::onAmountValueChange, - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - keyboardActions = KeyboardActions( - onDone = { clickIntents.onAmountNext() }, - ), - isFiatValue = false, - cryptoAmount = cryptoAmount, - fiatAmount = getAppCurrencyAmount(fiatDecimal, appCurrencyProvider()), - isError = false, - isWarning = false, - error = TextReference.EMPTY, - isFiatUnavailable = fiatRate == null, - isValuePasted = false, - onValuePastedTriggerDismiss = clickIntents::onAmountPasteTriggerDismiss, - ) - } - - private fun getAppCurrencyAmount(fiatValue: BigDecimal?, appCurrency: AppCurrency) = Amount( - currencySymbol = appCurrency.symbol, - value = fiatValue, - decimals = FIAT_DECIMALS, - type = AmountType.FiatType(appCurrency.code), - ) - - private companion object { - private const val FIAT_DECIMALS = 2 - } -} - /** * Converts initial [String] to [AmountFieldModel] * @@ -89,7 +24,7 @@ class AmountFieldConverter( * @property appCurrency selected app currency * @property cryptoCurrencyStatus current cryptocurrency status */ -class AmountFieldConverterV2( +class AmountFieldConverter( private val clickIntents: AmountScreenClickIntents, private val cryptoCurrencyStatus: CryptoCurrencyStatus, private val appCurrency: AppCurrency, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt index a8c20c0745..db245b6b30 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/converters/field/AmountFieldSetMaxAmountTransformer.kt @@ -54,7 +54,7 @@ class AmountFieldSetMaxAmountTransformer( isError = isLessThanMinimumIfProvided, error = when { isLessThanMinimumIfProvided -> { - val minimumAmount = minAmount?.amount.format { crypto(cryptoCurrencyStatus.currency) } + val minimumAmount = minAmount.amount.format { crypto(cryptoCurrencyStatus.currency) } resourceReference( R.string.transfer_notification_invalid_minimum_transaction_amount_text, wrappedList(minimumAmount, minimumAmount), diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt deleted file mode 100644 index 23469c4c03..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountSegmentedButtonsConfig.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.common.ui.amountScreen.models - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.TextReference - -/** - * Segmented buttons config - * - * @param title button title - * @param iconState currency icon state - * @param iconUrl currency icon url - * @param isFiat is fiat currency - */ -@Immutable -data class AmountSegmentedButtonsConfig( - val title: TextReference, - val iconState: CurrencyIconState? = null, - val iconUrl: String? = null, - val isFiat: Boolean, -) \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt index a063b984f5..d207f14e5a 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/models/AmountState.kt @@ -4,7 +4,6 @@ import androidx.compose.runtime.Stable import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.appcurrency.model.AppCurrency -import kotlinx.collections.immutable.PersistentList import java.math.BigDecimal /** Model for amount state */ @@ -12,18 +11,13 @@ import java.math.BigDecimal sealed class AmountState { abstract val isPrimaryButtonEnabled: Boolean - abstract val isRedesignEnabled: Boolean /** * @param isPrimaryButtonEnabled indicates if next state button enabled * @param title title - * @param availableBalance user crypto currency balance with fiat balance * @param availableBalanceCrypto user crypto currency balance in crypto * @param availableBalanceFiat user crypto currency balance in fiat * @param tokenIconState crypto currency icon state - * @param segmentedButtonConfig currency switcher config - * @param selectedButton selected currency index - * @param isSegmentedButtonsEnabled indicates if currency switches is enabled * @param amountTextField amount field state * @param appCurrency app currency * @param isEditingDisabled indicated whether amount is editable @@ -32,17 +26,11 @@ sealed class AmountState { */ data class Data( override val isPrimaryButtonEnabled: Boolean, - override val isRedesignEnabled: Boolean, val title: TextReference, - @Deprecated("Remove with SEND_REDESIGNED toggle") - val availableBalance: TextReference, val availableBalanceCrypto: TextReference, val availableBalanceFiat: TextReference, val tokenName: TextReference, val tokenIconState: CurrencyIconState, - val segmentedButtonConfig: PersistentList, - val selectedButton: Int, - val isSegmentedButtonsEnabled: Boolean, val amountTextField: AmountFieldModel, val appCurrency: AppCurrency, val isEditingDisabled: Boolean = false, @@ -50,8 +38,7 @@ sealed class AmountState { val isIgnoreReduce: Boolean = false, ) : AmountState() - data class Empty( - override val isPrimaryButtonEnabled: Boolean = false, - override val isRedesignEnabled: Boolean, - ) : AmountState() + data object Empty : AmountState() { + override val isPrimaryButtonEnabled: Boolean = false + } } \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt index 98f7c1e21d..0624a2c89d 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/preview/AmountStatePreviewData.kt @@ -3,7 +3,6 @@ package com.tangem.common.ui.amountScreen.preview import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.extensions.TextReference @@ -12,31 +11,18 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.Amount import com.tangem.domain.tokens.model.AmountType import com.tangem.utils.StringsSigns -import kotlinx.collections.immutable.persistentListOf import java.math.BigDecimal object AmountStatePreviewData { - val emptyState = AmountState.Empty(isRedesignEnabled = true) + val emptyState = AmountState.Empty val amountState = AmountState.Data( isPrimaryButtonEnabled = false, title = stringReference("Family Wallet"), - availableBalance = stringReference("2 130,81231238 USDT • 2 129,12 \$)"), availableBalanceCrypto = stringReference("2 130,81231238 USDT"), availableBalanceFiat = stringReference("1 232 129,12 \$"), tokenIconState = CurrencyIconState.Loading, - segmentedButtonConfig = persistentListOf( - AmountSegmentedButtonsConfig( - title = stringReference("USDT"), - iconState = CurrencyIconState.Locked, - isFiat = false, - ), - AmountSegmentedButtonsConfig( - title = stringReference("USD"), - isFiat = true, - ), - ), appCurrency = AppCurrency.Default, tokenName = stringReference("Tether"), amountTextField = AmountFieldModel( @@ -65,12 +51,9 @@ object AmountStatePreviewData { isValuePasted = false, onValuePastedTriggerDismiss = {}, ), - isSegmentedButtonsEnabled = true, - selectedButton = 0, - isRedesignEnabled = false, ) - val amountWithValueState = amountState.copy( + private val amountWithValueState = amountState.copy( amountTextField = amountState.amountTextField.copy( value = "100.00", cryptoAmount = amountState.amountTextField.cryptoAmount.copy( @@ -84,16 +67,10 @@ object AmountStatePreviewData { ) val amountStateV2 = amountState.copy( - isRedesignEnabled = true, - availableBalance = stringReference("2 130,81231238 USDT • 2 129,12 \$)"), availableBalanceCrypto = stringReference("2 130,81231238 USDT"), availableBalanceFiat = stringReference(" ${StringsSigns.DOT} 1 232 129,12 $"), ) - val amountWithValueFiatState = amountWithValueState.copy( - amountTextField = amountWithValueState.amountTextField.copy(isFiatValue = false), - ) - val amountStateV2WithoutRates = amountState.copy( amountTextField = amountState.amountTextField.copy( fiatAmount = amountState.amountTextField.fiatAmount.copy( diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt index 8e21c12dd9..e659c88013 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountBlock.kt @@ -16,10 +16,13 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData import com.tangem.core.ui.components.ResizableText +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat @@ -56,7 +59,16 @@ fun AmountBlock(amountState: AmountState, isClickDisabled: Boolean, isEditingDis .clickable(enabled = !isClickDisabled && !isEditingDisabled, onClick = onClick) .padding(TangemTheme.dimens.spacing16), ) { - CurrencyIcon(state = amountState.tokenIconState) + Text( + text = amountState.title.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + SpacerH(20.dp) + CurrencyIcon( + state = amountState.tokenIconState, + iconSize = 40.dp, + ) ResizableText( text = firstAmount, style = TangemTheme.typography.h2, diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt deleted file mode 100644 index 64d1c8f25c..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountButtons.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.tangem.common.ui.amountScreen.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyListScope -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.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.platform.testTag -import com.tangem.common.ui.R -import com.tangem.common.ui.amountScreen.AmountScreenClickIntents -import com.tangem.common.ui.amountScreen.models.AmountSegmentedButtonsConfig -import com.tangem.core.ui.components.SpacerWMax -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.currency.fiaticon.FiatIcon -import com.tangem.core.ui.components.currency.icon.CurrencyIcon -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.test.SendScreenTestTags -import kotlinx.collections.immutable.PersistentList - -private const val AMOUNT_BUTTONS_KEY = "amountButtonsKey" - -internal fun LazyListScope.buttons( - segmentedButtonConfig: PersistentList, - clickIntents: AmountScreenClickIntents, - isSegmentedButtonsEnabled: Boolean, - selectedButton: Int, -) { - item( - key = AMOUNT_BUTTONS_KEY, - ) { - val hapticFeedback = LocalHapticFeedback.current - Row { - if (segmentedButtonConfig.isNotEmpty()) { - SegmentedButtons( - modifier = Modifier - .weight(1f) - .height(TangemTheme.dimens.size40), - config = segmentedButtonConfig, - showIndication = false, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clickIntents.onCurrencyChangeClick(it.isFiat) - }, - initialSelectedItem = segmentedButtonConfig.getOrNull(selectedButton), - isEnabled = isSegmentedButtonsEnabled, - ) { - AmountCurrencyButton( - button = it, - isSegmentedButtonsEnabled = isSegmentedButtonsEnabled, - ) - } - } else { - SpacerWMax() - } - Text( - text = stringResourceSafe(R.string.send_max_amount), - style = TangemTheme.typography.button, - color = TangemTheme.colors.text.primary1, - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing8) - .height(TangemTheme.dimens.size40) - .clip(shape = RoundedCornerShape(TangemTheme.dimens.radius26)) - .background(TangemTheme.colors.button.secondary) - .clickable { - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - clickIntents.onMaxValueClick() - } - .padding( - vertical = TangemTheme.dimens.spacing10, - horizontal = TangemTheme.dimens.spacing34, - ) - .testTag(SendScreenTestTags.MAX_BUTTON), - ) - } - } -} - -@Composable -private fun AmountCurrencyButton(button: AmountSegmentedButtonsConfig, isSegmentedButtonsEnabled: Boolean) { - Row( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing10, - ) - .testTag(SendScreenTestTags.CURRENCY_BUTTON), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - ) { - val iconModifier = Modifier - .size(TangemTheme.dimens.size18) - .padding(horizontal = TangemTheme.dimens.spacing1) - if (button.isFiat) { - FiatIcon( - url = button.iconUrl, - size = TangemTheme.dimens.size18, - isGrayscale = !isSegmentedButtonsEnabled, - modifier = iconModifier.testTag(SendScreenTestTags.FIAT_ICON), - ) - } else if (button.iconState != null) { - CurrencyIcon( - state = button.iconState, - shouldDisplayNetwork = false, - modifier = iconModifier.testTag(SendScreenTestTags.CURRENCY_ICON), - ) - } - Text( - text = button.title.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.button, - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing8, - ), - ) - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt deleted file mode 100644 index 848bd0ab36..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountField.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.tangem.common.ui.amountScreen.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.requiredHeightIn -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment.Companion.BottomCenter -import androidx.compose.ui.Alignment.Companion.TopCenter -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDirection -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.components.fields.AmountTextField -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.test.SendScreenTestTags -import com.tangem.core.ui.utils.rememberDecimalFormat -import kotlinx.coroutines.delay - -@Composable -internal fun AmountField( - amountField: AmountFieldModel, - appCurrencyCode: String, - onValueChange: (String) -> Unit, - onValuePastedTriggerDismiss: () -> Unit, -) { - val decimalFormat = rememberDecimalFormat() - val isFiatValue = amountField.isFiatValue - val currencyCode = if (isFiatValue) appCurrencyCode else null - val (primaryAmount, primaryValue) = if (isFiatValue) { - amountField.fiatAmount to amountField.fiatValue - } else { - amountField.cryptoAmount to amountField.value - } - val requester = remember { FocusRequester() } - val symbolColor = if (primaryValue.isBlank()) TangemTheme.colors.text.disabled else TangemTheme.colors.text.primary1 - AmountTextField( - value = primaryValue, - decimals = primaryAmount.decimals, - visualTransformation = AmountVisualTransformation( - decimals = primaryAmount.decimals, - symbol = primaryAmount.currencySymbol, - currencyCode = currencyCode, - decimalFormat = decimalFormat, - symbolColor = symbolColor, - ), - onValueChange = onValueChange, - keyboardOptions = amountField.keyboardOptions, - keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.h2.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - isAutoResize = true, - isValuePasted = amountField.isValuePasted, - onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, - modifier = Modifier - .focusRequester(requester) - .padding( - top = TangemTheme.dimens.spacing24, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ) - .requiredHeightIn(min = TangemTheme.dimens.size32), - ) - - LaunchedEffect(key1 = Unit) { - delay(timeMillis = 200) - requester.requestFocus() - } - - AmountSecondary(amountField, appCurrencyCode) -} - -@Composable -private fun AmountSecondary(amountField: AmountFieldModel, appCurrencyCode: String) { - val secondaryAmount = if (amountField.isFiatValue) amountField.cryptoAmount else amountField.fiatAmount - Box( - modifier = Modifier - .fillMaxWidth() - .animateContentSize() - .padding( - top = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), - ) { - val text = if (amountField.isFiatValue) { - secondaryAmount.value.format { crypto(secondaryAmount.currencySymbol, secondaryAmount.decimals) } - } else { - secondaryAmount.value.format { - fiat( - fiatCurrencySymbol = secondaryAmount.currencySymbol, - fiatCurrencyCode = appCurrencyCode, - ) - } - } - Text( - text = text, - style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - modifier = Modifier - .align(TopCenter) - .padding(bottom = TangemTheme.dimens.spacing32) - .testTag(SendScreenTestTags.SECONDARY_AMOUNT), - ) - AmountFieldError( - isError = amountField.isError, - isWarning = amountField.isWarning, - error = amountField.error, - modifier = Modifier - .align(BottomCenter) - .padding( - top = TangemTheme.dimens.spacing20, - bottom = TangemTheme.dimens.spacing12, - ), - ) - } -} - -@Composable -private fun AmountFieldError( - isError: Boolean, - isWarning: Boolean, - error: TextReference, - modifier: Modifier = Modifier, -) { - AnimatedVisibility( - visible = isError || isWarning, - enter = fadeIn(), - exit = fadeOut(), - modifier = modifier, - ) { - val errorText = remember(this, error) { error } - val color = if (isError) TangemTheme.colors.text.warning else TangemTheme.colors.text.attention - Text( - text = errorText.resolveReference(), - style = TangemTheme.typography.caption2, - color = color, - textAlign = TextAlign.Center, - ) - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt index 6fd563f173..d5973e72b8 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/amountScreen/ui/AmountFieldContainer.kt @@ -15,8 +15,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.tangem.common.ui.R import com.tangem.common.ui.amountScreen.models.AmountState @@ -25,68 +23,12 @@ import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.orMaskWithStars import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.test.SendScreenTestTags private const val AMOUNT_FIELD_KEY = "amountFieldKey" -internal fun LazyListScope.amountField( - amountState: AmountState.Data, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, - onValueChange: (String) -> Unit, - onValuePastedTriggerDismiss: () -> Unit, -) { - item(key = AMOUNT_FIELD_KEY) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action), - ) { - Text( - text = amountState.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing14) - .testTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE), - ) - - val balance = amountState.availableBalance.orMaskWithStars(isBalanceHidden).resolveReference() - AnimatedContent( - targetState = balance, - label = "Hide Balance Animation", - ) { - Text( - text = it, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing2) - .testTag(SendScreenTestTags.AMOUNT_CONTAINER_TEXT), - ) - } - CurrencyIcon( - state = amountState.tokenIconState, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing32), - ) - AmountField( - amountField = amountState.amountTextField, - appCurrencyCode = amountState.appCurrency.code, - onValueChange = onValueChange, - onValuePastedTriggerDismiss = onValuePastedTriggerDismiss, - ) - } - } -} - internal fun LazyListScope.amountFieldV2( amountState: AmountState, modifier: Modifier = Modifier, @@ -183,46 +125,49 @@ private fun AmountInfo(amountUM: AmountState, onMaxAmountClick: () -> Unit, modi @Composable private fun AmountInfoMain(amountUM: AmountState, modifier: Modifier = Modifier) { AnimatedContent( - targetState = amountUM !is AmountState.Data, + targetState = amountUM, modifier = modifier, - ) { isContent -> - if (isContent) { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - TextShimmer( - style = TangemTheme.typography.subtitle2, - modifier = Modifier.width(56.dp), - ) - TextShimmer( - style = TangemTheme.typography.caption2, - modifier = Modifier.width(72.dp), - ) - } - } else { - val amountUM = amountUM as AmountState.Data - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { - Text( - text = amountUM.tokenName.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - ) - Row { - EllipsisText( - text = amountUM.availableBalanceCrypto.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.cryptoAmount.currencySymbol.length), - modifier = Modifier.weight(1f, fill = false), + ) { currentAmount -> + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + when (currentAmount) { + is AmountState.Data -> { + val amountUM = amountUM as AmountState.Data + Text( + text = amountUM.tokenName.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + maxLines = 1, ) - EllipsisText( - text = amountUM.availableBalanceFiat.resolveReference(), + Row { + EllipsisText( + text = amountUM.availableBalanceCrypto.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = TextEllipsis.OffsetEnd( + amountUM.amountTextField.cryptoAmount.currencySymbol.length, + ), + modifier = Modifier.weight(1f, fill = false), + ) + EllipsisText( + text = amountUM.availableBalanceFiat.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ellipsis = TextEllipsis.OffsetEnd( + amountUM.amountTextField.fiatAmount.currencySymbol.length, + ), + ) + } + } + AmountState.Empty -> { + TextShimmer( + style = TangemTheme.typography.subtitle2, + modifier = Modifier.width(56.dp), + ) + TextShimmer( style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ellipsis = TextEllipsis.OffsetEnd(amountUM.amountTextField.fiatAmount.currencySymbol.length), + modifier = Modifier.width(72.dp), ) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt index a52403de79..47233d7291 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsBlock.kt @@ -55,7 +55,6 @@ fun NavigationButtonsBlock( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - PreviousButton(state?.prevButton) NavigationPrimaryButton(state?.primaryButton, modifier = Modifier.weight(1f)) } } diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index 59bab529ed..6191b4de9c 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -8,7 +8,6 @@ sealed class NavigationButtonsState { data class Data( val primaryButton: NavigationButton?, - val prevButton: NavigationButton?, val extraButtons: Pair?, val txUrl: String? = null, val onTextClick: (String) -> Unit, diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt index 76ebce30a8..6c5a8d5991 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/preview/NavigationButtonsPreview.kt @@ -3,7 +3,6 @@ package com.tangem.common.ui.navigationButtons.preview import com.tangem.common.ui.R import com.tangem.common.ui.navigationButtons.NavigationButton import com.tangem.common.ui.navigationButtons.NavigationButtonsState -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference internal object NavigationButtonsPreview { @@ -26,16 +25,6 @@ internal object NavigationButtonsPreview { onClick = {}, ) - private val prev = NavigationButton( - textReference = TextReference.EMPTY, - iconRes = R.drawable.ic_back_24, - isSecondary = true, - isIconVisible = true, - shouldShowProgress = false, - isEnabled = true, - onClick = {}, - ) - private val finished = NavigationButton( textReference = resourceReference(R.string.common_close), isSecondary = false, @@ -47,7 +36,6 @@ internal object NavigationButtonsPreview { val allButtons = NavigationButtonsState.Data( primaryButton = finished, - prevButton = prev, extraButtons = extraButtons, txUrl = "https://tangem.com", onTextClick = {}, 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 141773d3c7..4b8551fd88 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 @@ -454,7 +454,7 @@ internal class SendModel @Inject constructor( } private fun initialState(): SendUM = SendUM( - amountUM = AmountState.Empty(isRedesignEnabled = true), + amountUM = AmountState.Empty, destinationUM = SendDestinationInitialStateTransformer( cryptoCurrency = cryptoCurrency, ).transform(DestinationUM.Empty()), diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt index 0dcbee1083..acae2c05d8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/SendAmountComponent.kt @@ -26,12 +26,10 @@ internal class SendAmountComponent( @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() - val isBalanceHidden by params.isBalanceHidingFlow.collectAsStateWithLifecycle() val isSendWithSwapAvailable by model.isSendWithSwapAvailable.collectAsStateWithLifecycle() SendAmountContent( amountState = state, - isBalanceHidden = isBalanceHidden, clickIntents = model, isSendWithSwapAvailable = isSendWithSwapAvailable, modifier = modifier, 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 4d3ac6848c..aa40a2693e 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 @@ -172,7 +172,7 @@ internal class SendAmountModel @Inject constructor( if (uiState.value is AmountState.Empty && userWallet != null) { val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 _uiState.update { - AmountStateConverterV2( + AmountStateConverter( clickIntents = this, appCurrency = appCurrency, cryptoCurrencyStatus = cryptoCurrencyStatus, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/SendAmountContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/SendAmountContent.kt index b72232c3d4..ae0b8597ec 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/SendAmountContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/amount/ui/SendAmountContent.kt @@ -30,7 +30,6 @@ import com.tangem.features.send.v2.subcomponents.amount.ui.preview.SendAmountCli @Composable fun SendAmountContent( amountState: AmountState, - isBalanceHidden: Boolean, clickIntents: SendAmountClickIntents, isSendWithSwapAvailable: Boolean, modifier: Modifier = Modifier, @@ -38,7 +37,6 @@ fun SendAmountContent( Column(modifier = modifier.background(TangemTheme.colors.background.tertiary)) { AmountScreenContent( amountState = amountState, - isBalanceHidden = isBalanceHidden, clickIntents = clickIntents, extraContent = { SendConvertTokenButton( @@ -95,7 +93,6 @@ private fun SendAmountContent_Preview(@PreviewParameter(SendAmountContentPreview TangemThemePreview { SendAmountContent( amountState = params, - isBalanceHidden = true, clickIntents = SendAmountClickIntentsStub, isSendWithSwapAvailable = true, ) diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index 15b4768aaa..ea7e72a8c2 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -201,16 +201,11 @@ class SendConfirmationNotificationsTransformerV2Test { return AmountState.Data( isPrimaryButtonEnabled = true, - isRedesignEnabled = false, title = mockk(relaxed = true), - availableBalance = mockk(relaxed = true), availableBalanceCrypto = mockk(relaxed = true), availableBalanceFiat = mockk(relaxed = true), tokenName = mockk(relaxed = true), tokenIconState = mockk(relaxed = true), - segmentedButtonConfig = persistentListOf(), - selectedButton = 0, - isSegmentedButtonsEnabled = false, amountTextField = AmountFieldModel( value = "1.5", onValueChange = {}, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt index 131d64fb11..cdd95ace06 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/analytics/utils/StakingAnalyticSender.kt @@ -52,7 +52,9 @@ internal class StakingAnalyticSender( source = when (value.currentStep) { StakingStep.InitialInfo -> StakeScreenSource.Info StakingStep.Amount -> StakeScreenSource.Amount - StakingStep.Confirmation -> StakeScreenSource.Confirmation + StakingStep.Success, + StakingStep.Confirmation, + -> StakeScreenSource.Confirmation StakingStep.Validators, StakingStep.RestakeValidator, StakingStep.RewardsValidators, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt index e1e93296cf..92676e87dd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateController.kt @@ -87,7 +87,7 @@ internal class StakingStateController @Inject constructor( cryptoCurrencyBlockchainId = "", currentStep = StakingStep.InitialInfo, initialInfoState = StakingStates.InitialInfoState.Empty(), - amountState = AmountState.Empty(isRedesignEnabled = false), + amountState = AmountState.Empty, validatorState = StakingStates.ValidatorState.Empty(), rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmationState = StakingStates.ConfirmationState.Empty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt index 5ba32182c8..a4d0229f85 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingStateRouter.kt @@ -42,6 +42,7 @@ internal class StakingStateRouter( StakingStep.Amount, -> showConfirmation() StakingStep.Confirmation -> showInitial() + StakingStep.Success -> appRouter.pop() } } @@ -65,6 +66,7 @@ internal class StakingStateRouter( } } StakingStep.Validators -> showConfirmation() + StakingStep.Success -> appRouter.pop() } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt index ae13e50f62..56f8f60341 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt @@ -138,4 +138,5 @@ enum class StakingStep { RestakeValidator, Confirmation, Validators, + Success, } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt index 936fea5c1b..9bb2b5eac3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetAmountDataTransformer.kt @@ -46,10 +46,11 @@ internal class SetAmountDataTransformer( return prevState.copy( amountState = AmountStateConverter( clickIntents = clickIntents, - cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider, - appCurrencyProvider = appCurrencyProvider, iconStateConverter = iconStateConverter, maxEnterAmount = maxEnterAmount, + appCurrency = appCurrencyProvider(), + cryptoCurrencyStatus = cryptoCurrencyStatusProvider(), + isBalanceHidden = false, ).convert( AmountParameters( title = title, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index a5d35a5215..e51cdf8d61 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -25,7 +25,6 @@ internal class SetButtonsStateTransformer( val buttonsState = if (prevState.isButtonsVisible()) { NavigationButtonsState.Data( primaryButton = getPrimaryButton(prevState), - prevButton = getPrevButton(prevState), extraButtons = getExtraButtons(prevState).takeIf { txUrl != null }, txUrl = txUrl, onTextClick = urlOpener::openUrl, @@ -64,18 +63,6 @@ internal class SetButtonsStateTransformer( ) } - private fun getPrevButton(prevState: StakingUiState): NavigationButton? { - return NavigationButton( - textReference = TextReference.EMPTY, - iconRes = R.drawable.ic_back_24, - isSecondary = true, - isIconVisible = true, - shouldShowProgress = false, - isEnabled = true, - onClick = prevState.clickIntents::onPrevClick, - ).takeIf { prevState.currentStep.isPrevButtonVisible() } - } - private fun getExtraButtons(prevState: StakingUiState): Pair { return NavigationButton( textReference = resourceReference(R.string.common_explore), @@ -111,7 +98,7 @@ internal class SetButtonsStateTransformer( resourceReference(R.string.common_stake) } } - + StakingStep.Success -> resourceReference(R.string.common_close) StakingStep.Confirmation -> getConfirmationButtonText() StakingStep.Validators -> resourceReference(R.string.common_continue) StakingStep.Amount, @@ -125,21 +112,17 @@ internal class SetButtonsStateTransformer( val confirmationState = confirmationState as? StakingStates.ConfirmationState.Data val amountState = amountState as? AmountState.Data return if (confirmationState != null && amountState != null) { - if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) { - resourceReference(R.string.common_close) - } else { - when (actionType) { - is StakingActionCommonType.Enter -> { - val amount = amountState.amountTextField.cryptoAmount.value.orZero() - if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { - resourceReference(R.string.give_permission_title) - } else { - resourceReference(R.string.common_stake) - } + when (actionType) { + is StakingActionCommonType.Enter -> { + val amount = amountState.amountTextField.cryptoAmount.value.orZero() + if (confirmationState.isApprovalNeeded && confirmationState.allowance < amount) { + resourceReference(R.string.give_permission_title) + } else { + resourceReference(R.string.common_stake) } - is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) - is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() } + is StakingActionCommonType.Exit -> resourceReference(R.string.common_unstake) + is StakingActionCommonType.Pending -> confirmationState.pendingAction?.type.getPendingActionTitle() } } else { resourceReference(R.string.common_close) @@ -155,6 +138,7 @@ internal class SetButtonsStateTransformer( StakingStep.Amount -> clickIntents.onAmountEnterClick() StakingStep.Confirmation -> onConfirmationClick() StakingStep.RewardsValidators -> Unit + StakingStep.Success -> clickIntents.onBackClick() } } @@ -178,17 +162,6 @@ internal class SetButtonsStateTransformer( } } - private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) { - StakingStep.InitialInfo, - StakingStep.RewardsValidators, - StakingStep.RestakeValidator, - StakingStep.Confirmation, - StakingStep.Validators, - -> false - StakingStep.Amount, - -> true - } - private fun StakingUiState.isPrimaryButtonDisabled(): Boolean { val initialState = initialInfoState as? StakingStates.InitialInfoState.Data val hasNotStaking = initialState?.yieldBalance == InnerYieldBalanceState.Empty @@ -205,6 +178,7 @@ internal class SetButtonsStateTransformer( StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled StakingStep.RestakeValidator, StakingStep.Validators, + StakingStep.Success, -> true } } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt index 7eb0589c34..6f745765ff 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetConfirmationStateCompletedTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingStates +import com.tangem.features.staking.impl.presentation.state.StakingStep import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.TransactionDoneState import com.tangem.utils.transformer.Transformer @@ -17,6 +18,7 @@ internal class SetConfirmationStateCompletedTransformer( override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( confirmationState = prevState.confirmationState.copyWrapped(), + currentStep = StakingStep.Success, ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index d26cfd7e24..f36b37e184 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -229,10 +229,11 @@ internal class SetInitialDataStateTransformer( ) return AmountStateConverter( clickIntents = clickIntents, - cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, - appCurrencyProvider = appCurrencyProvider, + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrency = appCurrencyProvider(), iconStateConverter = iconStateConverter, maxEnterAmount = maxEnterAmount, + isBalanceHidden = false, ).convert( AmountParameters( title = stringReference(userWalletProvider().name), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt index d16bb669e4..12b26f1cfc 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetTitleTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.state.transformers +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.isNullOrEmpty import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -28,7 +29,7 @@ internal object SetTitleTransformer : Transformer { R.string.staking_title_stake, wrappedList(prevState.cryptoCurrencyName), ) - + StakingStep.Success -> TextReference.EMPTY StakingStep.Confirmation -> { when (actionType) { is StakingActionCommonType.Enter -> resourceReference( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt index 42f7777f9f..7f3853dbbd 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingConfirmationContent.kt @@ -14,12 +14,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData -import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.common.ui.amountScreen.ui.AmountBlockV2 import com.tangem.core.ui.components.transactions.TransactionDoneTitle 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.staking.impl.R +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState import com.tangem.features.staking.impl.presentation.state.StakingNotification import com.tangem.features.staking.impl.presentation.state.StakingStates @@ -30,7 +31,6 @@ import com.tangem.features.staking.impl.presentation.state.stub.StakingClickInte import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock -import com.tangem.features.staking.impl.presentation.model.StakingClickIntents @Suppress("LongParameterList") @Composable @@ -60,7 +60,7 @@ internal fun StakingConfirmationContent( subtitle = resourceReference(R.string.staking_transaction_in_progress_text), ) } - AmountBlock( + AmountBlockV2( amountState = amountState, isClickDisabled = !state.isAmountEditable || isTransactionSent || isTransactionInProgress, isEditingDisabled = !state.isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt index 5170756b32..3e5d834e16 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingScreen.kt @@ -91,6 +91,7 @@ private fun StakingAppBar(uiState: StakingUiState) { val (backIcon, click) = when (uiState.currentStep) { StakingStep.Amount, StakingStep.Confirmation, + StakingStep.Success, -> R.drawable.ic_close_24 to uiState.clickIntents::onBackClick StakingStep.Validators, StakingStep.RewardsValidators, @@ -108,6 +109,7 @@ private fun StakingAppBar(uiState: StakingUiState) { ) } +@Suppress("LongMethod") @Composable private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = Modifier) { val currentScreen = uiState.currentStep @@ -136,10 +138,10 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M contentAlignment = Alignment.TopCenter, label = "Staking Screen Navigation", transitionSpec = { - val direction = if (initialState.ordinal < targetState.ordinal) { - AnimatedContentTransitionScope.SlideDirection.Start - } else { - AnimatedContentTransitionScope.SlideDirection.End + val direction = when { + targetState == StakingStep.Success -> AnimatedContentTransitionScope.SlideDirection.Up + initialState.ordinal < targetState.ordinal -> AnimatedContentTransitionScope.SlideDirection.Start + else -> AnimatedContentTransitionScope.SlideDirection.End } slideIntoContainer(towards = direction, animationSpec = tween()) @@ -163,7 +165,6 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M } StakingStep.Amount -> AmountScreenContent( amountState = uiState.amountState, - isBalanceHidden = uiState.isBalanceHidden, clickIntents = uiState.clickIntents, modifier = Modifier.background(TangemTheme.colors.background.secondary), ) @@ -173,6 +174,12 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M validatorState = uiState.validatorState, clickIntents = uiState.clickIntents, ) + StakingStep.Success -> StakingSuccessContent( + amountState = uiState.amountState, + state = uiState.confirmationState, + validatorState = uiState.validatorState, + clickIntents = uiState.clickIntents, + ) StakingStep.RestakeValidator, StakingStep.Validators, -> StakingValidatorListContent( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingSuccessContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingSuccessContent.kt new file mode 100644 index 0000000000..d9297f768d --- /dev/null +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingSuccessContent.kt @@ -0,0 +1,86 @@ +package com.tangem.features.staking.impl.presentation.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.common.ui.amountScreen.models.AmountState +import com.tangem.common.ui.amountScreen.preview.AmountStatePreviewData +import com.tangem.common.ui.amountScreen.ui.AmountBlock +import com.tangem.core.ui.components.transactions.TransactionDoneTitle +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.staking.impl.R +import com.tangem.features.staking.impl.presentation.model.StakingClickIntents +import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState +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.previewdata.ConfirmationStatePreviewData +import com.tangem.features.staking.impl.presentation.state.previewdata.ValidatorStatePreviewData +import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub +import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock +import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock +import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock + +@Suppress("LongParameterList") +@Composable +internal fun StakingSuccessContent( + amountState: AmountState, + state: StakingStates.ConfirmationState, + validatorState: StakingStates.ValidatorState, + clickIntents: StakingClickIntents, +) { + if (state !is StakingStates.ConfirmationState.Data) return + val isTransactionSent = state.innerState == InnerConfirmationStakingState.COMPLETED + val isTransactionInProgress = state.notifications.any { it is StakingNotification.Warning.TransactionInProgress } + Column( + modifier = Modifier + .background(TangemTheme.colors.background.secondary) + .padding(horizontal = TangemTheme.dimens.spacing16) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + TransactionDoneTitle( + title = resourceReference(R.string.common_in_progress), + subtitle = resourceReference(R.string.staking_transaction_in_progress_text), + ) + AmountBlock( + amountState = amountState, + isClickDisabled = !state.isAmountEditable || isTransactionSent || isTransactionInProgress, + isEditingDisabled = !state.isAmountEditable && state.innerState != InnerConfirmationStakingState.COMPLETED, + onClick = clickIntents::onPrevClick, + ) + ValidatorBlock( + validatorState = validatorState, + isClickable = !isTransactionInProgress, + onClick = clickIntents::openValidators, + ) + StakingFeeBlock(feeState = state.feeState) + NotificationsBlock(notifications = state.notifications) + Spacer(Modifier) + } +} + +@Preview(widthDp = 360, showBackground = true) +@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_StakingConfirmationContent() { + TangemThemePreview { + Column(Modifier.background(TangemTheme.colors.background.primary)) { + StakingConfirmationContent( + amountState = AmountStatePreviewData.amountState, + state = ConfirmationStatePreviewData.assentStakingState, + validatorState = ValidatorStatePreviewData.validatorState, + clickIntents = StakingClickIntentsStub, + ) + } + } +} \ No newline at end of file diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt index b2d95fed03..62eb8a0d3b 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/entity/SwapAmountUM.kt @@ -64,19 +64,13 @@ sealed class SwapAmountFieldUM { data class Empty( override val amountType: SwapAmountType, ) : SwapAmountFieldUM() { - override val amountField: AmountState = AmountState.Empty( - isPrimaryButtonEnabled = false, - isRedesignEnabled = true, - ) + override val amountField: AmountState = AmountState.Empty } data class Loading( override val amountType: SwapAmountType, ) : SwapAmountFieldUM() { - override val amountField: AmountState = AmountState.Empty( - isPrimaryButtonEnabled = false, - isRedesignEnabled = true, - ) + override val amountField: AmountState = AmountState.Empty } data class Content( diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt index c8eb091285..a490422d2f 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/converter/SwapAmountFieldConverter.kt @@ -1,7 +1,7 @@ package com.tangem.features.swap.v2.impl.amount.model.converter import com.tangem.common.ui.amountScreen.AmountScreenClickIntents -import com.tangem.common.ui.amountScreen.converters.AmountStateConverterV2 +import com.tangem.common.ui.amountScreen.converters.AmountStateConverter import com.tangem.common.ui.amountScreen.converters.MaxEnterAmountConverter import com.tangem.common.ui.amountScreen.models.AmountParameters import com.tangem.core.ui.components.atoms.text.TextEllipsis @@ -44,7 +44,7 @@ internal class SwapAmountFieldConverter( subtitleEllipsisRight = TextEllipsis.OffsetEnd(appCurrency.symbol.length), priceImpact = null, isClickEnabled = selectedType.isViewingField(), - amountField = AmountStateConverterV2( + amountField = AmountStateConverter( clickIntents = clickIntents, appCurrency = appCurrency, cryptoCurrencyStatus = cryptoCurrencyStatus, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt index 52fc0217cd..b6b0ccec59 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountBalanceHiddenTransformer.kt @@ -46,9 +46,7 @@ internal class SwapAmountBalanceHiddenTransformer( val newData = recalculatedPrimary.amountField newData.copy( amountTextField = oldData.amountTextField, - selectedButton = oldData.selectedButton, isPrimaryButtonEnabled = oldData.isPrimaryButtonEnabled, - isSegmentedButtonsEnabled = oldData.isSegmentedButtonsEnabled, isEditingDisabled = oldData.isEditingDisabled, reduceAmountBy = oldData.reduceAmountBy, isIgnoreReduce = oldData.isIgnoreReduce, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt index bc921fd483..54177a3435 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/SwapAmountBlockContent.kt @@ -127,7 +127,6 @@ private fun ConstraintLayoutScope.SwapAmountBlock( AmountBlockV2( amountState = (amountUM.secondaryAmount.amountField as? AmountState.Data)?.copy( title = resourceReference(R.string.send_with_swap_recipient_amount_title), - availableBalance = TextReference.EMPTY, availableBalanceCrypto = TextReference.EMPTY, ) ?: amountUM.secondaryAmount.amountField, isClickDisabled = true, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index 3e6da5d36b..352cd998fa 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -97,9 +97,7 @@ internal data object SwapAmountContentPreview { val defaultState = SwapAmountUM.Content( primaryAmount = SwapAmountFieldUM.Content( amountType = SwapAmountType.From, - amountField = AmountStatePreviewData.amountState.copy( - availableBalance = stringReference("Balance: 100 BTC"), - ), + amountField = AmountStatePreviewData.amountState, title = stringReference("Tether"), subtitleLeft = stringReference("11 101,123123456 BTC"), subtitleRight = stringReference(" ${StringsSigns.DOT} 1 212,12 $"), @@ -112,7 +110,6 @@ internal data object SwapAmountContentPreview { amountType = SwapAmountType.To, amountField = AmountStatePreviewData.amountState.copy( title = stringReference("Amount to receive"), - availableBalance = TextReference.EMPTY, ), title = stringReference("Shiba Inu"), priceImpact = stringReference("(-10%)"), From 269a6d1d57aceb5b9c4fb34f8425638eeaf6915c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Oct 2025 12:50:16 +0500 Subject: [PATCH 19/22] Updated on 2026-08-14 --- .../tangem/common/ui/account/AccountLabel.kt | 53 +++++++++++++++++ core/res/src/main/res/values-de/strings.xml | 5 ++ core/res/src/main/res/values-es/strings.xml | 5 ++ core/res/src/main/res/values-fr/strings.xml | 7 ++- core/res/src/main/res/values-ja/strings.xml | 16 ++++++ core/res/src/main/res/values-ru/strings.xml | 1 + .../src/main/res/values-uk-rUA/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 8 +++ .../ui/components/tokenlist/TokenListItem.kt | 4 +- .../tokenlist/state/TokensListItemUM.kt | 7 ++- .../LoadingAccountTokenItemConverter.kt | 2 +- .../SetNoAvailablePairsTransformerV2.kt | 2 +- .../onramp/swap/entity/ExchangeCardUM.kt | 26 +++++++-- .../transformer/SelectFromTokenTransformer.kt | 6 ++ .../transformer/SelectToTokenTransformer.kt | 10 +++- .../swap/entity/utils/ExchangeCardUMExt.kt | 23 +++++++- .../swap/model/SwapSelectTokensModel.kt | 32 ++++++++++- .../features/onramp/swap/ui/ExchangeCard.kt | 57 ++++++++++++++----- .../UpdateAccountTokenItemConverter.kt | 2 +- .../common/preview/WalletScreenPreviewData.kt | 9 +-- .../converter/TokenListStateConverter.kt | 4 +- 21 files changed, 240 insertions(+), 40 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt new file mode 100644 index 0000000000..51f13b848f --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountLabel.kt @@ -0,0 +1,53 @@ +package com.tangem.common.ui.account + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Displays account name with icon + * + * @param name account name + * @param icon portfolio account icon model + * @param iconSize portfolio account icon size + * @param nameStyle account name style + * @param nameColor account name color + * @see AccountIcon + */ +@Composable +fun AccountLabel( + name: TextReference, + icon: CryptoPortfolioIconUM, + iconSize: AccountIconSize, + modifier: Modifier = Modifier, + nameStyle: TextStyle = TangemTheme.typography.subtitle2, + nameColor: Color = TangemTheme.colors.text.tertiary, +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + AccountIcon( + name = name, + icon = icon, + size = iconSize, + ) + Text( + text = name.resolveReference(), + style = nameStyle, + color = nameColor, + maxLines = 1, + ) + } +} \ No newline at end of file diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 36849c64d9..2817667d7e 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -320,6 +320,7 @@ Tangem Wallet Allgemeine Geschäftsbedingungen Nutzungsbedingungen + An Heute %d Token @@ -1304,8 +1305,12 @@ Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten. Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen. Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten + Echtzeit-Warnungen für Transaktionen, Umtausch und wichtige Aktualisierungen. + Transaktionswarnungen Genehmigung für Transaktionen anfordern Sei der Erste, der von neuen Aktionen erfährt + Frühzeitiger Zugriff auf neue Funktionen und exklusive Angebote. + Updates zu Funktionen und Neuigkeiten Möchtest du Push-Benachrichtigungen verwenden? Neues Wallet hinzufügen Möchtest Du diese Wallet wirklich entfernen? diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index 36b3355615..af4f17bfca 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -258,6 +258,7 @@ Intercambiar términos y condiciones Condiciones de uso + A Hoy %d token @@ -1204,8 +1205,12 @@ Use %s o escanee una tarjeta/anillo para tener acceso a su billetera Error de conexión: Esta dApp utiliza la versión 1.0 de Wallet Connect, que no es compatible. Asegúrese de que la dApp sea compatible con la versión 2.0 de Wallet Connect para conectarse correctamente. Manténgase actualizado con las últimas funciones y noticias + Alertas en tiempo real de transacciones, intercambios y actualizaciones críticas. + Alertas de transacciones Recibir notificaciones de las transacciones entrantes Sea el primero en enterarte de nuevas promociones + Acceso anticipado a nuevas funciones y ofertas exclusivas. + Actualizaciones de características y noticias ¿Quiere utilizar\nnotificaciones push? Agregar una nueva billetera ¿Estás seguro de que deseas olvidar esta billetera? diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 47bac750bc..503724daf0 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -235,6 +235,7 @@ Échanger termes et conditions Conditions d\'utilisation + À Aujourd\'hui %d jeton @@ -1106,7 +1107,7 @@ Échanger ce montant de jetons sélectionnés aura un impact significatif sur les prix et réduira votre résultat. Fonds insuffisants Donner l\'autorisation - Échangez + Échanger Vous recevez Choisir le jeton non disponible @@ -1177,8 +1178,12 @@ 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 + Alertes en temps réel pour les transactions, les échanges et les mises à jour importantes. + Alertes de transaction Recevez des notifications des transactions entrantes Soyez le premier informé des nouvelles promotions + Accès anticipé à de nouvelles fonctionnalités et à des offres exclusives. + Actualités et mises à jour Souhaitez-vous utiliser les\nnotifications push? Ajouter un nouveau portefeuille Êtes-vous sûr de vouloir supprimer ce portefeuille ? diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 48e34783ae..d76ede5115 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -33,6 +33,7 @@ アカウントを追加 保存 アカウント名 + アカウント名はすでに存在しています アカウント 新しいアカウント アカウントを追加 @@ -323,6 +324,7 @@ Tangem 利用規約 利用規約 + 宛先 今日 %d トークン @@ -885,6 +887,7 @@ %s分 以下が手に入ります。 + サービスは外部プロバイダーによって提供されます。 \nTangemは責任を負いません。 この画面を閉じて、トークンの詳細画面で取引状況を確認できます。 最大 経由 @@ -1094,6 +1097,7 @@ ステーキングアカウントとは、ステーキングされたSOLが保管される特別なアカウントです。トークンをバリデーターに委任し、取引の検証に参加して報酬を受け取る際に作成されます。ステーキングアカウントの作成には少額の手数料がかかりますが、ステーキング完了後に返金されます。 年率 ステーキングに参加することで得られる年間収益率。 + APYは、バリデータのパフォーマンスに基づいた年間利回りを示します。 APR APY 報酬は毎日自動的にステーキング残高に蓄積されます。 @@ -1254,6 +1258,10 @@ 受け取る トークンを選択 利用不可 + データの読み込みに失敗しました。しばらくしてからもう一度お試しください。 + 非表示 + 表示 + カードの詳細 アカウントを作成してカードを発行 Tangem Pay ステータスを表示 @@ -1266,6 +1274,7 @@ カードの詳細は保護されており、アプリ内で完全に制御できます。 内蔵セキュリティ 無料のCryptoカード\nを数分で入手しましょう + Tangem Pay これは私のウォレットです 残高非表示 残高表示 @@ -1293,6 +1302,7 @@ %%image%% %1$sネットワーク上のトークン %1$s ( %2$s ) トークンは%3$sネットワークの主要通貨であり、このネットワーク上の他のトークンがリストにある限り、非表示にすることはできません。 %sを非表示にできません + QRコードを表示 このトークンは、2月%2$s-%3$s の間、%1$s のサービス手数料で別のトークンと交換できます。 Changellyでスワップ、手数料%s 今すぐスワップ @@ -1661,6 +1671,7 @@ 承認を確定する 資産残高に関するテキスト [プレースホルダー] あなたの%sはAaveに預けられています + チャートを読み込めません・・ 受け取った金額%1$s %2$sはAaveに入金されませんでした。 %1$s%% を獲得 利用可能 @@ -1678,6 +1689,7 @@ 最大手数料 手数料ポリシー ネットワーク手数料が現在高すぎます。設定した上限を下回るまで待機しています。 + 過去のリターン ここに説明を入力してください。1〜3行が理想的です。[プレースホルダー] トークン承認が必要 ネットワーク接続を確認してください @@ -1708,6 +1720,7 @@ 収益を停止する オフにすると、Aaveから資金が引き出され、ウォレットの%sに戻され、報酬の獲得が停止されます。 出金金額からネットワーク手数料が差し引かれます。 + 供給APR APY あなたの資産を眠らせない — 残高を運用して利息を得ましょう。 保有資産から報酬を得る @@ -1716,4 +1729,7 @@ 自動 取引のネットワーク手数料をカバーするために、 %1$s %2$sを入金してください %s手数料を支払えません + 利息サービスは現在利用できません。しばらくしてから再度お試しください。 + 収益は利用できません + チャートを読み込めません・・ diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 88c15e79d0..d3729cd4c9 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -250,6 +250,7 @@ Обменять условия участия Условиями использования + На Сегодня %d токен 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 6946a0d572..e832ab378e 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -233,6 +233,7 @@ Обмін умови участі Умовами використання + До Сьогодні %d токен diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index db57bd8a71..9987d7f342 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -179,6 +179,7 @@ Accept Access denied Accounts + Activate Add Add to portfolio Add token @@ -253,6 +254,7 @@ Speed and fee Finish Free + From Synchronize addresses Get started Go to provider @@ -331,6 +333,7 @@ Tangem Wallet terms and conditions Terms of Use + To Today %d token @@ -1112,6 +1115,9 @@ This will remove the wallet from the application. The wallet itself can be added again. Name Put your token to work + A network fee is a small payment to process and confirm your transaction on the blockchain. + To start staking, your TON account must be activated with a self-transaction of 1 TON. The funds stay in your wallet — this step only enables your account for staking. + Account activation The amount to stake must be at least %s Staking amount will be rounded to %1$s TRX due to network rules. Unstaking amount will be rounded to %1$s TRX due to network rules. @@ -1178,6 +1184,8 @@ Reinvests your earned rewards in your staked amount, increasing potential earnings. 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, your TON account must be activated with a self-transaction of 1 TON. The funds stay in your wallet — this step only enables your account for staking. + Account activation To start staking in TON, first send a small transaction to your own address — this will activate your wallet. Up to 0.2 TON may be required in addition to the network fee to complete the transaction. Any unused amount will be refunded. 0.2 TON is required to proceed with this operation, in addition to the network fee. Please top up your balance. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 989637b8eb..fa775ca36c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -60,10 +60,10 @@ fun TokenListItem(state: TokensListItemUM, isBalanceHidden: Boolean, modifier: M @Composable fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { if (state.isExpanded) { - ExpandedPortfolioHeader(state = state.state, isCollapsable = state.isCollapsable, modifier = modifier) + ExpandedPortfolioHeader(state = state.tokenItemUM, isCollapsable = state.isCollapsable, modifier = modifier) } else { TokenItem( - state = state.state, + state = state.tokenItemUM, isBalanceHidden = isBalanceHidden, modifier = modifier, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt index 23b2ef9da6..7797159dc1 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference +import kotlinx.collections.immutable.ImmutableList /** Tokens list item state */ @Immutable @@ -41,12 +42,12 @@ sealed interface TokensListItemUM { } data class Portfolio( - val state: TokenItemState, + val tokenItemUM: TokenItemState, val isExpanded: Boolean, val isCollapsable: Boolean, - val tokens: List, + val tokens: ImmutableList, ) : TokensListItemUM { - override val id: String = state.id + override val id: String = tokenItemUM.id } data class Text(override val id: Any, val text: TextReference) : TokensListItemUM diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt index 8d9c2042a9..9489858928 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/converters/LoadingAccountTokenItemConverter.kt @@ -16,7 +16,7 @@ internal class LoadingAccountTokenItemConverter( val (account, currencies) = value return TokensListItemUM.Portfolio( - state = AccountCryptoPortfolioItemStateConverter( + tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = account, onItemClick = null, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt index 669604724c..9ee61d4ba9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt @@ -33,7 +33,7 @@ internal class SetNoAvailablePairsTransformerV2( TokenListUMData.AccountList( tokensList = accountList.map { (account, cryptoCurrencies) -> TokensListItemUM.Portfolio( - state = AccountCryptoPortfolioItemStateConverter( + tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = account, onItemClick = null, diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt index 817e7218ba..0b159c1423 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/ExchangeCardUM.kt @@ -1,5 +1,7 @@ package com.tangem.features.onramp.swap.entity +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.CryptoPortfolioIconUM import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference @@ -11,7 +13,7 @@ import com.tangem.core.ui.extensions.TextReference internal sealed interface ExchangeCardUM { /** Title reference */ - val titleReference: TextReference + val titleUM: TitleUM /** Remove button UI model */ val removeButtonUM: RemoveButtonUM? @@ -19,11 +21,11 @@ internal sealed interface ExchangeCardUM { /** * Empty state * - * @property titleReference title reference + * @property titleUM title reference * @property subtitleReference empty token subtitle reference */ data class Empty( - override val titleReference: TextReference, + override val titleUM: TitleUM, val subtitleReference: TextReference, ) : ExchangeCardUM { @@ -33,15 +35,29 @@ internal sealed interface ExchangeCardUM { /** * Filled * - * @property titleReference title reference + * @property titleUM title reference * @property removeButtonUM remove button UI model * @property tokenItemState token item state */ data class Filled( - override val titleReference: TextReference, + override val titleUM: TitleUM, override val removeButtonUM: RemoveButtonUM?, val tokenItemState: TokenItemState, ) : ExchangeCardUM data class RemoveButtonUM(val onClick: () -> Unit) + + @Immutable + sealed interface TitleUM { + + data class Text( + val title: TextReference, + ) : TitleUM + + data class Account( + val prefixText: TextReference, + val name: TextReference, + val icon: CryptoPortfolioIconUM, + ) : TitleUM + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt index 08c088f59e..036c81e855 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectFromTokenTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.swap.entity.transformer import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.domain.models.account.Account import com.tangem.features.onramp.swap.entity.ExchangeCardUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer @@ -17,6 +18,8 @@ import com.tangem.features.onramp.swap.entity.utils.toFilled internal class SelectFromTokenTransformer( private val selectedTokenItemState: TokenItemState, private val onRemoveClick: () -> Unit, + private val account: Account.CryptoPortfolio, + private val isAccountsMode: Boolean, ) : SwapSelectTokensUMTransformer { override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { @@ -24,6 +27,9 @@ internal class SelectFromTokenTransformer( exchangeFrom = prevState.exchangeFrom.toFilled( selectedTokenItemState = selectedTokenItemState, removeButtonUM = ExchangeCardUM.RemoveButtonUM(onClick = onRemoveClick), + account = account, + isAccountsMode = isAccountsMode, + isFromCurrency = true, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt index 433b28f5ff..f8798e8f75 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/transformer/SelectToTokenTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.features.onramp.swap.entity.transformer import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.domain.models.account.Account import com.tangem.features.onramp.swap.entity.ExchangeCardUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUM import com.tangem.features.onramp.swap.entity.SwapSelectTokensUMTransformer @@ -15,12 +16,19 @@ import com.tangem.features.onramp.swap.entity.utils.toFilled */ internal class SelectToTokenTransformer( private val selectedTokenItemState: TokenItemState, + private val isAccountsMode: Boolean, + private val account: Account.CryptoPortfolio, ) : SwapSelectTokensUMTransformer { override fun transform(prevState: SwapSelectTokensUM): SwapSelectTokensUM { return prevState.copy( exchangeFrom = prevState.exchangeFrom.hideRemoveButton(), - exchangeTo = prevState.exchangeTo.toFilled(selectedTokenItemState = selectedTokenItemState), + exchangeTo = prevState.exchangeTo.toFilled( + selectedTokenItemState = selectedTokenItemState, + isAccountsMode = isAccountsMode, + account = account, + isFromCurrency = false, + ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt index 6daf403437..04615a8b92 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/entity/utils/ExchangeCardUMExt.kt @@ -1,14 +1,16 @@ package com.tangem.features.onramp.swap.entity.utils +import com.tangem.common.ui.account.toUM import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.Account import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.entity.ExchangeCardUM /** Create empty exchange "from" card */ internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty { return ExchangeCardUM.Empty( - titleReference = resourceReference(id = R.string.swapping_from_title), + titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)), subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap), ) } @@ -16,7 +18,7 @@ internal fun createEmptyExchangeFrom(): ExchangeCardUM.Empty { /** Create empty exchange "to" card */ internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty { return ExchangeCardUM.Empty( - titleReference = resourceReference(id = R.string.swapping_to_title), + titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_to_title)), subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_receive), ) } @@ -29,10 +31,25 @@ internal fun createEmptyExchangeTo(): ExchangeCardUM.Empty { */ internal fun ExchangeCardUM.toFilled( selectedTokenItemState: TokenItemState, + account: Account.CryptoPortfolio, + isAccountsMode: Boolean, + isFromCurrency: Boolean, removeButtonUM: ExchangeCardUM.RemoveButtonUM? = null, ): ExchangeCardUM.Filled { return ExchangeCardUM.Filled( - titleReference = titleReference, + titleUM = if (isAccountsMode) { + ExchangeCardUM.TitleUM.Account( + prefixText = if (isFromCurrency) { + resourceReference(R.string.common_from) + } else { + resourceReference(R.string.common_to) + }, + name = account.accountName.toUM().value, + icon = account.icon.toUM(), + ) + } else { + titleUM + }, tokenItemState = selectedTokenItemState, removeButtonUM = removeButtonUM, ) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt index 5472e0e61c..e0e2d8e657 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/model/SwapSelectTokensModel.kt @@ -8,7 +8,9 @@ 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.token.state.TokenItemState +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.onramp.swap.entity.SwapSelectTokensController @@ -32,6 +34,7 @@ internal class SwapSelectTokensModel @Inject constructor( private val router: Router, private val analyticsEventHandler: AnalyticsEventHandler, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, ) : Model() { val state: StateFlow = controller.state @@ -43,9 +46,12 @@ internal class SwapSelectTokensModel @Inject constructor( private val params = paramsContainer.require() + private var isAccountsMode: Boolean = false + init { controller.update { it.copy(onBackClick = ::onBackClick) } + subscribeOnAccountsMode() subscribeOnBalanceHidingSettings() } @@ -66,6 +72,11 @@ internal class SwapSelectTokensModel @Inject constructor( transformer = SelectFromTokenTransformer( selectedTokenItemState = selectedTokenItemState, onRemoveClick = ::onRemoveFromTokenClick, + isAccountsMode = isAccountsMode, + account = Account.CryptoPortfolio.createMainAccount( + userWalletId = params.userWalletId, + cryptoCurrencies = setOf(status.currency), + ), // todo account from from cryptocurrency ), ) } @@ -84,7 +95,16 @@ internal class SwapSelectTokensModel @Inject constructor( modelScope.launch { _toCurrencyStatus.value = status - controller.update(transformer = SelectToTokenTransformer(selectedTokenItemState)) + controller.update( + transformer = SelectToTokenTransformer( + selectedTokenItemState = selectedTokenItemState, + isAccountsMode = isAccountsMode, + account = Account.CryptoPortfolio.createMainAccount( + userWalletId = params.userWalletId, + cryptoCurrencies = setOf(status.currency), + ), // todo account from from cryptocurrency + ), + ) // require some delay to show state with selected "from" and "to" tokens delay(timeMillis = 500) @@ -119,6 +139,16 @@ internal class SwapSelectTokensModel @Inject constructor( .launchIn(modelScope) } + private fun subscribeOnAccountsMode() { + isAccountsModeEnabledUseCase() + .distinctUntilChanged() + .onEach { + isAccountsMode = it + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + private fun onBackClick() { analyticsEventHandler.send( event = MainScreenAnalyticsEvent.ButtonClose(source = AnalyticsParam.ScreensSources.Swap), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt index da3b5ead84..4ee9b3eb0e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/ui/ExchangeCard.kt @@ -14,11 +14,14 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountLabel +import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.rows.NetworkTitle @@ -46,13 +49,14 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif modifier = modifier .fillMaxWidth() .heightIn(min = 116.dp) - .background( - color = TangemTheme.colors.background.primary, - shape = TangemTheme.shapes.roundedCornersXMedium, - ), + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.primary), verticalArrangement = Arrangement.SpaceBetween, ) { - Title(titleReference = state.titleReference, removeButtonUM = state.removeButtonUM) + Title( + titleUM = state.titleUM, + removeButtonUM = state.removeButtonUM, + ) AnimatedContent( targetState = state, @@ -73,16 +77,39 @@ internal fun ExchangeCard(state: ExchangeCardUM, isBalanceHidden: Boolean, modif } @Composable -private fun Title(titleReference: TextReference, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) { +private fun Title(titleUM: ExchangeCardUM.TitleUM, removeButtonUM: ExchangeCardUM.RemoveButtonUM?) { NetworkTitle( title = { - Text( - text = titleReference.resolveReference(), - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography.subtitle2, - ) + AnimatedContent( + titleUM, + ) { currentState -> + when (currentState) { + is ExchangeCardUM.TitleUM.Account -> Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = currentState.prefixText.resolveReference(), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + AccountLabel( + name = currentState.name, + icon = currentState.icon, + iconSize = AccountIconSize.ExtraSmall, + nameStyle = TangemTheme.typography.subtitle2, + nameColor = TangemTheme.colors.text.tertiary, + ) + } + is ExchangeCardUM.TitleUM.Text -> Text( + text = currentState.title.resolveReference(), + color = TangemTheme.colors.text.tertiary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = TangemTheme.typography.subtitle2, + ) + } + } }, action = { RemoveButton(state = removeButtonUM) }, ) @@ -153,7 +180,7 @@ private class ExchangeCardUMProvider : PreviewParameterProvider override val values: Sequence = sequenceOf( ExchangeCardUM.Empty( - titleReference = resourceReference(id = R.string.swapping_from_title), + titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)), subtitleReference = resourceReference(id = R.string.action_buttons_you_want_to_swap), ), createFilled(removeButtonUM = null), @@ -162,7 +189,7 @@ private class ExchangeCardUMProvider : PreviewParameterProvider private fun createFilled(removeButtonUM: ExchangeCardUM.RemoveButtonUM?): ExchangeCardUM.Filled { return ExchangeCardUM.Filled( - titleReference = resourceReference(id = R.string.swapping_from_title), + titleUM = ExchangeCardUM.TitleUM.Text(resourceReference(id = R.string.swapping_from_title)), removeButtonUM = removeButtonUM, tokenItemState = TokenItemState.Content( id = "1", diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt index 2389d1aec9..f90f1e9794 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/UpdateAccountTokenItemConverter.kt @@ -26,7 +26,7 @@ internal class UpdateAccountTokenItemConverter( override fun convert(value: AccountAvailabilityUM): TokensListItemUM.Portfolio { return TokensListItemUM.Portfolio( - state = AccountCryptoPortfolioItemStateConverter( + tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = value.account, onItemClick = null, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt index 37319208bd..50569e441a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewData.kt @@ -19,6 +19,7 @@ import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarCon import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList internal object WalletScreenPreviewData { private val tokenItemState = TokenItemState.Content( @@ -86,17 +87,17 @@ internal object WalletScreenPreviewData { private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent( items = persistentListOf( TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance(), + tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), isExpanded = false, isCollapsable = true, - state = AccountItemPreviewData.accountItem + tokenItemUM = AccountItemPreviewData.accountItem .copy(iconState = AccountItemPreviewData.accountLetterIcon), ), TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance(), + tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), isExpanded = true, isCollapsable = true, - state = AccountItemPreviewData.accountItem, + tokenItemUM = AccountItemPreviewData.accountItem, ), ), organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 07afba9a94..8cbaeee5da 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -112,10 +112,10 @@ internal class TokenListStateConverter( is WalletTokensListState.Empty -> listOf() } return TokensListItemUM.Portfolio( - state = accountItem, + tokenItemUM = accountItem, isExpanded = isExtend, isCollapsable = true, - tokens = items.filterIsInstance(), + tokens = items.filterIsInstance().toPersistentList(), ) } From 161477c16a489db2aa5d483366a831f67bf909c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Oct 2025 12:41:59 +0400 Subject: [PATCH 20/22] Updated on 2026-08-14 --- .../tap/data/DefaultTangemPayStorage.kt | 6 +- .../datasource/local/visa/TangemPayStorage.kt | 4 +- .../repository/DefaultOnboardingRepository.kt | 67 +++++++++++++------ .../tangem/domain/pay/model/CustomerInfo.kt | 6 +- .../tangem/domain/pay/model/OrderStatus.kt | 2 +- .../model/TangemPayOnboardingModel.kt | 2 +- .../wallet/child/wallet/model/WalletModel.kt | 10 +-- ...kt => TangemPayInitialStateTransformer.kt} | 47 +++---------- ...TangemPayIssueAvailableStateTransformer.kt | 12 ++++ .../TangemPayIssueProgressStateTransformer.kt | 10 +++ .../state/util/TangemPayStateCreator.kt | 31 +++++++++ 11 files changed, 125 insertions(+), 72 deletions(-) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/{TangemPayStateTransformer.kt => TangemPayInitialStateTransformer.kt} (50%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueAvailableStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueProgressStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt diff --git a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt index 58e979c7dc..ce88071554 100644 --- a/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt +++ b/app/src/main/java/com/tangem/tap/data/DefaultTangemPayStorage.kt @@ -67,7 +67,11 @@ internal class DefaultTangemPayStorage @Inject constructor( secureStorage.get(createOrderIdKey(customerWalletAddress))?.decodeToString(throwOnInvalidSequence = true) } - override suspend fun clear(customerWalletAddress: String) = withContext(dispatcherProvider.io) { + override suspend fun clearOrderId(customerWalletAddress: String) = withContext(dispatcherProvider.io) { + secureStorage.delete(createOrderIdKey(customerWalletAddress)) + } + + override suspend fun clearAll(customerWalletAddress: String) = withContext(dispatcherProvider.io) { secureStorage.delete(createKey(customerWalletAddress)) secureStorage.delete(createOrderIdKey(customerWalletAddress)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt index 1ed58f2220..bf8bf7fa78 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/TangemPayStorage.kt @@ -12,5 +12,7 @@ interface TangemPayStorage { suspend fun getOrderId(customerWalletAddress: String): String? - suspend fun clear(customerWalletAddress: String) + suspend fun clearOrderId(customerWalletAddress: String) + + suspend fun clearAll(customerWalletAddress: String) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt index b005177a40..d0d1af52ab 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultOnboardingRepository.kt @@ -18,6 +18,7 @@ import kotlinx.coroutines.withContext import javax.inject.Inject private const val VALID_STATUS = "valid" +private const val APPROVED_KYC_STATUS = "APPROVED" private const val TAG = "TangemPay: OnboardingRepository" internal class DefaultOnboardingRepository @Inject constructor( @@ -47,13 +48,36 @@ internal class DefaultOnboardingRepository @Inject constructor( override suspend fun getMainScreenCustomerInfo(): Either { return requestHelper.runWithErrorLogs(TAG) { - val result = requestHelper.requestWithPersistedToken { authHeader -> - tangemPayApi.getCustomerMe(authHeader) - }.result + val customerWalletAddress = requestHelper.getCustomerWalletAddress() - val orderStatus = getOrderStatus().getOrNull() ?: error("Order status is null") + when (val orderId = tangemPayStorage.getOrderId(customerWalletAddress)) { + // If order id wasn't saved -> get customer info + null -> { + MainScreenCustomerInfo( + info = getCustomerInfoWithPersistedToken(), + orderStatus = OrderStatus.UNKNOWN, + ) + } + // If order id was saved -> check its status + else -> { + val orderStatus = getOrderStatus(orderId) + val customerInfo = when (orderStatus) { + // Kyc is passed and user waits for order creation -> no need to get customer info + OrderStatus.NEW, + OrderStatus.PROCESSING, + -> CustomerInfo(productInstance = null, isKycApproved = true, cardInfo = null) - MainScreenCustomerInfo(info = getCustomerInfo(result), orderStatus = orderStatus) + // Order was created/cancelled -> clear order id and get customer info + OrderStatus.UNKNOWN, + OrderStatus.COMPLETED, + OrderStatus.CANCELED, + -> getCustomerInfoWithPersistedToken().also { + tangemPayStorage.clearOrderId(customerWalletAddress) + } + } + MainScreenCustomerInfo(info = customerInfo, orderStatus = orderStatus) + } + } } } @@ -84,27 +108,28 @@ internal class DefaultOnboardingRepository @Inject constructor( } return CustomerInfo( productInstance = response?.productInstance?.let { ProductInstance(id = it.id, status = it.status) }, - kycStatus = response?.kyc?.status, + isKycApproved = response?.kyc?.status == APPROVED_KYC_STATUS, cardInfo = cardInfo, ) } - private suspend fun getOrderStatus(): Either { - return requestHelper.runWithErrorLogs(TAG) { - val walletAddress = requestHelper.getCustomerWalletAddress() - val orderId: String = tangemPayStorage.getOrderId(walletAddress) - ?: return@runWithErrorLogs OrderStatus.NOT_ISSUED + private suspend fun getOrderStatus(orderId: String): OrderStatus { + val result = requestHelper.request { authHeader -> + tangemPayApi.getOrder(authHeader, orderId) + }.result ?: error("Order result is null") - val result = requestHelper.request { authHeader -> - tangemPayApi.getOrder(authHeader, orderId) - }.result ?: error("Order result is null") - - when (result.status) { - OrderStatus.NEW.apiName -> OrderStatus.NEW - OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING - OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED - else -> OrderStatus.CANCELED - } + return when (result.status) { + OrderStatus.NEW.apiName -> OrderStatus.NEW + OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING + OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED + else -> OrderStatus.CANCELED } } + + private suspend fun getCustomerInfoWithPersistedToken(): CustomerInfo { + val result = requestHelper.requestWithPersistedToken { authHeader -> + tangemPayApi.getCustomerMe(authHeader) + }.result + return getCustomerInfo(result) + } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt index 9a5438f259..cb32317055 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/CustomerInfo.kt @@ -2,8 +2,6 @@ package com.tangem.domain.pay.model import java.math.BigDecimal -private const val APPROVED_KYC_STATUS = "APPROVED" - data class MainScreenCustomerInfo( val info: CustomerInfo, val orderStatus: OrderStatus, @@ -11,7 +9,7 @@ data class MainScreenCustomerInfo( data class CustomerInfo( val productInstance: ProductInstance?, - val kycStatus: String?, + val isKycApproved: Boolean, val cardInfo: CardInfo?, ) { @@ -26,6 +24,4 @@ data class CustomerInfo( val currencyCode: String, val customerWalletAddress: String, ) - - fun isKycApproved() = kycStatus == APPROVED_KYC_STATUS } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt index 6cab68a6fd..6ae706a0e1 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderStatus.kt @@ -1,7 +1,7 @@ package com.tangem.domain.pay.model enum class OrderStatus(val apiName: String) { - NOT_ISSUED(""), + UNKNOWN(""), NEW("NEW"), PROCESSING("PROCESSING"), COMPLETED("COMPLETED"), diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt index 65caca46f0..4b41785218 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayOnboardingModel.kt @@ -56,7 +56,7 @@ internal class TangemPayOnboardingModel @Inject constructor( repository.getCustomerInfo() .onRight { customerInfo -> when { - !customerInfo.isKycApproved() -> { + !customerInfo.isKycApproved -> { when (params) { is TangemPayOnboardingComponent.Params.Deeplink -> screenState.value = screenState.value.copy(fullScreenLoading = false) 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 c843cb1153..c2d9b76a0c 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 @@ -359,10 +359,10 @@ internal class WalletModel @Inject constructor( val info = tangemPayMainScreenCustomerInfoUseCase() if (info != null) { stateHolder.update( - transformer = TangemPayStateTransformer( + transformer = TangemPayInitialStateTransformer( value = info, - onIssueOrderClick = ::issueOrder, - onContinueKycClick = innerWalletRouter::openTangemPayOnboarding, + onClickIssue = ::issueOrder, + onClickKyc = innerWalletRouter::openTangemPayOnboarding, openDetails = innerWalletRouter::openTangemPayDetails, ), ) @@ -371,9 +371,9 @@ internal class WalletModel @Inject constructor( private fun issueOrder() { modelScope.launch { - stateHolder.update(TangemPayStateTransformer(issueProgressState = true)) + stateHolder.update(TangemPayIssueProgressStateTransformer()) tangemPayIssueOrderUseCase().onLeft { - stateHolder.update(TangemPayStateTransformer(issueState = true, onIssueOrderClick = ::issueOrder)) + stateHolder.update(TangemPayIssueAvailableStateTransformer(onClickIssue = ::issueOrder)) } } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt similarity index 50% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt index 9e367377ba..4af0c5bdc7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayInitialStateTransformer.kt @@ -1,33 +1,28 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers -import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.pay.model.CustomerInfo.CardInfo import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.OrderStatus.CANCELED -import com.tangem.domain.pay.model.OrderStatus.NOT_ISSUED +import com.tangem.domain.pay.model.OrderStatus.UNKNOWN import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState -import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createKycInProgressState import java.util.Currency -internal class TangemPayStateTransformer( +internal class TangemPayInitialStateTransformer( private val value: MainScreenCustomerInfo? = null, - private val onIssueOrderClick: () -> Unit = {}, - private val onContinueKycClick: () -> Unit = {}, + private val onClickIssue: () -> Unit = {}, + private val onClickKyc: () -> Unit = {}, private val openDetails: (customerWalletAddress: String, cardNumberEnd: String) -> Unit = { _, _ -> }, - private val issueProgressState: Boolean = false, - private val issueState: Boolean = false, ) : WalletScreenStateTransformer { override fun transform(prevState: WalletScreenState): WalletScreenState { - val tangemPayState = when { - issueProgressState -> createIssueProgressState() - issueState -> createIssueState() - else -> createInitialState() - } + val tangemPayState = createInitialState() return prevState.copy(tangemPayState = tangemPayState) } @@ -35,35 +30,13 @@ internal class TangemPayStateTransformer( val cardInfo = value?.info?.cardInfo return when { value == null -> TangemPayState.Empty - !value.info.isKycApproved() -> createKycInProgressState(onContinueKycClick) + !value.info.isKycApproved -> createKycInProgressState(onClickKyc) cardInfo != null -> getCardInfoState(cardInfo) - value.orderStatus == NOT_ISSUED || value.orderStatus == CANCELED -> createIssueState() + value.orderStatus == UNKNOWN || value.orderStatus == CANCELED -> createIssueAvailableState(onClickIssue) else -> createIssueProgressState() } } - private fun createIssueProgressState(): TangemPayState = Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - buttonText = TextReference.EMPTY, - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = {}, - showProgress = true, - ) - - private fun createIssueState() = Progress( - title = TextReference.Res(R.string.tangempay_issue_card_notification_title), - buttonText = TextReference.Res(R.string.common_continue), - iconRes = R.drawable.ic_tangem_pay_promo_card_36, - onButtonClick = onIssueOrderClick, - ) - - private fun createKycInProgressState(onContinueKycClick: () -> Unit): TangemPayState = Progress( - title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), - buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), - iconRes = R.drawable.ic_promo_kyc_36, - onButtonClick = onContinueKycClick, - ) - private fun getCardInfoState(cardInfo: CardInfo): TangemPayState = TangemPayState.Card( lastFourDigits = TextReference.Str("*${cardInfo.lastFourDigits}"), balanceText = TextReference.Str(getBalanceText(cardInfo)), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueAvailableStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueAvailableStateTransformer.kt new file mode 100644 index 0000000000..3f66ab33ad --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueAvailableStateTransformer.kt @@ -0,0 +1,12 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueAvailableState + +internal class TangemPayIssueAvailableStateTransformer( + private val onClickIssue: () -> Unit = {}, +) : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState = + prevState.copy(tangemPayState = createIssueAvailableState(onClickIssue)) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueProgressStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueProgressStateTransformer.kt new file mode 100644 index 0000000000..b7bc76806e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayIssueProgressStateTransformer.kt @@ -0,0 +1,10 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers + +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.util.TangemPayStateCreator.createIssueProgressState + +internal class TangemPayIssueProgressStateTransformer : WalletScreenStateTransformer { + + override fun transform(prevState: WalletScreenState): WalletScreenState = + prevState.copy(tangemPayState = createIssueProgressState()) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt new file mode 100644 index 0000000000..f8b3293d44 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/util/TangemPayStateCreator.kt @@ -0,0 +1,31 @@ +package com.tangem.feature.wallet.presentation.wallet.state.util + +import com.tangem.common.ui.R +import com.tangem.core.ui.extensions.TextReference +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState +import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState.Progress + +internal object TangemPayStateCreator { + + fun createKycInProgressState(onClickKyc: () -> Unit): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_title), + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = onClickKyc, + ) + + fun createIssueAvailableState(onClickIssue: () -> Unit) = Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.Res(R.string.common_continue), + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = onClickIssue, + ) + + fun createIssueProgressState(): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_issue_card_notification_title), + buttonText = TextReference.EMPTY, + iconRes = R.drawable.ic_tangem_pay_promo_card_36, + onButtonClick = {}, + showProgress = true, + ) +} \ No newline at end of file From 4dc994e8cf137ea1fdc282fee855fa39ca6a259c Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Oct 2025 18:17:18 +0700 Subject: [PATCH 21/22] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 10 +- .../com/tangem/common/routing/AppRoute.kt | 89 ++----------- .../intents/WalletContentClickIntents.kt | 22 ++-- .../WalletCurrencyActionsClickIntents.kt | 84 +++++------- .../intents/WalletWarningsClickIntents.kt | 2 +- .../router/DefaultWalletRouter.kt | 5 +- .../presentation/router/InnerWalletRouter.kt | 3 +- .../utils/TokenListAnalyticsSender.kt | 29 +++-- .../domain/GetMultiWalletWarningsFactory.kt | 123 ++++++++++-------- .../SetCryptoCurrencyActionsTransformer.kt | 9 +- .../transformers/SetVisaInfoTransformer.kt | 3 +- .../MultiWalletCurrencyActionsConverter.kt | 19 +-- .../converter/TokenListStateConverter.kt | 6 +- .../subscribers/BasicTokenListSubscriber.kt | 25 +++- .../MultiCurrencyAccountContent.kt | 12 +- .../utils/WalletFeatureUseCasesFacade.kt | 24 ---- 16 files changed, 196 insertions(+), 269 deletions(-) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletFeatureUseCasesFacade.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 900514a667..deb8b82709 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 @@ -200,7 +200,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = OnrampComponent.Params( - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, + userWalletId = route.userWalletId, cryptoCurrency = route.currency, source = route.source, shouldLaunchSepa = route.shouldLaunchSepa, @@ -274,7 +274,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = TokenDetailsComponent.Params( - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param + userWalletId = route.userWalletId, currency = route.currency, ), componentFactory = tokenDetailsComponentFactory, @@ -284,7 +284,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = StakingComponent.Params( - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, + userWalletId = route.userWalletId, cryptoCurrencyId = route.cryptoCurrencyId, yieldId = route.yieldId, ), @@ -297,7 +297,7 @@ internal class ChildFactory @Inject constructor( params = SwapComponent.Params( currencyFrom = route.currencyFrom, currencyTo = route.currencyTo, - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, + userWalletId = route.userWalletId, isInitialReverseOrder = route.isInitialReverseOrder, screenSource = route.screenSource, ), @@ -308,7 +308,7 @@ internal class ChildFactory @Inject constructor( createComponentChild( context = context, params = SendComponent.Params( - userWalletId = route.portfolioId.userWalletId, // todo account portfolioId param, + userWalletId = route.userWalletId, currency = route.currency, transactionId = route.transactionId, amount = route.amount, 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 d4566f6a58..d051bdb21e 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 @@ -11,7 +11,6 @@ import com.tangem.core.decompose.navigation.Route import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.feedback.models.WalletMetaInfo import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse @@ -51,50 +50,25 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class CurrencyDetails( - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val currency: CryptoCurrency, - ) : AppRoute(path = "/currency_details/${portfolioId.stringValue}/${currency.id.value}") { - companion object { - operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency) = CurrencyDetails( - portfolioId = PortfolioId(userWalletId), - currency = currency, - ) - } - } + ) : AppRoute(path = "/currency_details/${userWalletId.stringValue}/${currency.id.value}") @Serializable data class Send( - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val currency: CryptoCurrency, val transactionId: String? = null, val amount: String? = null, val tag: String? = null, val destinationAddress: String? = null, ) : AppRoute( - path = "/send/${portfolioId.stringValue}/${currency.id.value}?" + + path = "/send/${userWalletId.stringValue}/${currency.id.value}?" + "&$transactionId" + "&$amount" + "&$tag" + "&$destinationAddress", - ) { - companion object { - operator fun invoke( - userWalletId: UserWalletId, - currency: CryptoCurrency, - transactionId: String? = null, - amount: String? = null, - tag: String? = null, - destinationAddress: String? = null, - ) = Send( - portfolioId = PortfolioId(userWalletId), - currency = currency, - transactionId = transactionId, - amount = amount, - tag = tag, - destinationAddress = destinationAddress, - ) - } - } + ) @Serializable data class Details( @@ -199,51 +173,26 @@ sealed class AppRoute(val path: String) : Route { data class Swap( val currencyFrom: CryptoCurrency, val currencyTo: CryptoCurrency? = null, - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val isInitialReverseOrder: Boolean = false, val screenSource: String, ) : AppRoute( path = "/swap" + "/${currencyFrom.id.value}" + "/${currencyTo?.id?.value}" + - "/${portfolioId.stringValue}" + + "/${userWalletId.stringValue}" + "/$isInitialReverseOrder", - ) { - companion object { - operator fun invoke( - userWalletId: UserWalletId, - currencyFrom: CryptoCurrency, - currencyTo: CryptoCurrency? = null, - isInitialReverseOrder: Boolean = false, - screenSource: String, - ) = Swap( - portfolioId = PortfolioId(userWalletId), - currencyFrom = currencyFrom, - currencyTo = currencyTo, - isInitialReverseOrder = isInitialReverseOrder, - screenSource = screenSource, - ) - } - } + ) @Serializable data object AppCurrencySelector : AppRoute(path = "/app_currency_selector") @Serializable data class Staking( - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val cryptoCurrencyId: CryptoCurrency.ID, val yieldId: String, - ) : AppRoute(path = "/staking/${portfolioId.stringValue}/${cryptoCurrencyId.value}/$yieldId") { - companion object { - operator fun invoke(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, yieldId: String) = - Staking( - portfolioId = PortfolioId(userWalletId), - cryptoCurrencyId = cryptoCurrencyId, - yieldId = yieldId, - ) - } - } + ) : AppRoute(path = "/staking/${userWalletId.stringValue}/${cryptoCurrencyId.value}/$yieldId") @Serializable data class PushNotification( @@ -287,25 +236,11 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class Onramp( val source: OnrampSource, - val portfolioId: PortfolioId, + val userWalletId: UserWalletId, val currency: CryptoCurrency, val shouldLaunchSepa: Boolean = false, - ) : AppRoute(path = "/onramp/${portfolioId.stringValue}/${currency.symbol}"), RouteBundleParams { + ) : AppRoute(path = "/onramp/${userWalletId.stringValue}/${currency.symbol}"), RouteBundleParams { override fun getBundle(): Bundle = bundle(serializer()) - - companion object { - operator fun invoke( - source: OnrampSource, - userWalletId: UserWalletId, - currency: CryptoCurrency, - launchSepa: Boolean = false, - ) = Onramp( - source = source, - portfolioId = PortfolioId(userWalletId), - currency = currency, - shouldLaunchSepa = launchSepa, - ) - } } @Serializable 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 eb07bc02f7..a9e2270a44 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 @@ -4,10 +4,10 @@ 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.models.PortfolioId import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.redux.ReduxStateHolder @@ -45,9 +45,9 @@ internal interface WalletContentClickIntents { fun onDismissMarketsOnboarding() - fun onTokenItemClick(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) + fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) - fun onTokenItemLongClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) fun onAccountExpandClick(account: Account) @@ -138,13 +138,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } - override fun onTokenItemClick(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) { - router.openTokenDetails(portfolioId, currencyStatus) + override fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { + router.openTokenDetails(userWalletId, currencyStatus) } - override fun onTokenItemLongClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.main) { - val userWalletId = portfolioId.userWalletId val userWallet = getUserWalletUseCase(userWalletId).getOrElse { Timber.e( """ @@ -160,7 +159,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) .take(count = 1) .collectLatest { - showActionsBottomSheet(it, userWallet, portfolioId) + showActionsBottomSheet(it, userWallet) } } } @@ -175,17 +174,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( accountDependencies.expandedAccountsHolder.collapseAccount(userWalletId, account.accountId) } - private fun showActionsBottomSheet( - tokenActionsState: TokenActionsState, - userWallet: UserWallet, - portfolioId: PortfolioId, - ) { + private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) { stateHolder.showBottomSheet( ActionsBottomSheetConfig( actions = MultiWalletCurrencyActionsConverter( userWallet = userWallet, clickIntents = currencyActionsClickIntents, - portfolioId = portfolioId, ).convert(tokenActionsState), ), userWallet.walletId, 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 79f970b483..c2c173c0e9 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 @@ -29,7 +29,6 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency @@ -66,7 +65,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender -import com.tangem.feature.wallet.presentation.wallet.utils.WalletFeatureUseCasesFacade import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -80,7 +78,7 @@ import javax.inject.Inject interface WalletCurrencyActionsClickIntents { fun onSendClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) @@ -88,32 +86,32 @@ interface WalletCurrencyActionsClickIntents { fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) fun onBuyClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, - portfolioId: PortfolioId, + userWalletId: UserWalletId, unavailabilityReason: ScenarioUnavailabilityReason, ) fun onReceiveClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent? = null, ) - fun onStakeClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) + fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? - fun onCopyAddressClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onHideTokensClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onPerformHideToken(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) fun onExploreClick() @@ -135,7 +133,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val isDemoCardUseCase: IsDemoCardUseCase, private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val useCasesFacade: WalletFeatureUseCasesFacade, private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getStoryContentUseCase: GetStoryContentUseCase, @@ -154,10 +151,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, + private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, + private val removeCurrencyUseCase: RemoveCurrencyUseCase, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) { @@ -181,27 +180,21 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name) stateHolder.hideBottomSheet() - navigateToSend(cryptoCurrencyStatus, portfolioId) + navigateToSend(cryptoCurrencyStatus, userWalletId) } }, ) } else { - navigateToSend(cryptoCurrencyStatus, portfolioId) + navigateToSend(cryptoCurrencyStatus, userWalletId) } } } override fun onReceiveClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent?, ) { - val userWalletId = portfolioId.userWalletId - if (portfolioId is PortfolioId.Account) { - // todo account find address - TODO("account") - } - analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonWithParams.ButtonReceive( token = cryptoCurrencyStatus.currency.symbol, @@ -274,12 +267,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) } - override fun onCopyAddressClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) { - val userWalletId = portfolioId.userWalletId - if (portfolioId is PortfolioId.Account) { - // todo account find address - TODO("account") - } + override fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send( event = TokenReceiveNewAnalyticsEvent.ButtonCopyAddress( token = cryptoCurrencyStatus.currency.symbol, @@ -301,7 +289,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onHideTokensClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol), ) @@ -309,19 +297,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { walletEventSender.send( event = WalletEvent.ShowAlert( - state = getHideTokeAlertConfig(portfolioId, cryptoCurrencyStatus), + state = getHideTokeAlertConfig(userWalletId, cryptoCurrencyStatus), ), ) } } private suspend fun getHideTokeAlertConfig( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, ): WalletAlertState.DefaultAlert { val currency = cryptoCurrencyStatus.currency val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin && - !useCasesFacade.isCryptoCurrencyCoinCouldHide(portfolioId = portfolioId, cryptoCurrencyCoin = currency) + !isCryptoCurrencyCoinCouldHide(userWalletId = userWalletId, cryptoCurrencyCoin = currency) return if (isCryptoCurrencyCoinCouldHide) { WalletAlertState.DefaultAlert( title = resourceReference( @@ -347,14 +335,14 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( formatArgs = WrappedList(listOf(cryptoCurrencyStatus.currency.name)), ), message = resourceReference(R.string.token_details_hide_alert_message), - onConfirmClick = { onPerformHideToken(portfolioId, cryptoCurrencyStatus) }, + onConfirmClick = { onPerformHideToken(userWalletId, cryptoCurrencyStatus) }, ) } } - override fun onPerformHideToken(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.io) { - useCasesFacade.removeCurrencyUseCase(portfolioId, cryptoCurrencyStatus.currency) + removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency) .fold( ifLeft = { walletEventSender.send( @@ -362,7 +350,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) }, ifRight = { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId)) + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) }, ) } @@ -395,7 +383,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onBuyClick( - portfolioId: PortfolioId, + userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) { @@ -411,7 +399,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push( AppRoute.Onramp( - portfolioId = portfolioId, + userWalletId = userWalletId, currency = cryptoCurrencyStatus.currency, source = OnrampSource.TOKEN_LONG_TAP, ), @@ -420,7 +408,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, - portfolioId: PortfolioId, + userWalletId: UserWalletId, unavailabilityReason: ScenarioUnavailabilityReason, ) { analyticsEventHandler.send( @@ -443,12 +431,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name) stateHolder.hideBottomSheet() - navigateToSwap(cryptoCurrencyStatus, portfolioId) + navigateToSwap(cryptoCurrencyStatus, userWalletId) } }, ) } else { - navigateToSwap(cryptoCurrencyStatus, portfolioId) + navigateToSwap(cryptoCurrencyStatus, userWalletId) } } } @@ -489,15 +477,15 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onStakeClick(portfolioId: PortfolioId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId)) + override fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, yield: Yield?) { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) modelScope.launch { val cryptoCurrency = cryptoCurrencyStatus.currency appRouter.push( AppRoute.Staking( - portfolioId = portfolioId, + userWalletId = userWalletId, cryptoCurrencyId = cryptoCurrency.id, yieldId = yield?.id ?: return@launch, ), @@ -766,21 +754,21 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) } - private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, portfolioId: PortfolioId) { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = portfolioId.userWalletId)) + private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) { + stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) val route = AppRoute.Send( currency = cryptoCurrencyStatus.currency, - portfolioId = portfolioId, + userWalletId = userWalletId, ) appRouter.push(route) } - private fun navigateToSwap(cryptoCurrencyStatus: CryptoCurrencyStatus, portfolioId: PortfolioId) { + private fun navigateToSwap(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) { appRouter.push( AppRoute.Swap( currencyFrom = cryptoCurrencyStatus.currency, - portfolioId = portfolioId, + userWalletId = userWalletId, screenSource = AnalyticsParam.ScreensSources.LongTap.value, ), ) 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 d1e7c8cba5..937ae92eea 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 @@ -365,7 +365,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( userWalletId = userWallet.walletId, currency = cryptoCurrency, source = OnrampSource.SEPA_BANNER, - launchSepa = true, + shouldLaunchSepa = 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 95c0458d24..1bab1f8a28 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 @@ -7,7 +7,6 @@ import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -67,12 +66,12 @@ internal class DefaultWalletRouter @Inject constructor( urlOpener.openUrl(url) } - override fun openTokenDetails(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) { + override fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { val networkAddress = currencyStatus.value.networkAddress if (networkAddress != null && networkAddress.defaultAddress.value.isNotEmpty()) { router.push( AppRoute.CurrencyDetails( - portfolioId = portfolioId, + userWalletId = userWalletId, currency = currencyStatus.currency, ), ) 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 2ff741c7b5..106cafa945 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 @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.router import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -42,7 +41,7 @@ internal interface InnerWalletRouter { fun openUrl(url: String) /** Open token details screen */ - fun openTokenDetails(portfolioId: PortfolioId, currencyStatus: CryptoCurrencyStatus) + fun openTokenDetails(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) /** Open stories screen */ fun openStoriesScreen() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index bf5b27d5ea..fe583f7183 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -14,7 +14,6 @@ import com.tangem.domain.analytics.model.WalletBalanceState import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency @@ -38,33 +37,41 @@ internal class TokenListAnalyticsSender @Inject constructor( private val mutex = Mutex() private val loadingTraces = mutableMapOf() - suspend fun send(displayedUiState: WalletState?, userWallet: UserWallet, tokenList: TokenList) { + suspend fun send( + displayedUiState: WalletState?, + userWallet: UserWallet, + totalFiatBalance: TotalFiatBalance, + flattenCurrencies: List, + ) { if (screenLifecycleProvider.isBackgroundState.value) return if (displayedUiState == null || displayedUiState.pullToRefreshConfig.isRefreshing) return - if (tokenList.totalFiatBalance is TotalFiatBalance.Loading) { - startLoadingTraceIfNeeded(userWallet.walletId, tokenList) + if (totalFiatBalance is TotalFiatBalance.Loading) { + startLoadingTraceIfNeeded(userWallet.walletId, flattenCurrencies) return } - if (isTerminalState(tokenList.totalFiatBalance)) { - stopLoadingTraceIfNeeded(userWallet.walletId, tokenList.totalFiatBalance) + if (isTerminalState(totalFiatBalance)) { + stopLoadingTraceIfNeeded(userWallet.walletId, totalFiatBalance) } - val currenciesStatuses = tokenList.flattenCurrencies() + val currenciesStatuses = flattenCurrencies - sendBalanceLoadedEventIfNeeded(tokenList.totalFiatBalance, currenciesStatuses) - sendToppedUpEventIfNeeded(userWallet, tokenList.totalFiatBalance, currenciesStatuses) + sendBalanceLoadedEventIfNeeded(totalFiatBalance, currenciesStatuses) + sendToppedUpEventIfNeeded(userWallet, totalFiatBalance, currenciesStatuses) sendUnreachableNetworksEventIfNeeded(currenciesStatuses) sendTokenBalancesIfNeeded(currenciesStatuses) } - private suspend fun startLoadingTraceIfNeeded(userWalletId: UserWalletId, tokenList: TokenList) { + private suspend fun startLoadingTraceIfNeeded( + userWalletId: UserWalletId, + flattenCurrencies: List, + ) { mutex.withLock { if (!loadingTraces.containsKey(userWalletId)) { val trace = FirebasePerformance.getInstance().newTrace(BALANCE_LOADED_TRACE_NAME) trace.start() - trace.putAttribute(TOKENS_COUNT, tokenList.flattenCurrencies().size.toString()) + trace.putAttribute(TOKENS_COUNT, flattenCurrencies.size.toString()) loadingTraces[userWalletId] = trace } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 2d5684a5c0..be35057603 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -9,17 +9,16 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.notifications.NotificationConfig.IconTint import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce +import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.onramp.GetOnrampCountryUseCase @@ -45,6 +44,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject @@ -70,72 +70,81 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - val tokenListFlow = if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) { + val accountStatusList by lazy { val params = SingleAccountStatusListProducer.Params(userWallet.walletId) accountDependencies.singleAccountStatusListSupplier(params) - } else { - tokenListStore.getOrThrow(userWallet.walletId) + .map { it.totalFiatBalance to it.flattenCurrencies() } + .map { Lce.Content(it) } } + + fun tokenListFlow(): LceFlow>> { + return if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) { + accountStatusList + } else { + runCatching { tokenListStore.getOrThrow(userWallet.walletId) } + .map { result -> result.map { lce -> lce.map { it.totalFiatBalance to it.flattenCurrencies() } } } + .getOrNull() + // in case of runtime change ft in tester menu + ?: accountStatusList + } + } + + // val params = SingleAccountStatusListProducer.Params(userWallet.walletId) + // val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) return combine( - tokenListFlow, + // todo account just use it, after delete accountsFeatureToggles + // accountStatusListFlow, isReadyToShowRateAppUseCase(), isNeedToBackupUseCase(userWallet.walletId), seedPhraseNotificationUseCase(userWalletId = userWallet.walletId), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Referral), shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa), notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key), - ) { array -> - val totalFiatBalance: Lce - val flattenCurrencies: Lce> - if (accountDependencies.accountsFeatureToggles.isFeatureEnabled) { - val accountStatusList = array[0] as AccountStatusList - totalFiatBalance = Lce.Content(accountStatusList.totalFiatBalance) - flattenCurrencies = Lce.Content(accountStatusList.flattenCurrencies()) - } else { - val maybeTokenList = array[0] as Lce - totalFiatBalance = maybeTokenList.map { it.totalFiatBalance } - flattenCurrencies = maybeTokenList.map { it.flattenCurrencies() } + ) { array -> array } + .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } + .map { array -> + val lceTokens = array[0] as Lce>> + val totalFiatBalance = lceTokens.map { it.first } + val flattenCurrencies = lceTokens.map { it.second } + val isReadyToShowRating = array[1] as Boolean + val isNeedToBackup = array[2] as Boolean + val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus + val shouldShowReferralPromo = array[4] as Boolean + val shouldShowSepaBanner = array[5] as Boolean + val shouldShowEnablePushesReminderNotification = array[6] as Boolean + + buildList { + addUsedOutdatedDataNotification(totalFiatBalance) + + addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + + addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents) + + addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) + + addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner) + + addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) + + addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) + + addPushReminderNotification( + clickIntents = clickIntents, + shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification && + !notificationsRepository.isUserAllowToSubscribeOnPushNotifications(), + ) + + addYieldSupplyNotifications(flattenCurrencies) + + val hasCriticalOrWarning = any { notification -> + notification is WalletNotification.Critical || notification is WalletNotification.Warning + } + + if (!hasCriticalOrWarning) { + addRateTheAppNotification(isReadyToShowRating, clickIntents) + } + }.toImmutableList() } - - val isReadyToShowRating = array[1] as Boolean - val isNeedToBackup = array[2] as Boolean - val seedPhraseIssueStatus = array[3] as SeedPhraseNotificationsStatus - val shouldShowReferralPromo = array[4] as Boolean - val shouldShowSepaBanner = array[5] as Boolean - val shouldShowEnablePushesReminderNotification = array[6] as Boolean - - buildList { - addUsedOutdatedDataNotification(totalFiatBalance) - - addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) - - addFinishWalletActivationNotification(userWallet, totalFiatBalance, clickIntents) - - addReferralPromoNotification(cardTypesResolver, clickIntents, shouldShowReferralPromo) - - addSepaPromoNotification(userWallet, clickIntents, shouldShowSepaBanner) - - addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) - - addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) - - addPushReminderNotification( - clickIntents = clickIntents, - shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification && - !notificationsRepository.isUserAllowToSubscribeOnPushNotifications(), - ) - - addYieldSupplyNotifications(flattenCurrencies) - - val hasCriticalOrWarning = any { notification -> - notification is WalletNotification.Critical || notification is WalletNotification.Warning - } - - if (!hasCriticalOrWarning) { - addRateTheAppNotification(isReadyToShowRating, clickIntents) - } - }.toImmutableList() - } } private fun MutableList.addUsedOutdatedDataNotification( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt index 9565609246..230eac3a61 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetCryptoCurrencyActionsTransformer.kt @@ -50,7 +50,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onBuyClick( - portfolioId = portfolioId, + userWalletId = portfolioId.userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, unavailabilityReason = action.unavailabilityReason, ) @@ -62,7 +62,10 @@ internal class SetCryptoCurrencyActionsTransformer( enabled = true, dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { - clickIntents.onReceiveClick(portfolioId, cryptoCurrencyStatus = cryptoCurrencyStatus) + clickIntents.onReceiveClick( + portfolioId.userWalletId, + cryptoCurrencyStatus = cryptoCurrencyStatus, + ) }, onLongClick = { clickIntents.onCopyAddressLongClick(cryptoCurrencyStatus = cryptoCurrencyStatus) @@ -87,7 +90,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onSendClick( - portfolioId = portfolioId, + userWalletId = portfolioId.userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, unavailabilityReason = action.unavailabilityReason, ) 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 18974cedeb..0c3ac24ecd 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 @@ -9,7 +9,6 @@ import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.card.common.util.getCardsCount -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.visa.exception.RefreshTokenExpiredException @@ -153,7 +152,7 @@ internal class SetVisaInfoTransformer( dimContent = false, onClick = { clickIntents.onReceiveClick( - portfolioId = PortfolioId(userWalletId), // todo account Visa use Main account? + userWalletId = userWalletId, cryptoCurrencyStatus = cryptoCurrencyStatus, event = MainScreenAnalyticsEvent.ButtonReceive, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 6a88f70616..72e55cfb23 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents @@ -18,10 +18,11 @@ import kotlinx.collections.immutable.toImmutableList internal class MultiWalletCurrencyActionsConverter( private val userWallet: UserWallet, - private val portfolioId: PortfolioId, private val clickIntents: WalletCurrencyActionsClickIntents, ) : Converter> { + private val userWalletId: UserWalletId = userWallet.walletId + override fun convert(value: TokenActionsState): ImmutableList { return value.states .filterIfSingleWithToken() @@ -56,17 +57,17 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Buy -> { title = resourceReference(R.string.common_buy) icon = R.drawable.ic_plus_24 - action = { clickIntents.onBuyClick(portfolioId, cryptoCurrencyStatus, noneReason) } + action = { clickIntents.onBuyClick(userWalletId, cryptoCurrencyStatus, noneReason) } } is TokenActionsState.ActionState.Receive -> { title = resourceReference(R.string.common_receive) icon = R.drawable.ic_arrow_down_24 - action = { clickIntents.onReceiveClick(portfolioId, cryptoCurrencyStatus) } + action = { clickIntents.onReceiveClick(userWalletId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Stake -> { title = resourceReference(R.string.common_stake) icon = R.drawable.ic_staking_24 - action = { clickIntents.onStakeClick(portfolioId, cryptoCurrencyStatus, actionsState.yield) } + action = { clickIntents.onStakeClick(userWalletId, cryptoCurrencyStatus, actionsState.yield) } } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) @@ -76,7 +77,7 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.Send -> { title = resourceReference(R.string.common_send) icon = R.drawable.ic_arrow_up_24 - action = { clickIntents.onSendClick(portfolioId, cryptoCurrencyStatus, noneReason) } + action = { clickIntents.onSendClick(userWalletId, cryptoCurrencyStatus, noneReason) } } is TokenActionsState.ActionState.Swap -> { title = resourceReference(R.string.swapping_swap_action) @@ -84,7 +85,7 @@ internal class MultiWalletCurrencyActionsConverter( action = { clickIntents.onSwapClick( cryptoCurrencyStatus = cryptoCurrencyStatus, - portfolioId = portfolioId, + userWalletId = userWalletId, unavailabilityReason = noneReason, ) } @@ -92,12 +93,12 @@ internal class MultiWalletCurrencyActionsConverter( is TokenActionsState.ActionState.CopyAddress -> { title = resourceReference(R.string.common_copy_address) icon = R.drawable.ic_copy_24 - action = { clickIntents.onCopyAddressClick(portfolioId, cryptoCurrencyStatus) } + action = { clickIntents.onCopyAddressClick(userWalletId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.HideToken -> { title = resourceReference(R.string.token_details_hide_token) icon = R.drawable.ic_hide_24 - action = { clickIntents.onHideTokensClick(portfolioId, cryptoCurrencyStatus) } + action = { clickIntents.onHideTokensClick(userWalletId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Analytics -> { title = resourceReference(R.string.common_analytics) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 8cbaeee5da..909953e420 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -41,14 +41,12 @@ internal class TokenListStateConverter( private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> - val id = accountId?.let { PortfolioId(accountId) } ?: PortfolioId(selectedWallet.walletId) - clickIntents.onTokenItemClick(id, currencyStatus) + clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) } private val onTokenLongClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> - val id = accountId?.let { PortfolioId(accountId) } ?: PortfolioId(selectedWallet.walletId) - clickIntents.onTokenItemLongClick(id, currencyStatus) + clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) } private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index b9b464642d..80068f621c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -8,7 +8,9 @@ import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.getOrElse import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.RunPolkadotAccountHealthCheckUseCase @@ -63,7 +65,10 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { flow = tokenListFlow(coroutineScope) .onEach { maybeTokenList -> coroutineScope.launch { - sendTokenListAnalytics(maybeTokenList) + sendTokenListAnalytics( + flattenCurrencies = maybeTokenList.getOrNull()?.flattenCurrencies(), + totalFiatBalance = maybeTokenList.getOrNull()?.totalFiatBalance, + ) }.saveIn(sendAnalyticsJobHolder) } .distinctUntilChanged() @@ -140,12 +145,14 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { private fun createAccountListFlow(coroutineScope: CoroutineScope): Flow<*> = combine( flow = accountListFlow(coroutineScope) - // todo account analytics for account total balance - /*.onEach { maybeTokenList -> + .onEach { accountStatusList -> coroutineScope.launch { - sendTokenListAnalytics(maybeTokenList) + sendTokenListAnalytics( + flattenCurrencies = accountStatusList.flattenCurrencies(), + totalFiatBalance = accountStatusList.totalFiatBalance, + ) }.saveIn(sendAnalyticsJobHolder) - }*/ + } .distinctUntilChanged() .onEach { accountList -> // todo account see[onAccountListReceived] @@ -207,13 +214,17 @@ internal abstract class BasicTokenListSubscriber : WalletSubscriber() { } } - private suspend fun sendTokenListAnalytics(maybeTokenList: Lce) { + private suspend fun sendTokenListAnalytics( + flattenCurrencies: List?, + totalFiatBalance: TotalFiatBalance?, + ) { val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) tokenListAnalyticsSender.send( displayedUiState = displayedState, userWallet = userWallet, - tokenList = maybeTokenList.getOrNull() ?: return, + flattenCurrencies = flattenCurrencies ?: return, + totalFiatBalance = totalFiatBalance ?: return, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt index 140a458bfc..95126a8582 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyAccountContent.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.tokenlist.PortfolioListItem import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM @@ -59,6 +60,7 @@ internal fun LazyListScope.portfolioTokensList( contentType = { _, item -> item::class.java }, itemContent = { tokenIndex, token -> val indexWithHeader = tokenIndex.inc() + val lastIndex = tokens.lastIndex.inc() val isPreview = LocalInspectionMode.current val appear = remember { MutableTransitionState(isPreview).apply { targetState = true } @@ -69,11 +71,12 @@ internal fun LazyListScope.portfolioTokensList( .animateItem() .roundedShapeItemDecoration( currentIndex = indexWithHeader, - lastIndex = tokens.lastIndex.inc(), + lastIndex = lastIndex, backgroundColor = TangemTheme.colors.background.primary, ), visibleState = appear, ) { + val modifier = if (indexWithHeader == lastIndex) Modifier.padding(bottom = 8.dp) else Modifier PortfolioTokensListItem( state = token, isBalanceHidden = isBalanceHidden, @@ -114,10 +117,15 @@ private fun LazyListScope.portfolioItem( modifier = anchorModifier, visibleState = appear, ) { + val modifier = if (portfolio.tokens.isEmpty()) { + Modifier.padding(vertical = 8.dp) + } else { + Modifier.padding(top = 8.dp) + } PortfolioListItem( state = portfolio, isBalanceHidden = isBalanceHidden, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + modifier = modifier, ) } } else { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletFeatureUseCasesFacade.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletFeatureUseCasesFacade.kt deleted file mode 100644 index 8ab07acad0..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/utils/WalletFeatureUseCasesFacade.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.utils - -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.tokens.IsCryptoCurrencyCoinCouldHideUseCase -import com.tangem.domain.tokens.RemoveCurrencyUseCase -import javax.inject.Inject - -class WalletFeatureUseCasesFacade @Inject constructor( - private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, -) { - - suspend fun isCryptoCurrencyCoinCouldHide(portfolioId: PortfolioId, cryptoCurrencyCoin: CryptoCurrency.Coin) = - when (portfolioId) { - is PortfolioId.Account -> TODO("account") - is PortfolioId.Wallet -> isCryptoCurrencyCoinCouldHide(portfolioId.userWalletId, cryptoCurrencyCoin) - } - - suspend fun removeCurrencyUseCase(portfolioId: PortfolioId, currency: CryptoCurrency) = when (portfolioId) { - is PortfolioId.Account -> TODO("account") - is PortfolioId.Wallet -> removeCurrencyUseCase(portfolioId.userWalletId, currency) - } -} \ No newline at end of file From 03da1731ea29f89041ca1624fba73019c1fc2922 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Oct 2025 18:25:02 +0700 Subject: [PATCH 22/22] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 10 ++- .../com/tangem/common/routing/AppRoute.kt | 4 +- core/res/src/main/res/values-de/strings.xml | 88 ++++++++++++++++++- core/res/src/main/res/values-ja/strings.xml | 19 +++- core/res/src/main/res/values-ru/strings.xml | 24 +++++ .../src/main/res/values-uk-rUA/strings.xml | 4 + core/res/src/main/res/values/strings.xml | 13 ++- .../SingleAccountStatusListSupplier.kt | 10 ++- .../com/tangem/domain/tokens/TokensAction.kt | 16 ---- .../account/details/AccountDetailsModel.kt | 14 ++- .../details/entity/AccountDetailsUM.kt | 1 + .../details/ui/AccountDetailsContent.kt | 29 +++--- .../component/ManageTokensComponent.kt | 10 +-- .../component/ManageTokensSource.kt | 9 +- features/manage-tokens/impl/build.gradle.kts | 10 +++ .../entity/managetokens/ManageTokensUM.kt | 3 + .../model/CustomTokenSelectorModel.kt | 73 ++++++++++++++- .../managetokens/model/ManageTokensModel.kt | 54 +++++++++--- .../managetokens/ui/ManageTokensScreen.kt | 10 ++- .../model/WalletSettingsModel.kt | 5 +- .../walletsettings/utils/ItemsBuilder.kt | 3 +- .../intents/WalletContentClickIntents.kt | 10 --- .../router/DefaultWalletRouter.kt | 5 -- .../presentation/router/InnerWalletRouter.kt | 3 - 24 files changed, 335 insertions(+), 92 deletions(-) delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.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 deb8b82709..e2ce3a2b4a 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 @@ -2,6 +2,7 @@ package com.tangem.tap.routing.utils import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.domain.models.PortfolioId import com.tangem.domain.qrscanning.models.SourceType import com.tangem.feature.qrscanning.QrScanningComponent import com.tangem.feature.referral.api.ReferralComponent @@ -19,6 +20,7 @@ import com.tangem.features.hotwallet.* import com.tangem.features.kyc.KycComponent import com.tangem.features.managetokens.component.ChooseManagedTokensComponent import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.markets.details.MarketsTokenDetailsComponent import com.tangem.features.markets.tokenlist.MarketsTokenListComponent @@ -140,9 +142,15 @@ internal class ChildFactory @Inject constructor( AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES } + val mode = when (val portfolio = route.portfolioId) { + is PortfolioId.Account -> ManageTokensMode.Account(portfolio.accountId) + is PortfolioId.Wallet -> ManageTokensMode.Wallet(portfolio.userWalletId) + null -> ManageTokensMode.None + } + createComponentChild( context = context, - params = ManageTokensComponent.Params(route.userWalletId, source), + params = ManageTokensComponent.Params(mode, source), componentFactory = manageTokensComponentFactory, ) } 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 d051bdb21e..ce692050bc 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 @@ -121,8 +121,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class ManageTokens( val source: Source, - val userWalletId: UserWalletId? = null, - ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/$userWalletId") { + val portfolioId: PortfolioId? = null, + ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") { enum class Source { STORIES, diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 2817667d7e..645d4943f0 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -4,8 +4,8 @@ Trotzdem überspringen Zugangscode nicht festgelegt Zugangscode eingeben - Falscher Zugangscode. Deine Hot-Wallet wird nach %s weiteren Fehlversuchen gelöscht. - Falscher Zugangscode. App wird bei %s weiteren Eingabefehlern gesperrt + Falscher Zugangscode. Deine mobile Wallet wird nach %s weiteren Fehlversuchen gelöscht. + Falscher Zugangscode. Die App wird nach %s weiteren Fehlversuchen gesperrt. Falscher Zugangscode.\nBitte warte %s Sekunden und versuche es erneut. Bestätige Deinen zuvor eingegebenen Code, um fortzufahren Zugangscode erneut eingeben @@ -19,6 +19,7 @@ Wiederherstellen Du bist dabei, dich zu erholen \"%1$s”. Konto wiederherstellen + Du hast das Limit von 20 aktiven Konten bereits überschritten. Archiviere eines, um es wiederherzustellen. Archiviert Konto erstellt Archivkonto @@ -30,6 +31,7 @@ Konto hinzufügen Speichern Kontoname + Der Kontoname ist bereits vorhanden Konto Neues Konto Konto hinzufügen @@ -104,7 +106,7 @@ %s Karte %s Karten - Handy-Wallet + Mobile-Wallet Du hast Deine Wallet erfolgreich gesichert. Diese Wörter können bei Verlust nicht wiederhergestellt werden. Bewahre diese an einem sicheren Ort auf. Sicherung abgeschlossen @@ -174,6 +176,7 @@ Akzeptieren Zugang verweigert Konten + Aktivieren Hinzufügen Zum Portfolio hinzufügen Token hinzufügen @@ -182,6 +185,7 @@ Erlauben Betrag Analysen + und Anwenden Genehmigung Genehmigen @@ -198,6 +202,7 @@ D hast keinen Zugang zur Kamera erteilt, bitte passe deine Datenschutzeinstellungen an Abbrechen Ändern + Konto auswählen Aktion wählen Netzwerk auswählen Token auswählen @@ -246,7 +251,9 @@ Gebühren Beenden Kostenlos + Vom Adressen synchronisieren + Erste Schritte Zum Anbieter gehen Zum Token Verstanden @@ -255,6 +262,7 @@ Importieren In Arbeit Später + Mehr erfahren %1$s übrig Legacy Bitcoin Gesperrt @@ -505,6 +513,7 @@ Neues Wallet erstellen Karte oder Ring bestellen Karte oder Ring scannen + Möchtest Du „Tangem“ die biometrische Authentifizierung erlauben? Um Deine Identität zu bestätigen und die App zu öffnen An %s Im %s Netzwerk Willst Du den Vorgang zur Erstellung des Zugangscodes wirklich beenden? @@ -516,7 +525,7 @@ Wenn dies der Fall ist, musst Du von vorne beginnen. Bist Du sicher, dass Du den Aktivierungsprozess beenden willst? Wenn dies der Fall ist, musst Du von vorne beginnen. - Wiederherstellung einer bestehenden Wallet, die in Ihrem Google Drive-Backup gespeichert ist + Vorhandene Wallet über Google Drive-Backup wiederherstellen Google Drive-Backup Verbesser Deine Sicherheit sofort mit einer erstklassigen Hardware-Wallet von Tangem. Hardware-Wallet @@ -528,6 +537,7 @@ Schlüssel werden in der App gespeichert Sicherung der Seed-Phrase Mobile Wallet erstellen + Vorhandene Wallet importieren Diese Wiederherstellungsphrase wurde bereits importiert Mobile Wallet Dein Geld bleibt während des gesamten Prozesses sicher und vollständig zugänglich @@ -848,6 +858,7 @@ Zugangscode wiederherstellen Identische Karten oder Ring Zugangscode + Alle Angebote Verfügbar mit %s Anbieter erleichtern Transaktionen Suche nach Land @@ -855,13 +866,24 @@ Andere Währungen Beliebte Fiats Suche nach Währung + Sofort Durch die Nutzung der Onramp-Funktionalität stimmst Du den %1$s und %2$s des Anbieters zu. + Der Service wird von einem externen Anbieter bereitgestellt. \n Tangem übernimmt keine Verantwortung. Der Kaufbetrag sollte nicht höher sein als %s Der zu kaufende Betrag muss mindestens %s betragen Keine verfügbaren Anbieter für diese Währung + Schnellste Bezahlen mit + Zahlungsmethode Verfügbar bis zu %s Erhältlich bei %s + + Anbieter + Anbieter + + Anbieter + Kürzlich verwendet + Empfohlen Du kannst Deine Transaktion beim Drittanbieter %s abschließen. Umleitung auf %s… Unsere Dienstleistungen sind in diesem Land nicht verfügbar @@ -870,13 +892,18 @@ Bitte wähle das richtige Land aus, um korrekte Zahlungsoptionen und Dienstleistungen zu gewährleisten. Einstellungen Du kannst diesen Bildschirm schließen und den Transaktionsstatus auf dem Bildschirm mit den Token-Details überprüfen. + %s min + Du erhältst + Der Service wird von einem externen Anbieter bereitgestellt. \n Tangem übernimmt keine Verantwortung. Du kannst diesen Bildschirm schließen und den Transaktionsstatus auf dem Bildschirm mit den Token-Details überprüfen. + Bis zu Über Du zahlst Gruppe erstellen Nach Guthaben Token organisieren Gruppe löschen + %s Unterstützung Mehr Infos Du kannst Benachrichtigungen für Tangem in den Einstellungen aktivieren. Später aktivieren @@ -895,6 +922,7 @@ 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 Überweise Geld von einem anderen Wallet oder einer anderen Börse + Adresse für Prämien Teilnehmen Die Informationen zum Empfehlungsprogramm konnten nicht geladen werden. Bitte versuche es später noch einmal. Die Informationen über das Empfehlungsprogramm konnten nicht geladen werden. Fehlercode: %s. Bitte versuche es später noch einmal. @@ -1071,6 +1099,9 @@ Hiermit wird die Wallet aus der Anwendung entfernt. Die Wallet selbst kann wieder hinzugefügt werden. Name Lass deinen Token für dich arbeiten + Eine Netzwerkgebühr ist eine kleine Zahlung, die erforderlich ist, um Deine Transaktion auf der Blockchain zu verarbeiten und zu bestätigen. + Um mit dem Staking zu beginnen, muss Dein TON-Konto mit einer Selbsttransaktion von 1 TON aktiviert werden. Das Guthaben verbleibt in Deiner Wallet – dieser Schritt aktiviert Dein Konto lediglich für das Staking. + Kontoaktivierung Die Anzahl der zu stakenden Krypros muss mindesten %s betragen Der Stakingbetrag wird aufgrund der Netzwerkregeln auf %1$s TRX aufgerundet. Der Betrag der unstaked wird, wird aufgrund der Netzwerkregeln auf %1$s TRX gerundet. @@ -1079,6 +1110,7 @@ Ein Staking-Konto ist ein spezielles Konto, auf dem eingesetzte SOL-Token gespeichert werden. Es wird erstellt, indem Sie Ihre Token an einen Validator delegieren, um an der Transaktionsvalidierung teilzunehmen und Belohnungen zu erhalten. Für die Einrichtung des Staking-Kontos wird eine geringe Gebühr erhoben, die nach Abschluss des Stakings zurückerstattet wird. Jährliche prozentuale Rendite Die jährliche prozentuale Rendite, die du durch die Teilnahme am Staking erzielen kannst. + APY zeigt den jährlichen prozentualen Ertrag des Validators basierend auf seiner Leistung Effektiver Jahreszins Belohnungen sammeln sich täglich automatisch in deinem Staking-Konto an. Verfügbar @@ -1134,6 +1166,8 @@ Reinvestiert Deine verdienten Prämien in Deinen Einsatzbetrag und erhöht so den potenziellen Gewinn. Mit Restake kannst Du Dein Guthaben von einem Validator zu einem anderen verschieben, ohne dass Du den Stake aufheben musst. Du bist dabei, Dein gesamtes Guthaben zu staken. Wir empfehlen, einen kleinen Betrag übrig zu lassen, um die Netzwerkgebühren für die Aufhebung des Stakes oder das Einfordern von Prämien abzudecken. + Um mit dem Staking zu beginnen, muss Dein TON-Konto mit einer Selbsttransaktion von 1 TON aktiviert werden. Das Guthaben verbleibt in Deiner Wallet – dieser Schritt aktiviert Dein Konto lediglich für das Staking. + Kontoaktivierung Um mit dem Staking bei TON zu beginnen, führe zunächst eine ausgehende Transaktion in beliebiger Höhe durch – dadurch wird Deine Wallet aktiviert. Für den Abschluss der Transaktion können zusätzlich zur Netzwerkgebühr bis zu 0,2 TON erforderlich sein. Nicht genutzte Beträge werden zurückerstattet. Für diesen Vorgang sind zusätzlich zur Netzwerkgebühr 0,2 TON erforderlich. Bitte lade Dein Guthaben auf. @@ -1166,6 +1200,7 @@ Monatlich Wöchentlich Belohnungen + Belohnungen auf Solana werden automatisch Deinem Staking-Guthaben hinzugefügt und können nicht separat angezeigt werden. Stake gesperrt Mehr staken Beim Staking %1$s wird Dein gesamtes %2$s -Guthaben eingesetzt. Alle weiteren %2$s due Du in Deine Tangem-Wallet einzahlst, werden ebenfalls automatisch eingesetzt. @@ -1205,6 +1240,7 @@ Web 3.0-kompatibel Zum Fortfahren ist eine eingehende Transaktion von mindestens %1$s erforderlich Unzureichende Mittel + Durch die Genehmigung erlaubst Du dem Smart Contract, Deine Token in zukünftigen Transaktionen zu verwenden. Fester Zinssatz Das Netzwerk erhebt eine Token-Genehmigungsgebühr, um zu überprüfen, ob Sie die Verwendung Ihres Tokens für den Swap genehmigen. Tausche mehr Token zu besseren Kursen direkt in deiner Brieftasche. @@ -1237,6 +1273,19 @@ Du erhältst Token auswählen Nicht verfügbar + Daten konnten nicht geladen werden. Versuche es später noch einmal. + Verstecken + Aufdecken + Kartendetails + Karte erhalten + Füge es Deiner Wallet hinzu und bezahle überall mit Deinem Smartphone. + Apple Pay & Google Pay + Verwende Dein USDC-Guthaben, um alltägliche Einkäufe problemlos zu bezahlen. + Einkäufe im Alltag + Deine Kartendaten sind geschützt – volle Kontrolle in der App. + Integrierte Sicherheit + Hol Dir Deine kostenlose Crypto Card \n in wenigen Minuten + Tangem Pay Das ist meine Wallet Guthaben versteckt Angezeigte Salden @@ -1264,6 +1313,7 @@ Token in %%image%% %1$s Netzwerk Der %1$s (%2$s) Token ist die Hauptwährung im %3$s Netzwerk und kann nicht versteckt werden, solange du andere Token dieses Netzwerks in der Liste aktiv hast. %s kann nicht ausgeblendet werden + QR-Code anzeigen Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s. Tausche mit Changelly, %s Gebühren Jetzt tauschen @@ -1312,6 +1362,8 @@ Frühzeitiger Zugriff auf neue Funktionen und exklusive Angebote. Updates zu Funktionen und Neuigkeiten Möchtest du Push-Benachrichtigungen verwenden? + Aktiviere Push-Benachrichtigungen und wir benachrichtigen Dich sofort, wenn Gelder eintreffen \n + Verpasse keine Transaktion Neues Wallet hinzufügen Möchtest Du diese Wallet wirklich entfernen? Es ist ein Fehler aufgetreten, bitte scanne deine Karte oder Ring, um sich anzumelden @@ -1548,6 +1600,7 @@ Wir haben eine Art Problem Alle dApps getrennt Erlaubnis auszugeben + Durch die Genehmigung erlaubst Du dApps oder Smart Contracts, Token in zukünftigen Transaktionen zu verwenden. Vertragsadresse Verbinden Laden @@ -1622,4 +1675,31 @@ 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 + Chart konnte nicht geladen werden... + Der erhaltene Betrag %1$s %2$s wurde nicht auf Aave eingezahlt. + Aktuelle Gebühr + Maximale Gebühr + Gebührenpolitik + Die Netzwerkgebühr ist derzeit zu hoch. Warte, bis sie unter Dein Limit fällt. + Historische Renditen + Sofortige Auszahlung + Wie funktioniert das? + Verdiene %s%% jährlich + Aave • Variabler Zinssatz + Aave + Durchschnitt %s + Renditen des letzten Jahres + Unterstützt durch + Der Zinssatz ist variabel + Beginn des Verdienstes + Siehe Gebührenrichtlinie + Aktiv + Pausiert + Effektiver Jahreszins für Versorgung + Lass Dein Geld arbeiten – verdiene Zinsen auf Dein Guthaben. + Verdienst auf Dein Guthaben + Verdienen %1$s%% pro Jahr + Der Stakingservice ist derzeit nicht verfügbar. Bitte versuche es später erneut. + Einnahmen nicht verfügbar + Chart konnte nicht geladen werden... diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index d76ede5115..f252579b6e 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -19,7 +19,7 @@ 回復する 「 %1$s 」を回復しようとしています。 アカウントを回復する - すでにアクティブアカウント数の上限(20件)を超えています。復元するには1件アーカイブしてください。 + すでにアクティブアカウントの上限(20件)に達しています。復元するには、1つをアーカイブしてください。 アカウントを復元できません アーカイブ済み アカウントを作成できませんでした。しばらくしてからもう一度お試しください。 @@ -176,6 +176,7 @@ 受け入れる アクセスが拒否されました アカウント + 有効化 追加 ポートフォリオに追加 トークンを追加 @@ -248,6 +249,7 @@ 速度と料金 終了 無料 + 送信元 アドレスを同期する はじめる プロバイダーへ移動 @@ -532,6 +534,7 @@ 鍵はアプリに保存されます シードフレーズのバックアップ モバイルウォレットを作成する + 既存のウォレットをインポートする このリカバリーフレーズはすでにインポートされています。 モバイルウォレット アップグレードできません。このデバイスにはすでにウォレットが存在します。 @@ -896,6 +899,7 @@ 残高順 トークンを整理する グループ解除 + %sサポート 詳細はこちら Tangemの通知は設定で有効にできます。 後で有効にする @@ -1089,6 +1093,9 @@ これにより、ウォレットがアプリから削除されます。ウォレットは再度追加できます。 名前 トークンを活用しよう + ネットワーク手数料とは、ブロックチェーン上で取引を処理し、承認するために支払う少額の料金です。 + ステーキングを始めるには、1 TONの自己取引によってTONアカウントを有効化する必要があります。資金はウォレット内にそのまま残ります。これは、ステーキング有効化のためのステップにすぎません。 + アカウントの有効化 ステーキング金額は %s 以上である必要があります ネットワークルールにより、ステーキング金額は%1$s TRX に切り上げられます。 ネットワークルールにより、ステーキング解除の量は%1$s TRX に切り上げられます。 @@ -1154,6 +1161,8 @@ 獲得した報酬をステーキングに再投資し、潜在的な収益を増やします。 再ステーキングを使うと、ステーキングを解除することなく、あるバリデータから別のバリデータに資金を移動できます。 残高のすべてをステーキングしようとしています。ステーキング解除や報酬請求にかかるネットワーク手数料をカバーするために、少額を残しておくことをお勧めします。 + ステーキングを始めるには、TONアカウントを1 TONの自己取引で有効化する必要があります。資金はウォレット内にそのまま残り、この手順でステーキング用にアカウントを有効化できます。 + アカウントの有効化 TONのステーキングを開始するには、まず自分のアドレスに少額の取引を送信します。これによりウォレットが有効になります。 取引を完了するには、ネットワーク手数料に加えて最大0.2TONが必要になる場合があります。未使用分は返金されます。 この操作を続行するには、ネットワーク手数料に加えて0.2 TONが必要です。残高を補充してください。 @@ -1186,6 +1195,7 @@ 毎月 毎週 報酬 + Solanaの報酬は自動的にステーキング残高に追加され、個別に表示することはできません。 ステーキングはロックされています もっとステーキングする %1$sをステーキングすると、 %2$s残高全体がステーキングされます。Tangemウォレットに入金した追加の%2$sも、自動的にステーキングされます。 @@ -1647,6 +1657,13 @@ URIはすでに使用されています WalletConnect 不審な取引 + 業界最高水準のハードウェアウォレット + 迅速な配送 + シンプルな操作 + Tangemでハードウェアウォレットを作成しましょう。銀行のカードのようにスリムで、金庫のように安全です。 + ソフトウェアウォレットを作成またはインポート + モバイルウォレットから始める + その他の方法 破棄 バックアップが中断されました。再開しますか? はい、再開します diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index d3729cd4c9..cf16e56da7 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -1,8 +1,11 @@ Ваш кошелёк не защищён без кода доступа. + Лимит в 20 активных аккаунтов достигнут. Пожалуйста, зархивируйте один аккаунт, чтобы продолжить. + Невозможно восстановить аккаунт Архив Вы архивируете свой аккаунт, но в любое время можете вернуть его обратно + Имя аккаунта уже существует Аккаунт Новый аккаунт Основной аккаунт @@ -118,6 +121,7 @@ Недостаточно ADA Принять Доступ запрещен + Активировать Добавить Добавить в портфель Добавить токен @@ -188,6 +192,7 @@ Медленно Скорость и комиссия Завершить + Из Синхронизировать адреса К провайдеру Перейти в токен @@ -859,6 +864,7 @@ Отправляйте только %s на этот адрес. Использование другой сети может привести к утрате средств. Отправляйте только %1$s в сети %2$s Переводите средства с любого кошелька или биржи + Адрес для начислений Участвовать Не удалось загрузить информацию по реферальной программе. Пожалуйста, попробуйте позже. Не удалось загрузить информацию по реферальной программе. Код ошибки: %s. Пожалуйста, попробуйте позже. @@ -1035,6 +1041,9 @@ Это приведет к удалению кошелька из приложения. Сам кошелек можно добавить снова. Имя Заставьте ваш токен работать + Сетевая комиссия — это небольшая плата за обработку и подтверждение вашей транзакции в блокчейне. + Чтобы начать стейкинг, ваш TON-аккаунт должен быть активирован транзакцией самому себе на 1 TON. Средства остаются в вашем кошельке — этот шаг лишь активирует ваш аккаунт для участия в стейкинге. + Активация аккаунта Сумма для стейкинга должна быть не менее %s Согласно правилам сети, сумма стейкинга будет округлена до %1$sTRX. Сумма для вывода из стейкинга будет округлена до %1$s TRX ввиду особенностей сети. @@ -1098,6 +1107,8 @@ Реинвестируйте свои заработанные награды в вашу застейканную сумму, увеличивая потенциальный доход Рестейк позволяет вам переместить средства из одного валидатора в другого без необходимости выхода из стейкинга. Вы собираетесь застейкать весь баланс, рекомендуем оставить небольшую сумму для оплаты комиссии сети при выходе из стейкинга или получении награды. + Чтобы начать стейкинг, ваш TON-аккаунт должен быть активирован транзакцией самому себе на 1 TON. Средства остаются в вашем кошельке — этот шаг лишь активирует ваш аккаунт для участия в стейкинге. + Активация аккаунта Чтобы начать стейкинг в TON, сначала отправьте небольшую транзакцию на свой же адрес — это активирует ваш кошелёк. До 0.2 TON может потребоваться сверх сетевой комиссии для завершения транзакции. Неиспользованная часть будет возвращена. Для выполнения операции требуется дополнительно 0.2 TON, помимо сетевой комиссии. Пожалуйста, пополните баланс. @@ -1130,6 +1141,7 @@ Месяц Еженедельно Вознаграждения + Вознаграждения в сети Solana автоматически добавляются к вашему стейкинг-балансу и не могут отображаться отдельно. Стейкинг закрыт Застейкать еще При стейкинге %1$s используется весь ваш баланс в %2$s. Любой дополнительный %2$s депозит будет автоматически застейкан. @@ -1169,6 +1181,7 @@ Поддержка Web 3.0 Для отправки требуется входящая транзакция на сумму не менее %1$s Недостаточно средств + Подтверждая, вы разрешаете смарт-контракту использовать ваши токены в будущих транзакциях. Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. Обменивайте больше токенов по лучшим курсам прямо в вашем кошельке. Новый провайдер обмена! @@ -1227,6 +1240,7 @@ Токен в сети %%image%% %1$s Токен %1$s (%2$s) является основной валютой в сети %3$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети Невозможно скрыть %s + Показать QR код Обменивайте этот токен на другие с %1$s комиссии за обслуживание с %2$s по %3$s февраля. Обмен с Changelly, %s комиссии Обменять @@ -1516,6 +1530,7 @@ Изменения в кошельке не обнаружены. Обнаружены потенциальные риски или вредоносное поведение. Подключение или подписание транзакций может привести к потере средств. Известный риск безопасности + Откройте Web3-приложение и выберите опцию WalletConnect. Запрос от Подписать всё равно Тип подписи @@ -1544,4 +1559,13 @@ Нет, отправить все Уменьшить на %s XTZ Чтобы не платить повышенную комиссию при следующем пополнении кошелька, уменьшите сумму на %s XTZ + Невозможно загрузить график + Полученная сумма, %1$s %2$s, не была зачислена на Aave. + Сетевая комиссия сейчас слишком высокая. Ожидаем, пока она упадёт ниже вашего лимита. + Историческая доходность + Проверьте ваше интернет соединение + Годовая доходность + Сервис начисления процентов в данный момент недоступен. Пожалуйста, попробуйте позже. + Данные о доходе недоступны + Невозможно загрузить график 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 e832ab378e..ddc9e58212 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1183,8 +1183,12 @@ Використовуйте %s або відскануйте картку/кільце, щоб отримати доступ до свого гаманця Не вдалося встановити з\'єднання: Цей dApp використовує Wallet Connect версії 1.0, яка не підтримується. Будь ласка, переконайтеся, що dApp підтримує Wallet Connect версії 2.0 для успішного підключення. Будьте в курсі останніх функцій та новин + Миттєві сповіщення про транзакції, обміни та важливі оновлення. + Сповіщення про транзакції Отримуйте сповіщення про вхідні транзакції Дізнавайтеся першими про нові акції + Ранній доступ до нових функцій та ексклюзивних пропозицій. + Нові функції та важливі новини Бажаєте використовувати Push-повідомлення? Додати новий гаманець Ви впевнені, що хочете видалити цей гаманець? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 9987d7f342..6479a28a3f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -19,7 +19,7 @@ Recover You’re about to recover “%1$s”. Recover account - You have already exceeded the limit of 20 active accounts. Archive one to recover + You have already exceeded the limit of 20 active accounts. Archive one to recover. Can\'t recover account Archived We couldn’t create account. Please try again later. @@ -355,6 +355,8 @@ Yes Contract address copied! Available networks + Derivation of your token matches the derivation of %1$s. Your token will be added to this account. + Derivation belongs to another account. Contract address Contract address is invalid Please select the network @@ -1713,7 +1715,7 @@ No wallet changes detected Potential risks or malicious behavior have been detected. Connecting or signing transactions may lead to loss of funds. Known security risk - Open Web3 app and chose WalletConnect option + Open the Web3 app and choose the WalletConnect option. Request from Sign anyway Signature Type @@ -1729,6 +1731,13 @@ URI already used WalletConnect Suspicious transaction + Best in class hardware wallet + Fast delivery + Simple to use + Create a hardware wallet with Tangem. Slim as a bank card, secure as a bank vault. + Create or import a software wallet + Start with Mobile Wallet + Other method Discard You have an interrupted backup. Do you want to resume? Yes, resume diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/SingleAccountStatusListSupplier.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/SingleAccountStatusListSupplier.kt index 5e5ef8f506..6177e2cde5 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/SingleAccountStatusListSupplier.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/supplier/SingleAccountStatusListSupplier.kt @@ -3,6 +3,8 @@ package com.tangem.domain.account.status.supplier import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow /** * Supplier that provides a single [AccountStatusList] for a specific user wallet. @@ -12,4 +14,10 @@ import com.tangem.domain.core.flow.FlowCachingSupplier abstract class SingleAccountStatusListSupplier( override val factory: SingleAccountStatusListProducer.Factory, override val keyCreator: (SingleAccountStatusListProducer.Params) -> String, -) : FlowCachingSupplier() \ No newline at end of file +) : FlowCachingSupplier() { + + operator fun invoke(userWalletId: UserWalletId): Flow { + val params = SingleAccountStatusListProducer.Params(userWalletId) + return this.invoke(params) + } +} \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt b/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt deleted file mode 100644 index 90378e19c2..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/tokens/TokensAction.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.domain.tokens - -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token -import org.rekotlin.Action - -sealed interface TokensAction : Action { - - /** Single way to pass data to the screen */ - sealed interface SetArgs : TokensAction { - object ManageAccess : SetArgs - object ReadAccess : SetArgs - } -} - -data class TokenWithBlockchain(val token: Token, val blockchain: Blockchain) \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt index 2c0a24413a..2cf339e309 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/AccountDetailsModel.kt @@ -13,7 +13,10 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.account.usecase.ArchiveCryptoPortfolioUseCase +import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account +import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.account.AccountDetailsComponent import com.tangem.features.account.createedit.entity.AccountCreateEditUMBuilder.Companion.portfolioIcon import com.tangem.features.account.details.entity.AccountDetailsUM @@ -30,6 +33,7 @@ internal class AccountDetailsModel @Inject constructor( private val router: Router, override val dispatchers: CoroutineDispatcherProvider, private val archiveCryptoPortfolioUseCase: ArchiveCryptoPortfolioUseCase, + private val getUserWalletUseCase: GetUserWalletUseCase, ) : Model() { private val params = paramsContainer.require() @@ -42,8 +46,11 @@ internal class AccountDetailsModel @Inject constructor( } private fun onManageTokensClick() { - // todo account add account param - router.push(AppRoute.ManageTokens(source = AppRoute.ManageTokens.Source.SETTINGS)) + val route = AppRoute.ManageTokens( + source = AppRoute.ManageTokens.Source.SETTINGS, + portfolioId = PortfolioId(params.account.accountId), + ) + router.push(route) } private fun onArchiveAccountClick() { @@ -89,6 +96,8 @@ internal class AccountDetailsModel @Inject constructor( ) } } + val isMultiCurrency = getUserWalletUseCase(params.account.accountId.userWalletId) + .getOrNull()?.isMultiCurrency ?: false return AccountDetailsUM( accountName = params.account.accountName.toUM().value, accountIcon = params.account.portfolioIcon.toUM(), @@ -96,6 +105,7 @@ internal class AccountDetailsModel @Inject constructor( onAccountEditClick = ::onEditAccountClick, onManageTokensClick = ::onManageTokensClick, archiveMode = archiveMode, + isManageTokensAvailable = isMultiCurrency, ) } } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt index 3938e757fb..27ad494553 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/entity/AccountDetailsUM.kt @@ -7,6 +7,7 @@ internal data class AccountDetailsUM( val accountName: TextReference, val accountIcon: CryptoPortfolioIconUM, val archiveMode: ArchiveMode, + val isManageTokensAvailable: Boolean, val onCloseClick: () -> Unit, val onAccountEditClick: () -> Unit, val onManageTokensClick: () -> Unit, diff --git a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt index bd4a0e319f..e3708f495f 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/details/ui/AccountDetailsContent.kt @@ -22,7 +22,6 @@ import com.tangem.common.ui.R import com.tangem.common.ui.account.AccountIconPreviewData import com.tangem.common.ui.account.AccountRow import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH16 import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig @@ -49,6 +48,7 @@ internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier = ) Column( + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .fillMaxSize() .padding(horizontal = TangemTheme.dimens.spacing16) @@ -61,21 +61,22 @@ internal fun AccountDetailsContent(state: AccountDetailsUM, modifier: Modifier = style = TangemTheme.typography.h1, color = TangemTheme.colors.text.primary1, ) - SpacerH16() AccountRow(state) - SpacerH16() - ManageTokensRow(state) + if (state.isManageTokensAvailable) { + ManageTokensRow(state) + } when (state.archiveMode) { is AccountDetailsUM.ArchiveMode.Available -> { - SpacerH16() - ArchiveAccountRow(state.archiveMode) - SpacerH(8.dp) - Text( - modifier = Modifier.padding(horizontal = 12.dp), - text = stringResourceSafe(R.string.account_details_archive_description), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) + Column { + ArchiveAccountRow(state.archiveMode) + SpacerH(8.dp) + Text( + modifier = Modifier.padding(horizontal = 12.dp), + text = stringResourceSafe(R.string.account_details_archive_description), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } } AccountDetailsUM.ArchiveMode.None -> Unit } @@ -186,10 +187,12 @@ private class PreviewStateProvider : CollectionPreviewParameterProvider } \ No newline at end of file diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt index 3c34dd0f34..bdd3cdbc10 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt @@ -17,6 +17,13 @@ sealed interface ManageTokensMode { } sealed interface AddCustomTokenMode { - data class Wallet(val userWalletId: UserWalletId) : AddCustomTokenMode + + val userWalletId: UserWalletId + get() = when (this) { + is Account -> accountId.userWalletId + is Wallet -> userWalletId + } + + data class Wallet(override val userWalletId: UserWalletId) : AddCustomTokenMode data class Account(val accountId: AccountId) : AddCustomTokenMode } \ No newline at end of file diff --git a/features/manage-tokens/impl/build.gradle.kts b/features/manage-tokens/impl/build.gradle.kts index 0506848d40..58cffdfd57 100644 --- a/features/manage-tokens/impl/build.gradle.kts +++ b/features/manage-tokens/impl/build.gradle.kts @@ -25,6 +25,8 @@ dependencies { implementation(projects.common.ui) /* Project - Domain */ + implementation(projects.domain.account.status) + implementation(projects.domain.account) implementation(projects.domain.card) implementation(projects.domain.legacy) implementation(projects.domain.manageTokens) @@ -35,6 +37,14 @@ dependencies { implementation(projects.domain.swap.models) implementation(projects.domain.notifications) + // region Project - Libs + implementation(projects.libs.crypto) + // endregion + + // region Tangem SDKs + implementation(tangemDeps.blockchain) + // endregion + /* AndroidX */ implementation(deps.androidx.activity.compose) implementation(deps.lifecycle.compose) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt index 56323dcd9a..13fca6567a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensUM.kt @@ -54,6 +54,7 @@ internal sealed class ManageTokensUM { isSavingInProgress: Boolean = this is ManageContent && this.isSavingInProgress, scrollToTop: StateEvent = this.scrollToTop, needToInteractWithColdWallet: Boolean = this is ManageContent && this.needToInteractWithColdWallet, + topBar: ManageTokensTopBarUM? = this.topBar, ): ManageTokensUM { return when (this) { is ManageContent -> copy( @@ -65,6 +66,7 @@ internal sealed class ManageTokensUM { isSavingInProgress = isSavingInProgress, scrollToTop = scrollToTop, needToInteractWithColdWallet = needToInteractWithColdWallet, + topBar = topBar, ) is ReadContent -> copy( search = search, @@ -72,6 +74,7 @@ internal sealed class ManageTokensUM { isInitialBatchLoading = isInitialBatchLoading, isNextBatchLoading = isNextBatchLoading, scrollToTop = scrollToTop, + topBar = topBar, ) } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt index 3bb119e580..cadcf9f356 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt @@ -3,13 +3,22 @@ package com.tangem.features.managetokens.model import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.blockchain.common.Blockchain +import com.tangem.common.ui.account.toUM 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.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.managetokens.GetSupportedNetworksUseCase +import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.network.Network import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.CustomTokenSelectorComponent @@ -25,10 +34,12 @@ import com.tangem.features.managetokens.entity.item.SelectableItemUM import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.utils.mapper.toCurrencyNetworkModel import com.tangem.features.managetokens.utils.mapper.toDerivationPathModel +import com.tangem.lib.crypto.derivation.AccountNodeRecognizer import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -38,6 +49,8 @@ internal class CustomTokenSelectorModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase, private val messageSender: UiMessageSender, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val accountsFeatureToggles: AccountsFeatureToggles, paramsContainer: ParamsContainer, ) : Model() { @@ -146,9 +159,8 @@ internal class CustomTokenSelectorModel @Inject constructor( return derivationPaths } - private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List = when (mode) { - is AddCustomTokenMode.Account -> TODO("Account") - is AddCustomTokenMode.Wallet -> getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e -> + private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List { + return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e -> val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)) messageSender.send(message) @@ -168,7 +180,60 @@ internal class CustomTokenSelectorModel @Inject constructor( fun selectCustomDerivationPath(value: SelectedDerivationPath) { when (params) { is NetworkSelector -> return - is DerivationPathSelector -> params.onDerivationPathSelected(value) + is DerivationPathSelector -> if (accountsFeatureToggles.isFeatureEnabled) { + params.checkAccountDerivation(value) + } else { + params.onDerivationPathSelected(value) + } } } + + private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) = + modelScope.launch { + val accountName = derivationPath.id + ?.let { Blockchain.fromId(it.rawId.value) }?.let(::AccountNodeRecognizer) + ?.let { recognizer -> derivationPath.value.value?.let { recognizer.recognize(it) } } + ?.let { accountNode -> + fun AccountStatus.CryptoPortfolio.sameNodeAndNotMain() = !this.account.isMainAccount && + this.account.derivationIndex.value.toLong() == accountNode + + val accounts = singleAccountStatusListSupplier(mode.userWalletId) + .first().accountStatuses + val account = accounts.find { + when (it) { + is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain() + } + } + val accountName = when (account) { + is AccountStatus.CryptoPortfolio -> account.account.accountName.toUM() + null -> null + } + accountName + } + + if (accountName == null) { + onDerivationPathSelected(derivationPath) + } else { + showAccountNameExist( + accountName = accountName.value, + onClick = { onDerivationPathSelected(derivationPath) }, + ) + } + } + + private fun showAccountNameExist(accountName: TextReference, onClick: () -> Unit) { + val firstAction = EventMessageAction( + title = resourceReference(R.string.common_got_it), + onClick = onClick, + ) + val dialogMessage = DialogMessage( + title = resourceReference(R.string.custom_token_another_account_dialog_title), + message = resourceReference( + R.string.custom_token_another_account_dialog_description, + wrappedList(accountName), + ), + firstActionBuilder = { firstAction }, + ) + messageSender.send(dialogMessage) + } } \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index c34a6ba32d..74ee2da6c5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -17,6 +17,7 @@ import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensComponent @@ -40,13 +41,14 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @ModelScoped internal class ManageTokensModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val messageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, manageTokensListManagerFactory: ManageTokensListManager.Factory, manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, paramsContainer: ParamsContainer, @@ -86,6 +88,7 @@ internal class ManageTokensModel @Inject constructor( modelScope.launch { manageTokensListManager.launchPagination(isCollapsed = true) } + checkIsSupportAddCustomTokens() } fun reloadList() { @@ -105,16 +108,34 @@ internal class ManageTokensModel @Inject constructor( } } + private fun getTopBarInitialState(): ManageTokensTopBarUM = when (params.mode) { + is ManageTokensMode.Wallet -> manageContentTopBar() + is ManageTokensMode.Account -> ManageTokensTopBarUM.ReadContent( + title = resourceReference(id = R.string.main_manage_tokens), + onBackButtonClick = router::pop, + ) + ManageTokensMode.None -> ManageTokensTopBarUM.ReadContent( + title = resourceReference(R.string.common_search_tokens), + onBackButtonClick = router::pop, + ) + } + + private fun manageContentTopBar() = ManageTokensTopBarUM.ManageContent( + title = resourceReference(id = R.string.main_manage_tokens), + onBackButtonClick = router::pop, + endButton = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_plus_24, + onClicked = ::navigateToAddCustomToken, + ), + ) + private fun createReadContentModel(): ManageTokensUM.ReadContent { return ManageTokensUM.ReadContent( popBack = router::pop, isInitialBatchLoading = true, isNextBatchLoading = false, items = getLoadingItems(), - topBar = ManageTokensTopBarUM.ReadContent( - title = resourceReference(R.string.common_search_tokens), - onBackButtonClick = router::pop, - ), + topBar = getTopBarInitialState(), search = SearchBarUM( placeholderText = resourceReference(R.string.common_search), query = "", @@ -132,14 +153,7 @@ internal class ManageTokensModel @Inject constructor( isInitialBatchLoading = true, isNextBatchLoading = false, items = getLoadingItems(), - topBar = ManageTokensTopBarUM.ManageContent( - title = resourceReference(id = R.string.main_manage_tokens), - onBackButtonClick = router::pop, - endButton = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_plus_24, - onClicked = ::navigateToAddCustomToken, - ), - ), + topBar = getTopBarInitialState(), search = SearchBarUM( placeholderText = resourceReference(R.string.common_search), query = "", @@ -174,6 +188,20 @@ internal class ManageTokensModel @Inject constructor( .launchIn(modelScope) } + private fun checkIsSupportAddCustomTokens() { + when (val mode = params.mode) { + is ManageTokensMode.Account -> modelScope.launch { + val mainAccount = singleAccountStatusListSupplier(mode.accountId.userWalletId).first().mainAccount + if (mode.accountId == mainAccount.account.accountId) { + state.update { it.copySealed(topBar = manageContentTopBar()) } + } + } + ManageTokensMode.None, + is ManageTokensMode.Wallet, + -> Unit // use init state + } + } + private fun updateItems(items: ImmutableList) { val updatedState = state.updateAndGet { state -> state.copySealed( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt index e7426ae87c..2b20edf084 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/ManageTokensScreen.kt @@ -57,6 +57,7 @@ import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.managetokens.component.ManageTokensComponent +import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.component.ManageTokensSource import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent import com.tangem.features.managetokens.entity.item.CurrencyItemUM @@ -442,20 +443,23 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider, ): PersistentList { + val accountsFeatureEnabled = accountsFeatureToggles.isFeatureEnabled val isMultiCurrency = when (userWallet) { is UserWallet.Cold -> userWallet.isMultiCurrency is UserWallet.Hot -> true @@ -188,7 +191,7 @@ internal class WalletSettingsModel @Inject constructor( is UserWallet.Cold -> userWallet.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup is UserWallet.Hot -> false }, - isManageTokensAvailable = isMultiCurrency, + isManageTokensAvailable = !accountsFeatureEnabled && isMultiCurrency, isNFTFeatureEnabled = isMultiCurrency, isNFTEnabled = isNFTEnabled, onCheckedNFTChange = ::onCheckedNFTChange, diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt index 6d2bf36754..e3caaf89d5 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/ItemsBuilder.kt @@ -9,6 +9,7 @@ import com.tangem.core.ui.components.block.model.BlockUM import com.tangem.core.ui.components.label.entity.LabelStyle import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.walletsettings.analytics.Settings import com.tangem.feature.walletsettings.entity.WalletSettingsAccountsUM @@ -207,7 +208,7 @@ internal class ItemsBuilder @Inject constructor( iconRes = R.drawable.ic_tether_24, onClick = { analyticsEventHandler.send(Settings.ButtonManageTokens) - router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId)) + router.push(AppRoute.ManageTokens(Source.SETTINGS, PortfolioId(userWalletId))) }, ).let(::add) } 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 a9e2270a44..39e5b37251 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 @@ -10,11 +10,9 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked 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 import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.TokensAction import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -39,8 +37,6 @@ internal interface WalletContentClickIntents { fun onDetailsClick() - fun onManageTokensClick() - fun onOrganizeTokensClick() fun onDismissMarketsOnboarding() @@ -81,7 +77,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val shouldShowMarketsTooltipUseCase: ShouldShowMarketsTooltipUseCase, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: ReduxStateHolder, private val walletEventSender: WalletEventSender, private val analyticsEventHandler: AnalyticsEventHandler, private val hotWalletFeatureToggles: HotWalletFeatureToggles, @@ -122,11 +117,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } - override fun onManageTokensClick() { - reduxStateHolder.dispatch(action = TokensAction.SetArgs.ManageAccess) - router.openManageTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) - } - override fun onOrganizeTokensClick() { router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) } 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 1bab1f8a28..f2f41882cc 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 @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.router import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.tangem.common.routing.AppRoute -import com.tangem.common.routing.AppRoute.ManageTokens.Source import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.navigation.url.UrlOpener @@ -86,10 +85,6 @@ internal class DefaultWalletRouter @Inject constructor( return router.stack.lastOrNull() is AppRoute.Wallet } - override fun openManageTokensScreen(userWalletId: UserWalletId) { - router.push(AppRoute.ManageTokens(Source.SETTINGS, userWalletId)) - } - override fun openScanFailedDialog(onTryAgain: () -> Unit) { reduxStateHolder.dispatchDialogShow(StateDialog.ScanFailsDialog(StateDialog.ScanFailsSource.MAIN, onTryAgain)) } 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 106cafa945..4c6c7ba002 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 @@ -49,9 +49,6 @@ internal interface InnerWalletRouter { /** Is wallet last screen */ fun isWalletLastScreen(): Boolean - /** Open manage tokens screen */ - fun openManageTokensScreen(userWalletId: UserWalletId) - /** Open scan failed dialog */ fun openScanFailedDialog(onTryAgain: () -> Unit)