From 8ae1c2294f56f9237db1691828c4f3e73bf3dcee Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 21:10:21 +0100 Subject: [PATCH 01/97] Updated on 2026-08-14 --- .../com/tangem/features/feed/model/earn/EarnModel.kt | 2 ++ .../UpdateMostlyUsedStateLoadingTransformer.kt | 11 +++++++++++ .../com/tangem/features/feed/ui/earn/EarnContent.kt | 12 +++++++----- 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index bb91b6a704..49defaa8e5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -35,6 +35,7 @@ import com.tangem.features.feed.model.earn.state.EarnStateController import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateLoadingTransformer import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager @@ -173,6 +174,7 @@ internal class EarnModel @Inject constructor( private fun fetchTopEarnTokens() { modelScope.launch(dispatchers.default) { + stateController.update(UpdateMostlyUsedStateLoadingTransformer()) fetchTopEarnTokensUseCase() } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt new file mode 100644 index 0000000000..e1fcb79a35 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/UpdateMostlyUsedStateLoadingTransformer.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.model.earn.state.transformers + +import com.tangem.features.feed.ui.earn.state.EarnListUM +import com.tangem.features.feed.ui.earn.state.EarnUM + +internal class UpdateMostlyUsedStateLoadingTransformer : EarnUMTransformer { + + override fun transform(prevState: EarnUM): EarnUM { + return prevState.copy(mostlyUsed = EarnListUM.Loading) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 0b7845919d..82175085d2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -151,10 +151,12 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { Box( modifier = Modifier .fillMaxWidth() - .padding( - horizontal = 16.dp, - vertical = 12.dp, - ), + .padding(horizontal = 16.dp, vertical = 12.dp) + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(vertical = 32.dp, horizontal = 12.dp), contentAlignment = Alignment.Center, ) { UnableToLoadData(onRetryClick = st.onRetryClicked) @@ -496,7 +498,7 @@ private fun EarnContentLoadingPreview() { ) { EarnContent( state = previewEarnUM( - mostlyUsed = EarnListUM.Loading, + mostlyUsed = EarnListUM.Error(onRetryClicked = {}), bestOpportunities = EarnBestOpportunitiesUM.Loading, ), ) From ff565f938320f0f14d4ca5268d66484ae73a44df Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Feb 2026 14:07:02 +0300 Subject: [PATCH 02/97] Updated on 2026-08-14 --- features/onramp/impl/build.gradle.kts | 1 + .../swap/DefaultSwapSelectTokensComponent.kt | 31 ++++++ .../AvailableSwapPairsComponent.kt | 8 ++ .../DefaultAvailableSwapPairsComponent.kt | 8 ++ .../market/SwapMarketsListBatchFlowManager.kt | 7 ++ .../model/AddToPortfolioRoute.kt | 7 ++ .../model/AvailableSwapPairsModel.kt | 103 +++++++++++++++++- 7 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index d836fce188..f0a6cb16c5 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -19,6 +19,7 @@ dependencies { implementation(projects.features.swap.domain) implementation(projects.features.swap.domain.api) implementation(projects.features.swap.domain.models) + implementation(projects.features.feed.api) /** Project - Core */ implementation(projects.core.analytics) 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 d9b66c559d..ee2dc2dd5b 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 @@ -5,13 +5,20 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.onramp.component.SwapSelectTokensComponent import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent +import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.swap.model.SwapSelectTokensModel import com.tangem.features.onramp.swap.ui.SwapSelectTokens import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent @@ -25,6 +32,7 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( tokenListComponentFactory: OnrampTokenListComponent.Factory, availableSwapPairsComponentFactory: AvailableSwapPairsComponent.Factory, analyticsEventHandler: AnalyticsEventHandler, + private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, @Assisted private val appComponentContext: AppComponentContext, @Assisted private val params: SwapSelectTokensComponent.Params, ) : AppComponentContext by appComponentContext, SwapSelectTokensComponent { @@ -49,15 +57,36 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( ), ) + private val bottomSheetSlot = childSlot( + source = selectToTokenListComponent.bottomSheetNavigation, + serializer = AddToPortfolioRoute.serializer(), + key = "add_to_portfolio_bottom_sheet", + handleBackButton = false, + childFactory = { _, context -> bottomSheetChild(context) }, + ) + init { analyticsEventHandler.send(event = MainScreenAnalyticsEvent.SwapScreenOpened()) } + @Suppress("UnsafeCallOnNullableType") + private fun bottomSheetChild(componentContext: ComponentContext): ComposableBottomSheetComponent { + return addToPortfolioComponentFactory.create( + context = childByContext(componentContext), + params = AddToPortfolioComponent.Params( + addToPortfolioManager = selectToTokenListComponent.addToPortfolioManager!!, + callback = selectToTokenListComponent.addToPortfolioCallback, + shouldSkipTokenActionsScreen = true, + ), + ) + } + @Composable override fun Content(modifier: Modifier) { val state by model.state.collectAsStateWithLifecycle() val fromTokensState by selectFromTokenListComponent.uiState.collectAsStateWithLifecycle() val toTokensState by selectToTokenListComponent.uiState.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() SwapSelectTokens( state = state, @@ -67,6 +96,8 @@ internal class DefaultSwapSelectTokensComponent @AssistedInject constructor( selectToTokenListState = toTokensState, modifier = modifier, ) + + bottomSheet.child?.instance?.BottomSheet() } @AssistedFactory 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 cb87a50dc2..bae0b5ba51 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 @@ -1,11 +1,15 @@ package com.tangem.features.onramp.swap.availablepairs import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.components.token.state.TokenItemState 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.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.tokenlist.entity.TokenListUM import kotlinx.coroutines.flow.StateFlow @@ -13,6 +17,10 @@ import kotlinx.coroutines.flow.StateFlow @Stable internal interface AvailableSwapPairsComponent : ComposableListContentComponent { + val bottomSheetNavigation: SlotNavigation + val addToPortfolioManager: AddToPortfolioManager? + val addToPortfolioCallback: AddToPortfolioComponent.Callback + /** 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 456b6e8c30..27e528cd04 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 @@ -3,8 +3,12 @@ package com.tangem.features.onramp.swap.availablepairs import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier +import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.swap.availablepairs.model.AvailableSwapPairsModel import com.tangem.features.onramp.tokenlist.entity.TokenListUM import com.tangem.features.onramp.tokenlist.ui.onrampSwapTokenList @@ -21,6 +25,10 @@ internal class DefaultAvailableSwapPairsComponent @AssistedInject constructor( private val model: AvailableSwapPairsModel = getOrCreateModel(params) + override val bottomSheetNavigation: SlotNavigation get() = model.bottomSheetNavigation + override val addToPortfolioManager: AddToPortfolioManager? get() = model.addToPortfolioManager + override val addToPortfolioCallback: AddToPortfolioComponent.Callback get() = model.addToPortfolioCallback + override val uiState: StateFlow get() = model.state diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt index 2aecafa4e4..8d9b34c5b8 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt @@ -228,6 +228,13 @@ internal class SwapMarketsListBatchFlowManager( } } + fun getTokenMarketById(id: CryptoCurrency.RawID): TokenMarket? { + return batchFlow.state.value.data + .asSequence() + .flatMap { it.data } + .firstOrNull { it.id == id } + } + fun getBatchKeysByItemIds(ids: List): Set { val currentData = batchFlow.state.value.data diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt new file mode 100644 index 0000000000..559ef6eb09 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AddToPortfolioRoute.kt @@ -0,0 +1,7 @@ +package com.tangem.features.onramp.swap.availablepairs.model + +import com.tangem.core.decompose.navigation.Route +import kotlinx.serialization.Serializable + +@Serializable +internal data object AddToPortfolioRoute : Route \ 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 5466084880..7867772800 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 @@ -9,28 +9,36 @@ 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.status.usecase.GetAccountCurrencyStatusUseCase 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 +import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.core.lce.Lce 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.markets.GetMarketsTokenListFlowUseCase +import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.filterCryptoPortfolio 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.tokens.GetTokenListUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.feature.swap.domain.models.domain.SwapPairLeast +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager import com.tangem.features.onramp.impl.R +import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent import com.tangem.features.swap.SwapFeatureToggles import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer @@ -48,9 +56,17 @@ 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 +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.lib.crypto.BlockchainUtils +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching +import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -71,7 +87,10 @@ internal class AvailableSwapPairsModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val accountsFeatureToggles: AccountsFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - private val swapFeatureToggles: SwapFeatureToggles, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val excludedBlockchains: ExcludedBlockchains, + private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, + swapFeatureToggles: SwapFeatureToggles, getWalletsUseCase: GetWalletsUseCase, ) : Model() { @@ -79,6 +98,17 @@ internal class AvailableSwapPairsModel @Inject constructor( private val params: AvailableSwapPairsComponent.Params = paramsContainer.require() private val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + private val allUserWallets = getWalletsUseCase.invokeSync() + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + var addToPortfolioManager: AddToPortfolioManager? = null + val addToPortfolioCallback: AddToPortfolioComponent.Callback = object : AddToPortfolioComponent.Callback { + override fun onDismiss() = bottomSheetNavigation.dismiss() + override fun onSuccess(addedToken: CryptoCurrency) { + onTokenAddedToPortfolio(addedToken) + } + } + private val addToPortfolioJobHolder = JobHolder() private val tokenListFlow = getTokenListUseCaseFlow() private val accountListFlow = getAccountListUseCaseFlow() @@ -86,6 +116,7 @@ internal class AvailableSwapPairsModel @Inject constructor( private val selectedAppCurrencyFlow: StateFlow = getSelectedAppCurrencyUseCase.invokeOrDefault() .stateIn(scope = modelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default) + private val refreshPairsTrigger = MutableSharedFlow() private val searchQueryStateForMarkets = MutableStateFlow("") private val visibleMarketItemIds = MutableStateFlow>(emptyList()) @@ -374,8 +405,12 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun subscribeOnAvailablePairsUpdates() { modelScope.launch { - params.selectedStatus - .filterNotNull() + combine( + params.selectedStatus.filterNotNull(), + refreshPairsTrigger + .onEach { availablePairsByNetworkFlow.value = emptyMap() } + .onStart { emit(Unit) }, + ) { status, _ -> status } .collectLatest { selectedStatus -> val networkInfo = selectedStatus.toLeastTokenInfo() @@ -542,7 +577,7 @@ internal class AvailableSwapPairsModel @Inject constructor( else -> SwapMarketState.Content( items = uiItems, loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = {}, + onItemClick = { item -> addToPortfolioItem(item) }, visibleIdsChanged = { visibleMarketItemIds.value = it }, total = total ?: uiItems.size, ) @@ -556,6 +591,66 @@ internal class AvailableSwapPairsModel @Inject constructor( .launchIn(modelScope) } + private fun onTokenAddedToPortfolio(addedToken: CryptoCurrency) { + modelScope.launch { + bottomSheetNavigation.dismiss() + + // Trigger re-fetch of available pairs (clears cache + re-enters collectLatest) + refreshPairsTrigger.emit(Unit) + + // Wait for the added token status to become Loaded + val addedTokenStatus = getAccountCurrencyStatusUseCase(params.userWalletId, addedToken) + .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded } + ?.status + ?: return@launch + + // Convert to TokenItemState and trigger token selection → navigates to swap + val converter = OnrampTokenItemStateConverterFactory.createAvailableItemConverter( + appCurrency = selectedAppCurrencyFlow.value, + onItemClick = params.onTokenClick, + ) + params.onTokenClick(converter.convert(addedTokenStatus), addedTokenStatus) + } + } + + private fun addToPortfolioItem(item: MarketsListItemUM) { + modelScope.launch { + val tokenMarket = searchMarketsListManager.getTokenMarketById(item.id) ?: return@launch + + val param = tokenMarket.toSerializableParam() + val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot } + + val networks = tokenMarket.networks?.filter { network -> + BlockchainUtils.isSupportedNetworkId( + blockchainId = network.networkId, + excludedBlockchains = excludedBlockchains, + hotExcludedBlockchains = hotWalletExcludedBlockchains, + hasOnlyHotWallets = hasOnlyHotWallets, + ) + }?.map { network -> + TokenMarketInfo.Network( + networkId = network.networkId, + isExchangeable = false, + contractAddress = network.contractAddress, + decimalCount = network.decimalCount, + ) + }.orEmpty() + + addToPortfolioManager = addToPortfolioManagerFactory + .create( + scope = modelScope, + token = param, + analyticsParams = null, + ).apply { + setTokenNetworks(networks) + } + + addToPortfolioManager?.state + ?.firstOrNull { it is AddToPortfolioManager.State.AvailableToAdd } + ?.run { bottomSheetNavigation.activate(AddToPortfolioRoute) } + }.saveIn(addToPortfolioJobHolder) + } + private fun subscribeOnVisibleMarketItems() { modelScope.launch { visibleMarketItemIds.mapNotNull { rawIds -> From db6f6a2e537dd09a5d8dc38d6d2b1bba785b8098 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Feb 2026 09:54:54 +0100 Subject: [PATCH 03/97] Updated on 2026-08-14 --- .../features/feed/model/earn/EarnModel.kt | 45 ++++++++++--------- .../model/earn/state/EarnStateController.kt | 6 ++- .../EarnFilterSelectedStateTransformer.kt | 13 +++++- ...rnNetworkFilterSelectedStateTransformer.kt | 13 ------ .../features/feed/ui/earn/EarnContent.kt | 38 ++++++++-------- .../feed/ui/earn/state/EarnFilterUM.kt | 11 +++++ .../features/feed/ui/earn/state/EarnUM.kt | 10 +---- .../feed/ui/feed/components/BlockHeader.kt | 28 ++++++++---- .../feed/ui/feed/components/EarnBlock.kt | 4 ++ .../feed/ui/feed/components/MarketsBlock.kt | 16 ++++--- .../feed/ui/feed/components/NewsBlock.kt | 9 ++++ 11 files changed, 114 insertions(+), 79 deletions(-) delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 49defaa8e5..822e18aee3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -70,8 +70,8 @@ internal class EarnModel @Inject constructor( private val earnNetworks = MutableStateFlow(Either.Right(emptyList())) private val earnListConfigProvider = Provider { createEarnTokensListConfig( - selectedTypeFilter = stateController.value.selectedTypeFilter, - selectedNetworkFilter = stateController.value.selectedNetworkFilter, + selectedTypeFilter = stateController.value.earnFilterUM.selectedTypeFilter, + selectedNetworkFilter = stateController.value.earnFilterUM.selectedNetworkFilter, earnNetworks = earnNetworks.value, ) } @@ -118,8 +118,8 @@ internal class EarnModel @Inject constructor( batchFlowManager.initialLoadingError, batchFlowManager.paginationStatus, ) { items, error, paginationStatus -> - val hasActiveFilters = state.value.selectedTypeFilter != EarnFilterTypeUM.All || - state.value.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks + val hasActiveFilters = state.value.earnFilterUM.selectedTypeFilter != EarnFilterTypeUM.All || + state.value.earnFilterUM.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks error?.let(::handleBestOpportunitiesErrorAnalytics) EarnListStateManager.calculateState( items = items, @@ -157,18 +157,21 @@ internal class EarnModel @Inject constructor( private fun subscribeOnStoredFilters() { modelScope.launch(dispatchers.default) { - getEarnFilterUseCase() - .collect { filter -> - val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) - val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) - stateController.update( - EarnFilterSelectedStateTransformer( - filterType = typeFilterUM, - filterNetwork = networkFilterUM, - ), - ) - batchFlowManager.reload() - } + combine( + getEarnFilterUseCase(), + earnNetworks, + ) { filter, networks -> + val typeFilterUM = EarnFilterTypeConverter().convert(filter.earnFilterType) + val networkFilterUM = EarnFilterNetworkConverter().convert(filter.earnFilterNetwork) + stateController.update( + EarnFilterSelectedStateTransformer( + filterType = typeFilterUM, + filterNetwork = networkFilterUM, + earnNetworks = networks, + ), + ) + batchFlowManager.reload() + }.collect() } } @@ -191,7 +194,7 @@ internal class EarnModel @Inject constructor( bottomSheetNavigation.activate( FeedBottomSheetRoute.TypeFilter( params = EarnTypeFilterComponent.Params( - selectedFilter = EarnFilterTypeUMConverter().convert(currentState.selectedTypeFilter), + selectedFilter = EarnFilterTypeUMConverter().convert(currentState.earnFilterUM.selectedTypeFilter), onFilterSelected = ::onTypeFilterOptionSelected, onDismiss = { bottomSheetNavigation.dismiss() }, ), @@ -212,7 +215,7 @@ internal class EarnModel @Inject constructor( } private fun createNetworkFilters(): List { - val selectedFilter = state.value.selectedNetworkFilter + val selectedFilter = state.value.earnFilterUM.selectedNetworkFilter return buildList { add( EarnFilterNetwork.AllNetworks( @@ -279,7 +282,9 @@ internal class EarnModel @Inject constructor( modelScope.launch(dispatchers.default) { setEarnFilterUseCase( EarnFilter( - earnFilterNetwork = EarnFilterNetworkUMConverter().convert(state.value.selectedNetworkFilter), + earnFilterNetwork = EarnFilterNetworkUMConverter().convert( + value = state.value.earnFilterUM.selectedNetworkFilter, + ), earnFilterType = type, ), ) @@ -292,7 +297,7 @@ internal class EarnModel @Inject constructor( setEarnFilterUseCase( EarnFilter( earnFilterNetwork = filter, - earnFilterType = EarnFilterTypeUMConverter().convert(state.value.selectedTypeFilter), + earnFilterType = EarnFilterTypeUMConverter().convert(state.value.earnFilterUM.selectedTypeFilter), ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt index 6cec5cd393..f074cbdc66 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/EarnStateController.kt @@ -26,8 +26,10 @@ internal class EarnStateController @Inject constructor() { return EarnUM( mostlyUsed = EarnListUM.Loading, bestOpportunities = EarnBestOpportunitiesUM.Loading, - selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + earnFilterUM = EarnFilterUM( + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + ), onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt index 9bdb205b49..17cd292a95 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt @@ -1,18 +1,27 @@ package com.tangem.features.feed.model.earn.state.transformers +import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnUM internal class EarnFilterSelectedStateTransformer( + private val earnNetworks: EarnNetworks, private val filterType: EarnFilterTypeUM, private val filterNetwork: EarnFilterNetworkUM, ) : EarnUMTransformer { override fun transform(prevState: EarnUM): EarnUM { + val isFiltersApplicable = prevState.bestOpportunities !is EarnBestOpportunitiesUM.Error + val isNetworkFilterEnabled = earnNetworks.isRight() return prevState.copy( - selectedTypeFilter = filterType, - selectedNetworkFilter = filterNetwork, + earnFilterUM = prevState.earnFilterUM.copy( + selectedTypeFilter = filterType, + selectedNetworkFilter = filterNetwork, + isNetworkFilterEnabled = isFiltersApplicable && isNetworkFilterEnabled, + isTypeFilterEnabled = isFiltersApplicable, + ), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt deleted file mode 100644 index a4ae7833a3..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnNetworkFilterSelectedStateTransformer.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.feed.model.earn.state.transformers - -import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM -import com.tangem.features.feed.ui.earn.state.EarnUM - -internal class EarnNetworkFilterSelectedStateTransformer( - private val filter: EarnFilterNetworkUM, -) : EarnUMTransformer { - - override fun transform(prevState: EarnUM): EarnUM { - return prevState.copy(selectedNetworkFilter = filter) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 82175085d2..4939a58ede 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -91,12 +91,7 @@ internal fun EarnContent(state: EarnUM, modifier: Modifier = Modifier) { SpacerH(12.dp) BestOpportunitiesFilters( state = state.bestOpportunities, - selectedNetworkFilterText = when (state.selectedNetworkFilter) { - is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks) - is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks) - is EarnFilterNetworkUM.Network -> TextReference.Str(state.selectedNetworkFilter.text) - }, - selectedTypeFilterText = state.selectedTypeFilterText, + earnFilterUM = state.earnFilterUM, onNetworkFilterClick = state.onNetworkFilterClick, onTypeFilterClick = state.onTypeFilterClick, ) @@ -220,17 +215,14 @@ private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: @Composable private fun BestOpportunitiesFilters( state: EarnBestOpportunitiesUM, - selectedNetworkFilterText: TextReference, - selectedTypeFilterText: TextReference, + earnFilterUM: EarnFilterUM, onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, ) { when (state) { is EarnBestOpportunitiesUM.Loading -> FilterButtonsShimmer() else -> FilterButtons( - selectedNetworkFilterText = selectedNetworkFilterText, - selectedTypeFilterText = selectedTypeFilterText, - isEnabled = state is EarnBestOpportunitiesUM.Content || state is EarnBestOpportunitiesUM.EmptyFiltered, + earnFilterUM = earnFilterUM, onNetworkFilterClick = onNetworkFilterClick, onTypeFilterClick = onTypeFilterClick, ) @@ -309,9 +301,7 @@ private fun LazyListScope.bestOpportunitiesItems(state: EarnBestOpportunitiesUM) @Composable private fun FilterButtons( - selectedNetworkFilterText: TextReference, - selectedTypeFilterText: TextReference, - isEnabled: Boolean, + earnFilterUM: EarnFilterUM, onNetworkFilterClick: () -> Unit, onTypeFilterClick: () -> Unit, modifier: Modifier = Modifier, @@ -321,10 +311,14 @@ private fun FilterButtons( ) { SecondarySmallButton( config = SmallButtonConfig( - text = selectedNetworkFilterText, + text = when (earnFilterUM.selectedNetworkFilter) { + is EarnFilterNetworkUM.AllNetworks -> TextReference.Res(R.string.earn_filter_all_networks) + is EarnFilterNetworkUM.MyNetworks -> TextReference.Res(R.string.earn_filter_my_networks) + is EarnFilterNetworkUM.Network -> TextReference.Str(earnFilterUM.selectedNetworkFilter.text) + }, onClick = onNetworkFilterClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), - isEnabled = isEnabled, + isEnabled = earnFilterUM.isNetworkFilterEnabled, ), ) @@ -332,10 +326,10 @@ private fun FilterButtons( SecondarySmallButton( config = SmallButtonConfig( - text = selectedTypeFilterText, + text = earnFilterUM.selectedTypeFilter.text, onClick = onTypeFilterClick, icon = TangemButtonIconPosition.End(iconResId = R.drawable.ic_chevron_24), - isEnabled = isEnabled, + isEnabled = earnFilterUM.isTypeFilterEnabled, ), ) } @@ -590,8 +584,12 @@ private fun previewEarnUM( ): EarnUM = EarnUM( mostlyUsed = mostlyUsed, bestOpportunities = bestOpportunities, - selectedTypeFilter = EarnFilterTypeUM.All, - selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + earnFilterUM = EarnFilterUM( + selectedTypeFilter = EarnFilterTypeUM.All, + selectedNetworkFilter = EarnFilterNetworkUM.AllNetworks(isSelected = true), + isTypeFilterEnabled = true, + isNetworkFilterEnabled = true, + ), onBackClick = {}, onNetworkFilterClick = {}, onTypeFilterClick = {}, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt new file mode 100644 index 0000000000..3cf3b0e582 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnFilterUM.kt @@ -0,0 +1,11 @@ +package com.tangem.features.feed.ui.earn.state + +import androidx.compose.runtime.Immutable + +@Immutable +internal data class EarnFilterUM( + val selectedTypeFilter: EarnFilterTypeUM, + val selectedNetworkFilter: EarnFilterNetworkUM, + val isTypeFilterEnabled: Boolean = true, + val isNetworkFilterEnabled: Boolean = true, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt index a11f66f48e..974b5a71b2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnUM.kt @@ -1,20 +1,14 @@ package com.tangem.features.feed.ui.earn.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference @Immutable internal data class EarnUM( val mostlyUsed: EarnListUM, val bestOpportunities: EarnBestOpportunitiesUM, - val selectedTypeFilter: EarnFilterTypeUM, - val selectedNetworkFilter: EarnFilterNetworkUM, + val earnFilterUM: EarnFilterUM, val onBackClick: () -> Unit, val onNetworkFilterClick: () -> Unit, val onTypeFilterClick: () -> Unit, val onSliderScroll: () -> Unit, -) { - - val selectedTypeFilterText: TextReference - get() = selectedTypeFilter.text -} \ No newline at end of file +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt index c43c6b6a12..9fd9997c5b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/BlockHeader.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui.feed.components import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -8,12 +9,18 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.extensions.TextReference @Composable -internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title: @Composable () -> Unit) { +internal fun Header( + onSeeAllClick: () -> Unit, + isLoading: Boolean, + shouldShowSeeAll: Boolean, + title: @Composable () -> Unit, +) { AnimatedContent(isLoading) { animatedState -> Row( modifier = Modifier @@ -25,13 +32,18 @@ internal fun Header(onSeeAllClick: () -> Unit, isLoading: Boolean = false, title if (animatedState) { RectangleShimmer(modifier = Modifier.size(width = 104.dp, height = 18.dp)) } else { - title() - SecondarySmallButton( - config = SmallButtonConfig( - text = TextReference.Res(R.string.common_see_all), - onClick = onSeeAllClick, - ), - ) + Box(modifier = Modifier.weight(1f)) { + title() + } + SpacerW(8.dp) + AnimatedVisibility(shouldShowSeeAll) { + SecondarySmallButton( + config = SmallButtonConfig( + text = TextReference.Res(R.string.common_see_all), + onClick = onSeeAllClick, + ), + ) + } } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt index fb70686941..08e6849b20 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.R @@ -33,10 +34,13 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif text = stringResourceSafe(R.string.markets_earn_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = onSeeAllClick, isLoading = earnListUM is EarnListUM.Loading, + shouldShowSeeAll = earnListUM is EarnListUM.Content, ) SpacerH(12.dp) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt index 1aa00e6976..11360bacd7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/MarketsBlock.kt @@ -4,12 +4,7 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -19,6 +14,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.common.ui.markets.MarketsListItem @@ -55,9 +51,13 @@ internal fun MarketBlock(marketChart: MarketChartUM?, feedListCallbacks: FeedLis text = stringResourceSafe(R.string.markets_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = { feedListCallbacks.onMarketOpenClick(SortByTypeUM.Rating) }, + shouldShowSeeAll = currentChart is MarketChartUM.Content, + isLoading = currentChart is MarketChartUM.Loading, ) SpacerH(12.dp) @@ -88,9 +88,13 @@ internal fun MarketPulseBlock(marketChartConfig: MarketChartConfig, feedListCall text = stringResourceSafe(R.string.markets_pulse_common_title), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) }, onSeeAllClick = { onSeeAllClick() }, + shouldShowSeeAll = true, + isLoading = marketChartConfig.marketCharts[marketChartConfig.currentSortByType] is MarketChartUM.Loading, ) LazyRow( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt index e2fa4ecbf6..0acec46dcd 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import com.tangem.common.ui.news.ArticleCard @@ -104,10 +105,14 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, } }, style = TangemTheme.typography.subtitle1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } }, onSeeAllClick = { feedListCallbacks.onOpenAllNews(false) }, + isLoading = news.newsUMState == NewsUMState.LOADING, + shouldShowSeeAll = news.newsUMState == NewsUMState.CONTENT, ) SpacerH(12.dp) @@ -181,10 +186,14 @@ private fun NewsErrorBlock(onRetryClick: () -> Unit) { text = stringResourceSafe(R.string.common_news), style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, + overflow = TextOverflow.Ellipsis, + maxLines = 1, ) } }, onSeeAllClick = {}, + shouldShowSeeAll = false, + isLoading = false, ) SpacerH(12.dp) BlockCard( From e49dc700ac495c9e34c960f399a93726bd411021 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Feb 2026 12:25:11 +0100 Subject: [PATCH 04/97] Updated on 2026-08-14 --- .../tangem/tap/di/domain/EarnDomainModule.kt | 11 ++--- domain/earn/build.gradle.kts | 2 +- .../earn/usecase/GetEarnNetworksUseCase.kt | 47 +++++++------------ 3 files changed, 23 insertions(+), 37 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt index 3ae0f6d5c6..bc3f3f9376 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt @@ -1,9 +1,8 @@ package com.tangem.tap.di.domain -import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.earn.usecase.* -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -19,15 +18,13 @@ object EarnDomainModule { } @Provides - fun provideManageEarnNetworksUseCase( + fun provideGetEarnNetworksUseCase( earnRepository: EarnRepository, - userWalletsListRepository: UserWalletsListRepository, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + multiAccountListSupplier: MultiAccountListSupplier, ): GetEarnNetworksUseCase { return GetEarnNetworksUseCase( earnRepository = earnRepository, - userWalletsListRepository = userWalletsListRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, + multiAccountListSupplier = multiAccountListSupplier, ) } diff --git a/domain/earn/build.gradle.kts b/domain/earn/build.gradle.kts index f72d726fea..380db663f4 100644 --- a/domain/earn/build.gradle.kts +++ b/domain/earn/build.gradle.kts @@ -8,7 +8,7 @@ dependencies { api(projects.domain.core) api(projects.domain.models) api(projects.core.pagination) + implementation(projects.domain.account) implementation(projects.domain.common) - implementation(projects.domain.networks) implementation(deps.kotlin.serialization) } \ No newline at end of file diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt index 1988d0e166..4fc006ec5c 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt @@ -1,26 +1,26 @@ package com.tangem.domain.earn.usecase import arrow.core.Either -import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.account.models.AccountList +import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.models.earn.EarnNetwork import com.tangem.domain.models.earn.EarnNetworks -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.isLocked -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.networks.multi.MultiNetworkStatusProducer -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map /** - * Observes earn networks with [EarnNetwork.isAdded] enriched from user's wallets - * via [multiNetworkStatusSupplier]. Single entry point for all/mine filtering. + * Observes earn networks with [EarnNetwork.isAdded] enriched from user's active (non-archived) + * accounts via [multiAccountListSupplier]. Single entry point for all/mine filtering. + * + * Uses [MultiAccountListSupplier] so that only networks from active accounts are considered; + * archived accounts are not included in [AccountList.accounts]. */ class GetEarnNetworksUseCase( private val earnRepository: EarnRepository, - private val userWalletsListRepository: UserWalletsListRepository, - private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, + private val multiAccountListSupplier: MultiAccountListSupplier, ) { operator fun invoke(): Flow { @@ -36,24 +36,13 @@ class GetEarnNetworksUseCase( }.distinctUntilChanged() } - @OptIn(ExperimentalCoroutinesApi::class) private fun observeMyNetworkIds(): Flow> { - return userWalletsListRepository.userWallets - .map { it.orEmpty() } - .flatMapLatest { wallets -> - val activeWallets = wallets - .filterNot(UserWallet::isLocked) - .filter(UserWallet::isMultiCurrency) - if (activeWallets.isEmpty()) { - flowOf(emptySet()) - } else { - val flows = activeWallets.map { wallet -> - multiNetworkStatusSupplier( - MultiNetworkStatusProducer.Params(userWalletId = wallet.walletId), - ).map { statuses -> statuses.map { it.network.backendId }.toSet() } - } - combine(flows) { arrays -> arrays.flatMap { it }.toSet() } - } + return multiAccountListSupplier() + .map { accountLists -> + accountLists + .flatMap(AccountList::flattenCurrencies) + .map { it.network.backendId } + .toSet() } } } \ No newline at end of file From 9d273fb55632173c8ca1403a3e31ec09ffe0a818 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Feb 2026 16:29:23 +0500 Subject: [PATCH 05/97] Updated on 2026-08-14 --- ...ccountCryptoPortfolioItemStateConverter.kt | 6 +- .../account/AccountIconItemStateConverter.kt | 7 +- .../common/ui/notifications/Notifications.kt | 119 +++++- .../tangem/core/ui/ds/TangemPagerIndicator.kt | 370 ++++++++++++++++++ .../bigdecimal/BigDecimalCryptoFormat.kt | 87 ++++ .../format/bigdecimal/BigDecimalFiatFormat.kt | 59 ++- .../ui/format/bigdecimal/BigDecimalFormat.kt | 28 +- .../tangem/core/ui/res/TangemColorPalette.kt | 1 + .../tangem/core/ui/res/TangemThemeRedesign.kt | 8 +- 9 files changed, 673 insertions(+), 12 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt index 943fb98735..1955e4b9a0 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountCryptoPortfolioItemStateConverter.kt @@ -47,7 +47,7 @@ class AccountCryptoPortfolioItemStateConverter( ) return TokenItemState.Content( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(this), + iconState = AccountIconItemStateConverter().convert(this), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), @@ -73,7 +73,7 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToLoadingState(): TokenItemState.Content { return TokenItemState.Content( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(account), + iconState = AccountIconItemStateConverter().convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), @@ -95,7 +95,7 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToUnreachableState(): TokenItemState.Unreachable { return TokenItemState.Unreachable( id = account.accountId.toItemId(), - iconState = AccountIconItemStateConverter.convert(account), + iconState = AccountIconItemStateConverter().convert(account), titleState = TokenItemState.TitleState.Content( text = accountName.toUM().value, ), diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt index 102be5a837..6cc4b2bf7e 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/account/AccountIconItemStateConverter.kt @@ -1,11 +1,14 @@ package com.tangem.common.ui.account +import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.utils.converter.Converter -object AccountIconItemStateConverter : Converter { +class AccountIconItemStateConverter( + val size: AccountIconSize = AccountIconSize.Default, +) : Converter { override fun convert(value: Account): CurrencyIconState.CryptoPortfolio = when (value) { is Account.CryptoPortfolio -> when { @@ -13,11 +16,13 @@ object AccountIconItemStateConverter : Converter CurrencyIconState.CryptoPortfolio.Icon( resId = value.icon.value.getResId(), color = value.icon.color.getUiColor(), isGrayscale = false, + size = size, ) } is Account.Payment -> TODO("[REDACTED_JIRA]") diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt index 7f8ec96750..1c3870ab40 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -1,17 +1,35 @@ package com.tangem.common.ui.notifications +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.fillMaxSize 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.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +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.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.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.ds.TangemPagerIndicator import com.tangem.core.ui.ds.message.TangemMessage +import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf fun LazyListScope.notifications( notifications: ImmutableList, @@ -110,4 +128,103 @@ fun LazyListScope.notifications( ) }, ) -} \ No newline at end of file +} + +/** + * Displays a list of notifications in a stacked manner using a HorizontalPager. + * If there are multiple notifications, a PagerIndicator is shown below the notifications. + * + * @param notifications List of TangemMessageUM objects to be displayed. + * @param containerColor Color to be used for the background of the notifications. + * @param modifier Optional Modifier for the notifications. + */ +fun LazyListScope.stackedNotifications( + notifications: ImmutableList?, + containerColor: Color, + modifier: Modifier = Modifier, +) { + item { + if (!notifications.isNullOrEmpty()) { + val notificationsPagerState = rememberPagerState( + pageCount = { notifications.size }, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(top = TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + HorizontalPager( + state = notificationsPagerState, + modifier = Modifier + .fillMaxSize() + .animateItem(null, null, null), + ) { page -> + TangemMessage( + messageUM = notifications[page], + contentColor = containerColor, + modifier = modifier, + ) + } + if (notifications.size > 1) { + TangemPagerIndicator( + pagerState = notificationsPagerState, + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun StackedNotifications_Preview( + @PreviewParameter(StackedNotificationsPreviewProvider::class) params: ImmutableList, +) { + TangemThemePreviewRedesign { + val contentColor = TangemTheme.colors2.surface.level1 + LazyColumn( + modifier = Modifier + .background(contentColor) + .padding(16.dp), + ) { + stackedNotifications( + notifications = params, + containerColor = contentColor, + ) + } + } +} + +private class StackedNotificationsPreviewProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = sequenceOf( + persistentListOf( + TangemMessageUM( + id = "0", + title = stringReference("First notification"), + subtitle = stringReference("This is the first notification"), + messageEffect = TangemMessageEffect.Magic, + ), + ), + persistentListOf( + TangemMessageUM( + id = "0", + title = stringReference("First notification"), + subtitle = stringReference("This is the first notification"), + messageEffect = TangemMessageEffect.Magic, + ), + TangemMessageUM( + id = "1", + title = stringReference("Second notification"), + subtitle = stringReference("This is the second notification"), + messageEffect = TangemMessageEffect.Card, + ), + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt new file mode 100644 index 0000000000..5528190513 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/TangemPagerIndicator.kt @@ -0,0 +1,370 @@ +package com.tangem.core.ui.ds + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.min +import kotlin.math.roundToInt + +private const val ANIMATION_DURATION = 300 +private const val MAX_VISIBLE_DOTS = 5 +private const val MIN_HIDDEN_FOR_SMALL_DOT = 2 +private const val MIN_DISTANCE_FOR_SMALL_DOT = 3 +private const val MIN_DISTANCE_FOR_HINT_DOT = 2 + +private val SPACING = 4.dp +private val CURRENT_DOT_SIZE = DpSize(16.dp, 8.dp) +private val NORMAL_DOT_SIZE = DpSize(8.dp, 8.dp) +private val HINT_DOT_SIZE = DpSize(6.dp, 6.dp) +private val SMALL_DOT_SIZE = DpSize(4.dp, 4.dp) + +/** + * // TODO Cleanup and document this code, it's quite complex and has some "magic numbers" that need explanation. + * + * A pager indicator that adapts to the number of pages and the current page index. + * + * For 5 or fewer pages, it shows all dots with the current page highlighted. + * For more than 5 pages, it shows a sliding window of 5 dots with size and opacity indicating position. + * + * @param pagerState state of the pager to observe + * @param activeIndicatorColor color for the active page indicator + * @param inactiveIndicatorColor color for the inactive page indicators + * @param modifier modifier for styling + */ +@Suppress("LongMethod", "CyclomaticComplexMethod") +@Composable +fun TangemPagerIndicator( + pagerState: PagerState, + modifier: Modifier = Modifier, + activeIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.primary, + inactiveIndicatorColor: Color = TangemTheme.colors2.graphic.neutral.tertiary, +) { + val totalPages = pagerState.pageCount + val currentIndex = pagerState.currentPage + + if (totalPages == 0) return + + val density = LocalDensity.current + + val (targetLower, targetUpper) = getWindowBounds(totalPages, currentIndex) + + var displayLower by remember { mutableIntStateOf(targetLower) } + var displayUpper by remember { mutableIntStateOf(targetUpper) } + var prevTargetLower by remember { mutableIntStateOf(targetLower) } + + val slideOffset = remember { Animatable(0f) } + var isSliding by remember { mutableStateOf(false) } + var slideDirection by remember { mutableIntStateOf(0) } + val fadeProgress = remember { Animatable(0f) } + var fadeJob by remember { mutableStateOf(null) } + + LaunchedEffect(targetLower) { + if (targetLower != prevTargetLower && totalPages > MAX_VISIBLE_DOTS) { + fadeJob?.cancel() + slideOffset.stop() + fadeProgress.stop() + + val dir = if (targetLower > prevTargetLower) 1 else -1 + val edgeDotSize = with(density) { (HINT_DOT_SIZE.width + SPACING).toPx() } + val halfEdge = edgeDotSize / 2 + + isSliding = true + slideDirection = dir + fadeProgress.snapTo(0f) + + if (dir > 0) { + displayLower = prevTargetLower + displayUpper = targetUpper + slideOffset.snapTo(halfEdge) + } else { + displayLower = targetLower + displayUpper = prevTargetLower + MAX_VISIBLE_DOTS + slideOffset.snapTo(-halfEdge) + } + + prevTargetLower = targetLower + + fadeJob = launch { + fadeProgress.animateTo(1f, tween(ANIMATION_DURATION)) + } + slideOffset.animateTo( + if (dir > 0) -halfEdge else halfEdge, + tween(ANIMATION_DURATION), + ) + + displayLower = targetLower + displayUpper = targetUpper + slideOffset.snapTo(0f) + isSliding = false + slideDirection = 0 + } + } + val visibleIndices = (displayLower until displayUpper).toList() + + Box( + modifier = modifier, + contentAlignment = Alignment.Center, + ) { + Row( + modifier = Modifier.offset { + IntOffset(slideOffset.value.roundToInt(), 0) + }, + horizontalArrangement = Arrangement.spacedBy(SPACING), + verticalAlignment = Alignment.CenterVertically, + ) { + visibleIndices.forEach { index -> + val dotAlpha = when { + !isSliding -> 1f + slideDirection > 0 && index == displayLower -> 1f - fadeProgress.value + slideDirection > 0 && index == displayUpper - 1 -> fadeProgress.value + slideDirection < 0 && index == displayUpper - 1 -> 1f - fadeProgress.value + slideDirection < 0 && index == displayLower -> fadeProgress.value + else -> 1f + } + + key(index) { + Dot( + index = index, + currentIndex = currentIndex, + totalPages = totalPages, + activeColor = activeIndicatorColor, + inactiveColor = inactiveIndicatorColor, + modifier = Modifier.graphicsLayer { alpha = dotAlpha }, + ) + } + } + } + } +} + +private fun getWindowBounds(totalPages: Int, currentIndex: Int): Pair { + if (totalPages <= MAX_VISIBLE_DOTS) { + return 0 to totalPages + } + val lowerBound = when { + currentIndex <= 1 -> 0 + currentIndex >= totalPages - 2 -> totalPages - MAX_VISIBLE_DOTS + else -> currentIndex - 2 + } + val upperBound = min(lowerBound + MAX_VISIBLE_DOTS, totalPages) + return lowerBound to upperBound +} + +private fun getDotSize(index: Int, currentIndex: Int, totalPages: Int): DpSize { + if (index == currentIndex) { + return CURRENT_DOT_SIZE + } + if (totalPages <= MAX_VISIBLE_DOTS) { + return NORMAL_DOT_SIZE + } + val params = DotSizeParams.create(index, currentIndex, totalPages) + return params.calculateSize() +} + +private class DotSizeParams private constructor( + val posInWindow: Int, + val currentPosInWindow: Int, + val hiddenLeft: Int, + val hiddenRight: Int, + val distanceFromCurrent: Int, +) { + private val lastPos = MAX_VISIBLE_DOTS - 1 + private val isCentered = currentPosInWindow == 2 && hiddenLeft >= 1 && hiddenRight >= 1 + + fun calculateSize(): DpSize = when { + isCentered -> getCenteredSize() + hiddenRight >= 1 -> getRightEdgeSize() + hiddenLeft >= 1 -> getLeftEdgeSize() + else -> NORMAL_DOT_SIZE + } + + private fun getCenteredSize(): DpSize = when (posInWindow) { + 0, lastPos -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + + private fun getRightEdgeSize(): DpSize { + val isLastPos = posInWindow == lastPos + val isSecondToLast = posInWindow == lastPos - 1 + val hasExtraHidden = hiddenRight >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isLastPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isLastPos && isModerateDistance -> HINT_DOT_SIZE + isSecondToLast && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + private fun getLeftEdgeSize(): DpSize { + val isFirstPos = posInWindow == 0 + val isSecondPos = posInWindow == 1 + val hasExtraHidden = hiddenLeft >= MIN_HIDDEN_FOR_SMALL_DOT + val isFarFromCurrent = distanceFromCurrent >= MIN_DISTANCE_FOR_SMALL_DOT + val isModerateDistance = distanceFromCurrent >= MIN_DISTANCE_FOR_HINT_DOT + + return when { + isFirstPos && hasExtraHidden && isFarFromCurrent -> SMALL_DOT_SIZE + isFirstPos && isModerateDistance -> HINT_DOT_SIZE + isSecondPos && hasExtraHidden && isModerateDistance -> HINT_DOT_SIZE + else -> NORMAL_DOT_SIZE + } + } + + companion object { + fun create(index: Int, currentIndex: Int, totalPages: Int): DotSizeParams { + val (windowStart, windowEnd) = getWindowBounds(totalPages, currentIndex) + val posInWindow = index - windowStart + val currentPosInWindow = currentIndex - windowStart + return DotSizeParams( + posInWindow = posInWindow, + currentPosInWindow = currentPosInWindow, + hiddenLeft = windowStart, + hiddenRight = totalPages - windowEnd, + distanceFromCurrent = abs(posInWindow - currentPosInWindow), + ) + } + } +} + +@Composable +private fun Dot( + index: Int, + currentIndex: Int, + totalPages: Int, + activeColor: Color, + inactiveColor: Color, + modifier: Modifier = Modifier, +) { + val isActive = index == currentIndex + val size = getDotSize(index, currentIndex, totalPages) + + val animSpec = tween(ANIMATION_DURATION) + val colorSpec = tween(ANIMATION_DURATION) + + val animatedWidth by animateDpAsState(size.width, animSpec, label = "w$index") + val animatedHeight by animateDpAsState(size.height, animSpec, label = "h$index") + val animatedColor by animateColorAsState( + targetValue = if (isActive) activeColor else inactiveColor, + animationSpec = colorSpec, + label = "c$index", + ) + + val shape = RoundedCornerShape(animatedHeight / 2) + + Box( + modifier = modifier + .width(animatedWidth) + .height(animatedHeight) + .background(animatedColor, shape), + ) +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 5 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator6ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 6 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator7ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 7 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicator10ItemsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9).forEach { page -> + TangemPagerIndicator(rememberPagerState(page) { 10 }) + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun PagerIndicatorSmallCountsPreview() { + TangemThemePreviewRedesign { + Column( + Modifier + .background(TangemTheme.colors.background.primary) + .padding(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp), + ) { + TangemPagerIndicator(rememberPagerState(0) { 1 }) + TangemPagerIndicator(rememberPagerState(1) { 2 }) + TangemPagerIndicator(rememberPagerState(1) { 3 }) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt index cae9546835..89aaa014aa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalCryptoFormat.kt @@ -1,5 +1,6 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CRYPTO_FEE_FORMAT_THRESHOLD import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CURRENCY_SPACE @@ -9,6 +10,7 @@ import com.tangem.utils.StringsSigns.NON_BREAKING_SPACE import com.tangem.utils.extensions.isNotWhitespace import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Currency import java.util.Locale @@ -34,6 +36,17 @@ class BigDecimalCryptoFormatFull( override fun invoke(value: BigDecimal): String = defaultAmount()(value) } +open class BigDecimalCryptoFormatStyled( + val symbol: String, + val decimals: Int, + val spanStyleReference: SpanStyleReference, + val locale: Locale = Locale.getDefault(), + val shouldIgnoreSymbolPosition: Boolean = false, +) : BigDecimalFormatStyled { + + override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value) +} + // == Initializers == fun BigDecimalFormatScope.crypto( @@ -61,6 +74,35 @@ fun BigDecimalFormatScope.crypto( ) } +fun BigDecimalFormatScope.cryptoStyled( + symbol: String, + decimals: Int, + spanStyleReference: SpanStyleReference, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormatStyled { + return BigDecimalCryptoFormatStyled( + symbol = symbol, + decimals = decimals, + spanStyleReference = spanStyleReference, + locale = locale, + ) +} + +fun BigDecimalFormatScope.cryptoStyled( + cryptoCurrency: CryptoCurrency, + spanStyleReference: SpanStyleReference, + ignoreSymbolPosition: Boolean = false, + locale: Locale = Locale.getDefault(), +): BigDecimalCryptoFormatStyled { + return BigDecimalCryptoFormatStyled( + symbol = cryptoCurrency.symbol, + decimals = cryptoCurrency.decimals, + spanStyleReference = spanStyleReference, + shouldIgnoreSymbolPosition = ignoreSymbolPosition, + locale = locale, + ) +} + // == Formatters == fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> @@ -88,6 +130,51 @@ fun BigDecimalCryptoFormat.defaultAmount() = BigDecimalFormat { value -> } } +fun BigDecimalCryptoFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = + BigDecimalFormatStyled { value -> + if (shouldIgnoreSymbolPosition) { + val formatter = NumberFormat.getInstance(locale).apply { + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val formattedAmount = formatter.format(value) + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + combinedReference( + stringReference(formattedAmount.take(separatorIndex)), + styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference), + stringReference(NON_BREAKING_SPACE + symbol), + ) + } else { + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = usdCurrency + maximumFractionDigits = decimals.coerceAtMost(maximumValue = 8) + minimumFractionDigits = 2 + isGroupingUsed = true + roundingMode = RoundingMode.HALF_UP + } + + val formattedAmount = formatter.format(value) + .replaceFiatSymbolWithCrypto( + fiatCurrencySymbol = usdCurrency.getSymbol(locale), + cryptoCurrencySymbol = symbol, + ) + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + combinedReference( + stringReference(formattedAmount.take(separatorIndex)), + styledStringReference(formattedAmount.drop(separatorIndex), spanStyleReference), + ) + } + } + fun BigDecimalCryptoFormat.shorted() = BigDecimalFormat { value -> val formatter = if (value.isMoreThanThreshold()) { NumberFormat.getCurrencyInstance(locale).apply { diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 5c41281344..a1f6549b2b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -1,9 +1,11 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants.CAN_BE_LOWER_SIGN import com.tangem.utils.StringsSigns.TILDE_SIGN import java.math.BigDecimal import java.math.RoundingMode +import java.text.DecimalFormat import java.text.NumberFormat import java.util.Locale @@ -15,8 +17,16 @@ open class BigDecimalFiatFormat( override fun invoke(value: BigDecimal): String = defaultAmount()(value) } -// == Initializers == +open class BigDecimalFiatFormatStyled( + val fiatCurrencyCode: String, + val fiatCurrencySymbol: String, + val spanStyleReference: SpanStyleReference, + val locale: Locale = Locale.getDefault(), +) : BigDecimalFormatStyled { + override fun invoke(value: BigDecimal): TextReference = defaultAmount(spanStyleReference)(value) +} +//region == Initializers == fun BigDecimalFormatScope.fiat( fiatCurrencyCode: String, fiatCurrencySymbol: String, @@ -29,7 +39,20 @@ fun BigDecimalFormatScope.fiat( ) } -// == Formatters == +fun BigDecimalFormatScope.fiat( + fiatCurrencyCode: String, + fiatCurrencySymbol: String, + spanStyleReference: SpanStyleReference, + locale: Locale = Locale.getDefault(), +): BigDecimalFiatFormatStyled { + return BigDecimalFiatFormatStyled( + fiatCurrencyCode = fiatCurrencyCode, + fiatCurrencySymbol = fiatCurrencySymbol, + spanStyleReference = spanStyleReference, + locale = locale, + ) +} +// endregion == Formatters == /** * Formats fiat amount with default precision. @@ -58,6 +81,38 @@ fun BigDecimalFiatFormat.defaultAmount(): BigDecimalFormat = BigDecimalFormat { } } +fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleReference) = BigDecimalFormatStyled { value -> + val formatterCurrency = getJavaCurrencyByCode(fiatCurrencyCode) + + val formatter = NumberFormat.getCurrencyInstance(locale).apply { + currency = formatterCurrency + maximumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + minimumFractionDigits = FIAT_MARKET_DEFAULT_DIGITS + roundingMode = RoundingMode.HALF_UP + } + + val formattingAmount = if (value.isLessThanThreshold()) { + FIAT_FORMAT_THRESHOLD + } else { + value + } + + val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator + val formattedAmount = formatter.format(formattingAmount) + .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + + val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + + val wholePart = formattedAmount.take(separatorIndex) + val fractionalPart = formattedAmount.drop(separatorIndex) + + combinedReference( + if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY, + stringReference(wholePart), + styledStringReference(fractionalPart, spanStyleReference), + ) +} + /** * Formats fiat amount with default precision and adds tilde sign */ diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt index c685db7f1d..f7a87efe6a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFormat.kt @@ -1,17 +1,27 @@ package com.tangem.core.ui.format.bigdecimal +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference import java.math.BigDecimal interface BigDecimalFormatScope { - companion object { val Empty = object : BigDecimalFormatScope {} } + companion object { + val Empty = object : BigDecimalFormatScope {} + } } fun interface BigDecimalFormat : (BigDecimal) -> String, BigDecimalFormatScope +fun interface BigDecimalFormatStyled : (BigDecimal) -> TextReference, BigDecimalFormatScope + inline fun BigDecimal.format(block: BigDecimalFormatScope.() -> BigDecimalFormat): String { return BigDecimalFormatScope.Empty.block()(this) } +inline fun BigDecimal.formatStyled(block: BigDecimalFormatScope.() -> BigDecimalFormatStyled): TextReference { + return BigDecimalFormatScope.Empty.block()(this) +} + inline fun BigDecimal?.format( fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, block: BigDecimalFormatScope.() -> BigDecimalFormat, @@ -20,10 +30,26 @@ inline fun BigDecimal?.format( return BigDecimalFormatScope.Empty.block()(this) } +inline fun BigDecimal?.formatStyled( + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, + block: BigDecimalFormatScope.() -> BigDecimalFormatStyled, +): TextReference { + if (this == null) return stringReference(fallbackString) + return BigDecimalFormatScope.Empty.block()(this) +} + fun BigDecimal?.format( format: BigDecimalFormat, fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, ): String { if (this == null) return fallbackString return format(this) +} + +fun BigDecimal?.format( + format: BigDecimalFormatStyled, + fallbackString: String = BigDecimalFormatConstants.EMPTY_BALANCE_SIGN, +): TextReference { + if (this == null) return stringReference(fallbackString) + return format(this) } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 04801d50d7..150d6586d3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -16,6 +16,7 @@ object TangemColorPalette { val Dark4 = Color(0xFF3B3B3B) val Dark5 = Color(0xFF303030) val Dark6 = Color(0xFF1E1E1E) + val Dark7 = Color(0xFF171717) // endregion Dark // region Dark Alpha diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index efa104abf2..b838ad7601 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -122,8 +122,8 @@ private fun lightThemeColors2(): TangemColors2 { val surface = TangemColors2.Surface( level1 = TangemColorPalette.White, level2 = TangemColorPalette.Light1V2, - level3 = TangemColorPalette.Light1V2, - level4 = TangemColorPalette.White, + level3 = TangemColorPalette.White, + level4 = TangemColorPalette.Light1V2, ) val controls = TangemColors2.Controls( backgroundChecked = TangemColorPalette.Dark6, @@ -270,8 +270,8 @@ private fun darkThemeColors2(): TangemColors2 { borderPrimary = TangemColorPalette.Light4, ) val surface = TangemColors2.Surface( - level1 = TangemColorPalette.Dark6, - level2 = TangemColorPalette.Black, + level1 = TangemColorPalette.Black, + level2 = TangemColorPalette.Dark7, level3 = TangemColorPalette.Dark6, level4 = TangemColorPalette.Dark5, ) From e3f6d16441795f078237c9b737ba5b1ab372810b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 11 Feb 2026 15:48:18 +0300 Subject: [PATCH 06/97] Updated on 2026-08-14 --- .../market/SwapMarketsListBatchFlowManager.kt | 3 +- .../market/state/SwapMarketState.kt | 19 +- .../model/AvailableSwapPairsAnalyticsEvent.kt | 44 +++ .../model/AvailableSwapPairsModel.kt | 171 +++++++++-- .../availablepairs/ui/SwapMarketsListItems.kt | 3 +- .../onramp/tokenlist/ui/OnrampTokenList.kt | 16 +- .../feature/swap/analytics/SwapEvents.kt | 67 ++++- .../tangem/feature/swap/model/SwapModel.kt | 265 ++++++++++++++---- .../swap/model/SwapNotificationsFactory.kt | 6 + .../market/SwapMarketsListBatchFlowManager.kt | 6 +- .../models/market/state/SwapMarketState.kt | 19 +- .../swap/models/states/SwapNotificationUM.kt | 12 + .../tangem/feature/swap/ui/StateBuilder.kt | 55 ++++ .../feature/swap/ui/SwapSelectTokenScreen.kt | 22 +- .../ui/market/SwapMarketsListLazyColumn.kt | 3 +- .../preview/SwapSelectTokenPreviewProvider.kt | 4 + 16 files changed, 597 insertions(+), 118 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt index 8d9b34c5b8..5df10fda71 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/SwapMarketsListBatchFlowManager.kt @@ -22,6 +22,7 @@ import kotlinx.coroutines.flow.* internal class SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val order: TokenMarketListConfig.Order, private val currentAppCurrency: Provider, private val currentSearchText: Provider, private val modelScope: CoroutineScope, @@ -183,7 +184,7 @@ internal class SwapMarketsListBatchFlowManager( searchText ?: currentSearchText() }, priceChangeInterval = TokenMarketListConfig.Interval.H24, - order = TokenMarketListConfig.Order.ByRating, + order = order, shouldNetworks = true, ), ), diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt index bcd1c46b86..7e9abb3982 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/market/state/SwapMarketState.kt @@ -2,25 +2,40 @@ package com.tangem.features.onramp.swap.availablepairs.market.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.R +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class SwapMarketState { + abstract val marketsTitle: TextReference + abstract val shouldAssetsCount: Boolean + data class Content( val items: ImmutableList, val total: Int, val loadMore: () -> Unit, val onItemClick: (MarketsListItemUM) -> Unit, val visibleIdsChanged: (List) -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object Loading : SwapMarketState() + data class Loading( + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() data class LoadingError( val onRetryClicked: () -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object SearchNothingFound : SwapMarketState() + data object SearchNothingFound : SwapMarketState() { + override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) + override val shouldAssetsCount: Boolean = true + } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt new file mode 100644 index 0000000000..f5a57df2b5 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/model/AvailableSwapPairsAnalyticsEvent.kt @@ -0,0 +1,44 @@ +package com.tangem.features.onramp.swap.availablepairs.model + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam.Key.BLOCKCHAIN +import com.tangem.core.analytics.models.AnalyticsParam.Key.TOKEN_PARAM + +private const val SWAP_CATEGORY = "Swap" + +internal sealed class AvailableSwapPairsAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(SWAP_CATEGORY, event, params) { + + class TokenSelected( + val token: String, + val source: String, + val isSearched: Boolean, + ) : AvailableSwapPairsAnalyticsEvent( + event = "Token Selected", + params = mapOf( + TOKEN_PARAM to token, + SOURCE to source, + SEARCHED to if (isSearched) "True" else "False", + ), + ) { + companion object { + const val SOURCE = "Source" + const val SEARCHED = "Searched" + const val SOURCE_PORTFOLIO = "Portfolio" + const val SOURCE_MARKETS = "Markets" + } + } + + class TokenAdded( + val token: String, + val blockchain: String, + ) : AvailableSwapPairsAnalyticsEvent( + event = "Token Added", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), + ) +} \ 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 7867772800..62822d4108 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,8 +1,12 @@ package com.tangem.features.onramp.swap.availablepairs.model +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.model.Model +import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager +import com.tangem.core.ui.R as CoreUiR +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.capitalize import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList @@ -22,6 +26,7 @@ import com.tangem.domain.core.utils.lceError import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus @@ -66,6 +71,7 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.runSuspendCatching +import kotlinx.coroutines.ExperimentalCoroutinesApi import com.tangem.utils.coroutines.saveIn import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -77,6 +83,7 @@ private typealias AvailablePairsState = Lce> internal class AvailableSwapPairsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, + private val analyticsEventHandler: AnalyticsEventHandler, private val getTokenListUseCase: GetTokenListUseCase, private val tokenListUMController: TokenListUMController, private val searchManager: InputManager, @@ -120,10 +127,23 @@ internal class AvailableSwapPairsModel @Inject constructor( private val searchQueryStateForMarkets = MutableStateFlow("") private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val defaultMarketsListManager by lazy { + SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, + currentSearchText = Provider { null }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + private val searchMarketsListManager by lazy { SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, currentSearchText = Provider { searchQueryStateForMarkets.value }, modelScope = modelScope, @@ -131,6 +151,8 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + init { if (accountsFeatureToggles.isFeatureEnabled) { subscribeOnUpdateStateV2() @@ -298,7 +320,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } else { UpdateTokenItemsTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onPortfolioTokenClick, statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs), isBalanceHidden = isBalanceHidden, unavailableTokensHeaderReference = resourceReference( @@ -357,7 +379,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } else { UpdateAccountTokenListTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onPortfolioTokenClick, accountList = filterByQueryAccountList.filterByAvailability(availablePairs = availablePairs), isBalanceHidden = isBalanceHidden, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), @@ -543,6 +565,17 @@ internal class AvailableSwapPairsModel @Inject constructor( } } + private fun onPortfolioTokenClick(tokenItem: TokenItemState, status: CryptoCurrencyStatus) { + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenSelected( + token = status.currency.symbol, + source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_PORTFOLIO, + isSearched = state.value.searchBarUM.query.isNotEmpty(), + ), + ) + params.onTokenClick(tokenItem, status) + } + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", @@ -550,50 +583,121 @@ internal class AvailableSwapPairsModel @Inject constructor( ) } - @Suppress("LongMethod") + @OptIn(ExperimentalCoroutinesApi::class) private fun subscribeOnMarketsUpdates() { - combine( - flow = searchQueryStateForMarkets - .onEach { searchQuery -> - if (searchQuery.isNotEmpty()) { - searchMarketsListManager.reload(searchQuery) - } - }, - flow2 = searchMarketsListManager.uiItems, - flow3 = searchMarketsListManager.isInInitialLoadingErrorState, - flow4 = searchMarketsListManager.isSearchNotFoundState, - flow5 = searchMarketsListManager.totalCount, - ) { searchQuery, uiItems, isError, isSearchNotFound, total -> - when { - searchQuery.isEmpty() -> { + searchQueryStateForMarkets + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { visibleMarketItemIds.value = emptyList() - null + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() } + } + .onEach { marketsState -> + tokenListUMController.update { it.copy(marketsState = marketsState) } + } + .flowOn(dispatchers.main) + .launchIn(modelScope) + + searchQueryStateForMarkets + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + params.selectedStatus + .filterNotNull() + .take(1) + .onEach { defaultMarketsListManager.reload() } + .launchIn(modelScope) + } + + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(CoreUiR.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQuery) }, + onRetryClicked = { defaultMarketsListManager.reload() }, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { defaultMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + } + } + } + + private fun createSearchMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(CoreUiR.string.markets_common_title) + return combine( + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = searchMarketsListManager.totalCount, + ) { uiItems, isError, isSearchNotFound, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { + searchMarketsListManager.reload(searchQueryStateForMarkets.value) + }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, ) isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) else -> SwapMarketState.Content( items = uiItems, loadMore = { searchMarketsListManager.loadMore() }, onItemClick = { item -> addToPortfolioItem(item) }, visibleIdsChanged = { visibleMarketItemIds.value = it }, total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = true, ) } } - .distinctUntilChanged() - .onEach { marketsState -> - tokenListUMController.update { it.copy(marketsState = marketsState) } - } - .flowOn(dispatchers.main) - .launchIn(modelScope) } private fun onTokenAddedToPortfolio(addedToken: CryptoCurrency) { modelScope.launch { bottomSheetNavigation.dismiss() + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenAdded( + token = addedToken.symbol, + blockchain = addedToken.network.name, + ), + ) + analyticsEventHandler.send( + AvailableSwapPairsAnalyticsEvent.TokenSelected( + token = addedToken.symbol, + source = AvailableSwapPairsAnalyticsEvent.TokenSelected.SOURCE_MARKETS, + isSearched = state.value.searchBarUM.query.isNotEmpty(), + ), + ) // Trigger re-fetch of available pairs (clears cache + re-enters collectLatest) refreshPairsTrigger.emit(Unit) @@ -615,7 +719,9 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun addToPortfolioItem(item: MarketsListItemUM) { modelScope.launch { - val tokenMarket = searchMarketsListManager.getTokenMarketById(item.id) ?: return@launch + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch val param = tokenMarket.toSerializableParam() val hasOnlyHotWallets = allUserWallets.all { it is UserWallet.Hot } @@ -663,5 +769,16 @@ internal class AvailableSwapPairsModel @Inject constructor( searchMarketsListManager.loadCharts(visibleBatchKeys) } } + modelScope.launch { + visibleDefaultMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + } } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt index b8e098fa31..18600da888 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/ui/SwapMarketsListItems.kt @@ -17,6 +17,7 @@ import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.core.ui.R import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState @@ -28,7 +29,7 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { val totalCount = (state as? SwapMarketState.Content)?.total Text( text = buildAnnotatedString { - append(stringResourceSafe(R.string.markets_common_title)) + append(state.marketsTitle.resolveReference()) if (totalCount != null) { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { append(" $totalCount") 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 6cecf0ac0e..d92870b44f 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 @@ -50,8 +50,7 @@ import kotlinx.collections.immutable.ImmutableList * */ internal fun LazyListScope.onrampSwapTokenList(state: TokenListUM) { - val isSearchMode = state.searchBarUM.query.isNotEmpty() && state.marketsState != null - if (isSearchMode) { + if (state.marketsState != null) { onrampTokenListWithMarkets(state = state) } else { onrampTokenList(state = state) @@ -91,7 +90,10 @@ private fun LazyListScope.onrampTokenListWithMarkets(state: TokenListUM) { state.tokensListData.totalTokensCount != 0 if (hasAssets) { - assetsTitle(count = state.tokensListData.totalTokensCount) + assetsTitle( + count = state.tokensListData.totalTokensCount, + showCount = state.marketsState?.shouldAssetsCount == true, + ) tokensList(items = state.availableItems, isBalanceHidden = state.isBalanceHidden) @@ -159,13 +161,15 @@ private fun LazyListScope.searchBarItem(searchBarUM: SearchBarUM, modifier: Modi } } -private fun LazyListScope.assetsTitle(count: Int) { +private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { item(key = "assets_title") { Text( text = buildAnnotatedString { append(stringResourceSafe(R.string.swap_your_assets_title)) - withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { - append(" $count") + if (showCount) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } } }, style = TangemTheme.typography.h3, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 98955f6373..cd44e2f95c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -25,7 +25,7 @@ sealed class SwapEvents( params: Map = emptyMap(), ) : AnalyticsEvent(SWAP_CATEGORY, event, params) { - data class SwapScreenOpened( + class SwapScreenOpened( val token: String, val blockchain: String, ) : SwapEvents( @@ -38,25 +38,47 @@ sealed class SwapEvents( class SendTokenBalanceClicked : SwapEvents(event = "Send Token Balance Clicked") - data class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( + class ChooseTokenScreenOpened(val hasAvailableTokens: Boolean) : SwapEvents( event = "Choose Token Screen Opened", params = mapOf("Available tokens" to if (hasAvailableTokens) "Yes" else "No"), ) - data class ChooseTokenScreenResult(val isTokenChosen: Boolean, val token: String? = null) : SwapEvents( + class ChooseTokenScreenResult( + val isTokenChosen: Boolean, + val token: String? = null, + val source: String? = null, + val isSearched: Boolean? = null, + ) : SwapEvents( event = "Choose Token Screen Result", params = buildMap { put("Token Chosen", if (isTokenChosen) "Yes" else "No") token?.let { put("Token", it) } + source?.let { put(TOKEN_SELECTED_SOURCE, it) } + isSearched?.let { put(SEARCHED, if (it) "True" else "False") } }, + ) { + companion object { + const val TOKEN_SELECTED_SOURCE = "Token Selected Source" + const val SEARCHED = "Searched" + const val SOURCE_PORTFOLIO = "Portfolio" + const val SOURCE_MARKETS = "Markets" + } + } + + class TokenAdded(val token: String, val blockchain: String) : SwapEvents( + event = "Token Added", + params = mapOf( + TOKEN_PARAM to token, + BLOCKCHAIN to blockchain, + ), ) - data class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( + class ButtonSwapClicked(val sendToken: String, val receiveToken: String) : SwapEvents( event = "Button - Swap", params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), ) - data class ButtonGivePermissionClicked( + class ButtonGivePermissionClicked( val sendToken: String, val receiveToken: String, val provider: SwapProvider, @@ -69,7 +91,7 @@ sealed class SwapEvents( ), ) - data class ButtonPermissionApproveClicked( + class ButtonPermissionApproveClicked( val sendToken: String, val receiveToken: String, val approveType: ApproveType, @@ -88,8 +110,8 @@ sealed class SwapEvents( class ButtonSwipeClicked : SwapEvents(event = "Button - Swipe") - @Suppress("NullableToStringCall") - data class SwapInProgressScreen( + @Suppress("NullableToStringCall", "LongParameterList") + class SwapInProgressScreen( val provider: SwapProvider, val commission: FeeType, // Market / Fast val sendBlockchain: String, @@ -118,24 +140,39 @@ sealed class SwapEvents( class ProviderClicked : SwapEvents("Provider Clicked") - data class ProviderChosen(val provider: SwapProvider) : SwapEvents( + class ProviderChosen(val provider: SwapProvider) : SwapEvents( event = "Provider Chosen", params = mapOf("Provider" to provider.name), ) - data class ButtonStatus(val token: String) : SwapEvents( + class ButtonStatus(val token: String) : SwapEvents( event = "Button - Status", params = mapOf("Token" to token), ) - data class ButtonExplore(val token: String) : SwapEvents( + class ButtonExplore(val token: String) : SwapEvents( event = "Button - Explore", params = mapOf("Token" to token), ) class NoticeNoAvailableTokensToSwap : SwapEvents("Notice - No Available Tokens To Swap") - data class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( + class NoticeUnavailableToSwapPair( + val sendToken: String, + val receiveToken: String, + val sendBlockchain: String, + val receiveBlockchain: String, + ) : SwapEvents( + event = "Notice - Unavailable To Swap Pair", + params = mapOf( + SEND_TOKEN to sendToken, + RECEIVE_TOKEN to receiveToken, + "Send Blockchain" to sendBlockchain, + "Receive Blockchain" to receiveBlockchain, + ), + ) + + class NoticeNotEnoughFee(val token: String, val blockchain: String) : SwapEvents( event = "Notice - Not Enough Fee", params = mapOf( "Token" to token, @@ -143,7 +180,7 @@ sealed class SwapEvents( ), ) - data class NoticeProviderError( + class NoticeProviderError( val sendToken: String, val receiveToken: String, val provider: SwapProvider, @@ -162,7 +199,7 @@ sealed class SwapEvents( // TODO parameters // region Promo activity - data class ChangellyActivity( + class ChangellyActivity( val promoState: PromoState, ) : AnalyticsEvent( category = PROMO_CATEGORY, @@ -177,7 +214,7 @@ sealed class SwapEvents( } } - data class NoticePermissionNeeded( + class NoticePermissionNeeded( val sendToken: String, val receiveToken: String, val provider: SwapProvider, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 35c5fd4fa3..a90bd3640a 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue + import arrow.core.Either import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation @@ -44,6 +45,7 @@ import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.TokenMarketInfo +import com.tangem.domain.markets.TokenMarketListConfig import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency @@ -88,7 +90,7 @@ import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.models.states.SwapNotificationUM -import com.tangem.feature.swap.presentation.R +import com.tangem.core.ui.R import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder @@ -105,6 +107,7 @@ import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -120,6 +123,7 @@ import javax.inject.Inject typealias SuccessLoadedSwapData = Map +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped @@ -154,7 +158,7 @@ internal class SwapModel @Inject constructor( private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val sendFeatureToggles: SendFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, - swapFeatureToggles: SwapFeatureToggles, + private val swapFeatureToggles: SwapFeatureToggles, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, private val excludedBlockchains: ExcludedBlockchains, private val getUserWalletsUseCase: GetWalletsUseCase, @@ -258,10 +262,26 @@ internal class SwapModel @Inject constructor( private val searchQueryState = MutableStateFlow("") private val visibleMarketItemIds = MutableStateFlow>(emptyList()) + private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) + private var latestMarketsState: SwapMarketState? = null + + private val defaultMarketsListManager by lazy { + SwapMarketsListBatchFlowManager( + getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, + batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Main, + order = TokenMarketListConfig.Order.Trending, + currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, + currentSearchText = Provider { null }, + modelScope = modelScope, + dispatchers = dispatchers, + ) + } + private val searchMarketsListManager by lazy { SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase = getMarketsTokenListFlowUseCase, batchFlowType = GetMarketsTokenListFlowUseCase.BatchFlowType.Search, + order = TokenMarketListConfig.Order.ByRating, currentAppCurrency = Provider { selectedAppCurrencyFlow.value }, currentSearchText = Provider { searchQueryState.value }, modelScope = modelScope, @@ -279,13 +299,21 @@ internal class SwapModel @Inject constructor( override fun onSuccess(addedToken: CryptoCurrency) { modelScope.launch { bottomSheetNavigation.dismiss() - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = null, - ), - ) - } + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult( + isTokenChosen = true, + token = addedToken.symbol, + source = SwapEvents.ChooseTokenScreenResult.SOURCE_MARKETS, + isSearched = searchQueryState.value.isNotEmpty(), + ), + ) + analyticsEventHandler.send( + SwapEvents.TokenAdded( + token = addedToken.symbol, + blockchain = addedToken.network.name, + ), + ) + searchQueryState.value = "" getAccountCurrencyStatusUseCase.invoke(userWalletId, addedToken) .firstOrNull { it.status.value is CryptoCurrencyStatus.Loaded @@ -362,50 +390,7 @@ internal class SwapModel @Inject constructor( } .launchIn(modelScope) - if (swapFeatureToggles.isMarketListFeatureEnabled) { - combine( - flow = searchQueryState - .onEach { searchQuery -> - searchMarketsListManager.reload(searchQuery) - }, - flow2 = searchMarketsListManager.uiItems, - flow3 = searchMarketsListManager.isInInitialLoadingErrorState, - flow4 = searchMarketsListManager.isSearchNotFoundState, - flow5 = searchMarketsListManager.totalCount.filterNotNull(), - ) { searchQuery, uiItems, isError, isSearchNotFound, total -> - when { - searchQuery.isEmpty() -> { - visibleMarketItemIds.value = emptyList() - null - } - isError -> SwapMarketState.LoadingError( - onRetryClicked = { searchMarketsListManager.reload(searchQuery) }, - ) - isSearchNotFound -> SwapMarketState.SearchNothingFound - uiItems.isEmpty() -> SwapMarketState.Loading - else -> SwapMarketState.Content( - items = uiItems, - loadMore = { searchMarketsListManager.loadMore() }, - onItemClick = { item -> - addToPortfolioItem(item) - }, - visibleIdsChanged = { visibleMarketItemIds.value = it }, - total = total, - ) - } - } - .distinctUntilChanged() - .onEach { marketsState -> - uiState.selectTokenState?.let { currentSelectState -> - uiState = uiState.copy( - selectTokenState = currentSelectState.copy( - marketsState = marketsState, - ), - ) - } - } - .launchIn(modelScope) - } + subscribeMarketTokens() modelScope.launch { visibleMarketItemIds.mapNotNull { rawIDS -> @@ -418,6 +403,18 @@ internal class SwapModel @Inject constructor( searchMarketsListManager.loadCharts(visibleBatchKeys) } } + + modelScope.launch { + visibleDefaultMarketItemIds.mapNotNull { rawIds -> + if (rawIds.isNotEmpty()) { + defaultMarketsListManager.getBatchKeysByItemIds(rawIds) + } else { + null + } + }.distinctUntilChanged().collectLatest { visibleBatchKeys -> + defaultMarketsListManager.loadCharts(visibleBatchKeys) + } + } } fun onStart() { @@ -443,6 +440,41 @@ internal class SwapModel @Inject constructor( analyticsEventHandler.send(SwapEvents.ChooseTokenScreenOpened(hasAvailableTokens = isAnyAvailableTokens)) } + private fun subscribeMarketTokens() { + if (swapFeatureToggles.isMarketListFeatureEnabled) { + // Switch between default and search market flows + searchQueryState + .map { it.isEmpty() } + .distinctUntilChanged() + .flatMapLatest { isDefaultMode -> + if (isDefaultMode) { + visibleMarketItemIds.value = emptyList() + createDefaultMarketsFlow() + } else { + visibleDefaultMarketItemIds.value = emptyList() + createSearchMarketsFlow() + } + } + .onEach { marketsState -> + latestMarketsState = marketsState + applyMarketsState(marketsState) + } + .launchIn(modelScope) + + // Reload search markets when query changes + searchQueryState + .onEach { searchQuery -> + if (searchQuery.isNotEmpty()) { + searchMarketsListManager.reload(searchQuery) + } + } + .launchIn(modelScope) + + // Initial load of default markets + defaultMarketsListManager.reload() + } + } + @Suppress("LongMethod") private fun initTokens(isReverseFromTo: Boolean) { modelScope.launch(dispatchers.main) { @@ -613,6 +645,25 @@ internal class SwapModel @Inject constructor( toAccount = toAccount, tokensDataState = state, ) + + if (!isTokenAvailableForSwap(state, selectedCurrency, isReverseFromTo)) { + analyticsEventHandler.send( + SwapEvents.NoticeUnavailableToSwapPair( + sendToken = fromCurrencyStatus.currency.symbol, + receiveToken = toCurrencyStatus.currency.symbol, + sendBlockchain = fromCurrencyStatus.currency.network.name, + receiveBlockchain = toCurrencyStatus.currency.network.name, + ), + ) + uiState = stateBuilder.createSwapNotSupportedState( + uiStateHolder = uiState, + fromToken = fromCurrencyStatus, + toToken = toCurrencyStatus, + toAccount = toAccount, + ) + return + } + startLoadingQuotes( fromToken = fromCurrencyStatus, fromAccount = fromAccount, @@ -640,6 +691,17 @@ internal class SwapModel @Inject constructor( fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom, ) } + latestMarketsState?.let(::applyMarketsState) + } + + private fun applyMarketsState(marketsState: SwapMarketState) { + uiState.selectTokenState?.let { currentSelectState -> + uiState = uiState.copy( + selectTokenState = currentSelectState.copy( + marketsState = marketsState, + ), + ) + } } private fun startLoadingQuotes( @@ -1275,7 +1337,14 @@ internal class SwapModel @Inject constructor( val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) foundToken?.currency?.symbol?.let { symbol -> - analyticsEventHandler.send(SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol)) + analyticsEventHandler.send( + SwapEvents.ChooseTokenScreenResult( + isTokenChosen = true, + token = symbol, + source = SwapEvents.ChooseTokenScreenResult.SOURCE_PORTFOLIO, + isSearched = searchQueryState.value.isNotEmpty(), + ), + ) } if (foundToken != null) { @@ -1871,6 +1940,28 @@ internal class SwapModel @Inject constructor( .orEmpty() } + private fun isTokenAvailableForSwap( + state: TokensDataStateExpress, + selectedCurrency: CryptoCurrencyStatus, + isReverseFromTo: Boolean, + ): Boolean { + val group = if (isReverseFromTo) state.fromGroup else state.toGroup + val idToFind = selectedCurrency.currency.id.value + + return if (accountsFeatureToggles.isFeatureEnabled) { + group.accountCurrencyList.any { (_, currencyList) -> + currencyList.any { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable + } + } + } else { + group.available.any { swapAvailability -> + idToFind == swapAvailability.currencyStatus.currency.id.value + } + } + } + private fun List.filterForTangemPayWithdrawal(): List { return if (tangemPayInput?.isWithdrawal == true) { filter { it.type == ExchangeProviderType.CEX } @@ -2058,9 +2149,73 @@ internal class SwapModel @Inject constructor( } } + private fun createDefaultMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.feed_trending_now) + return combine( + defaultMarketsListManager.uiItems, + defaultMarketsListManager.isInInitialLoadingErrorState, + defaultMarketsListManager.totalCount, + ) { uiItems, isError, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { defaultMarketsListManager.reload() }, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { defaultMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleDefaultMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = false, + ) + } + } + } + + private fun createSearchMarketsFlow(): Flow { + val marketsTitle = TextReference.Res(R.string.markets_common_title) + return combine( + flow = searchMarketsListManager.uiItems, + flow2 = searchMarketsListManager.isInInitialLoadingErrorState, + flow3 = searchMarketsListManager.isSearchNotFoundState, + flow4 = searchMarketsListManager.totalCount, + ) { uiItems, isError, isSearchNotFound, total -> + when { + isError -> SwapMarketState.LoadingError( + onRetryClicked = { searchMarketsListManager.reload(searchQueryState.value) }, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + isSearchNotFound -> SwapMarketState.SearchNothingFound + uiItems.isEmpty() -> SwapMarketState.Loading( + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + else -> SwapMarketState.Content( + items = uiItems, + loadMore = { searchMarketsListManager.loadMore() }, + onItemClick = { item -> addToPortfolioItem(item) }, + visibleIdsChanged = { visibleMarketItemIds.value = it }, + total = total ?: uiItems.size, + marketsTitle = marketsTitle, + shouldAssetsCount = true, + ) + } + } + } + private fun addToPortfolioItem(item: MarketsListItemUM) { modelScope.launch { - val tokenMarket = searchMarketsListManager.getTokenMarketById(item.id) ?: return@launch + val tokenMarket = defaultMarketsListManager.getTokenMarketById(item.id) + ?: searchMarketsListManager.getTokenMarketById(item.id) + ?: return@launch val param = tokenMarket.toSerializableParam() val hasOnlyHotWallets = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt index 6c9bd0a484..fdb774f17d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapNotificationsFactory.kt @@ -64,6 +64,12 @@ internal class SwapNotificationsFactory( ) } + fun getSwapNotSupportedNotifications(tokenName: String): ImmutableList { + return persistentListOf( + SwapNotificationUM.Warning.SwapNotSupported(tokenName), + ) + } + fun getQuotesErrorStateNotifications( expressDataError: ExpressDataError, fromToken: CryptoCurrency, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt index e8331efed3..77609fade7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/SwapMarketsListBatchFlowManager.kt @@ -22,12 +22,14 @@ import kotlinx.coroutines.flow.* internal class SwapMarketsListBatchFlowManager( getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val batchFlowType: GetMarketsTokenListFlowUseCase.BatchFlowType, + private val order: TokenMarketListConfig.Order, private val currentAppCurrency: Provider, private val currentSearchText: Provider, private val modelScope: CoroutineScope, private val dispatchers: CoroutineDispatcherProvider, ) { - private val actionsFlow = MutableSharedFlow>() + private val actionsFlow = + MutableSharedFlow>(replay = 1) private val updateStateJob = JobHolder() private val batchFlow = getMarketsTokenListFlowUseCase( @@ -183,7 +185,7 @@ internal class SwapMarketsListBatchFlowManager( searchText ?: currentSearchText() }, priceChangeInterval = TokenMarketListConfig.Interval.H24, - order = TokenMarketListConfig.Order.ByRating, + order = order, shouldNetworks = true, ), ), diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt index 50e6d14a74..8be842bcfd 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/market/state/SwapMarketState.kt @@ -2,25 +2,40 @@ package com.tangem.feature.swap.models.market.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.core.ui.R import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class SwapMarketState { + abstract val marketsTitle: TextReference + abstract val shouldAssetsCount: Boolean + data class Content( val items: ImmutableList, val total: Int, val loadMore: () -> Unit, val onItemClick: (MarketsListItemUM) -> Unit, val visibleIdsChanged: (List) -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object Loading : SwapMarketState() + data class Loading( + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, + ) : SwapMarketState() data class LoadingError( val onRetryClicked: () -> Unit, + override val marketsTitle: TextReference, + override val shouldAssetsCount: Boolean, ) : SwapMarketState() - data object SearchNothingFound : SwapMarketState() + data object SearchNothingFound : SwapMarketState() { + override val marketsTitle: TextReference = TextReference.Res(R.string.markets_common_title) + override val shouldAssetsCount: Boolean = true + } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt index d140ef1ff0..7e5209dca5 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/SwapNotificationUM.kt @@ -132,6 +132,18 @@ internal object SwapNotificationUM { ), ) + data class SwapNotSupported( + val tokenName: String, + ) : Warning( + title = resourceReference( + id = com.tangem.feature.swap.presentation.R.string.express_swap_not_supported_title, + formatArgs = wrappedList(tokenName), + ), + subtitle = resourceReference( + com.tangem.feature.swap.presentation.R.string.express_swap_not_supported_text, + ), + ) + data class NeedReserveToCreateAccount( val receiveAmount: String, val receiveToken: String, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index c3a85fe555..6c955e7c86 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -200,6 +200,60 @@ internal class StateBuilder( ) } + fun createSwapNotSupportedState( + uiStateHolder: SwapStateHolder, + fromToken: CryptoCurrencyStatus, + toToken: CryptoCurrencyStatus, + toAccount: Account.CryptoPortfolio?, + ): SwapStateHolder { + if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder + return uiStateHolder.copy( + sendCardData = SwapCardState.SwapCardData( + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + amountTextFieldValue = null, + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = fromToken, + tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, + coinId = uiStateHolder.sendCardData.coinId, + isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, + tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, + canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, + balance = fromToken.getFormattedAmount(isNeedSymbol = false), + networkIconRes = getActiveIconRes(fromToken.currency.network.rawId), + isBalanceHidden = isBalanceHiddenProvider(), + ), + receiveCardData = SwapCardState.SwapCardData( + type = TransactionCardType.ReadOnly( + accountTitleUM = getToCardAccountTitle(toAccount), + ), + amountTextFieldValue = TextFieldValue( + text = "0", + ), + amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", + token = toToken, + tokenIconUrl = toToken.currency.iconUrl, + coinId = toToken.currency.network.backendId, + isNotNativeToken = toToken.currency is CryptoCurrency.Token, + tokenCurrency = toToken.currency.symbol, + canSelectAnotherToken = true, + balance = toToken.getFormattedAmount(isNeedSymbol = false), + networkIconRes = getActiveIconRes(toToken.currency.network.rawId), + isBalanceHidden = isBalanceHiddenProvider(), + ), + notifications = notificationsFactory.getSwapNotSupportedNotifications(toToken.currency.name), + fee = FeeItemState.Empty, + swapButton = SwapButton( + walletInteractionIcon = walletInterationIcon(userWalletProvider()), + isEnabled = false, + isHoldToConfirm = isHoldToConfirmEnabled, + onClick = { }, + ), + changeCardsButtonState = ChangeCardsButtonState.DISABLED, + providerState = ProviderState.Empty(), + priceImpact = PriceImpact.Empty(), + ) + } + @Suppress("LongParameterList") fun createQuotesLoadingState( uiStateHolder: SwapStateHolder, @@ -438,6 +492,7 @@ internal class StateBuilder( notification is SwapNotificationUM.Warning.ExpressError || notification is SwapNotificationUM.Warning.ExpressGeneralError || notification is SwapNotificationUM.Warning.NoAvailableTokensToSwap || + notification is SwapNotificationUM.Warning.SwapNotSupported || notification is SwapNotificationUM.Warning.NeedReserveToCreateAccount || notification is SwapNotificationUM.Info.PermissionNeeded } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 2bc3cc5c61..a6ddf89de1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -189,7 +189,7 @@ private fun ListOfTokensWithMarkets( state = lazyListState, ) { if (state.tokensListData !is TokenListUMData.EmptyList) { - assetsTitle(count = state.tokensListData.totalTokensCount) + assetsTitle(count = state.tokensListData.totalTokensCount, showCount = marketsState.shouldAssetsCount) } tokensListItems( @@ -197,10 +197,18 @@ private fun ListOfTokensWithMarkets( isBalanceHidden = state.isBalanceHidden, ) - if (state.tokensListData is TokenListUMData.EmptyList) { + tokensToSelectItems(state.availableTokens, state.onTokenSelected) + if (state.unavailableTokens.isNotEmpty()) { item { SpacerH12() } - } else { + tokensToSelectItems(state.unavailableTokens, state.onTokenSelected) + } + + val hasPortfolioContent = state.tokensListData !is TokenListUMData.EmptyList || + state.availableTokens.isNotEmpty() + if (hasPortfolioContent) { item { SpacerH32() } + } else { + item { SpacerH12() } } swapMarketsListItems(marketsState) @@ -242,13 +250,15 @@ private fun VisibleItemsTracker(lazyListState: LazyListState, marketState: SwapM } } -private fun LazyListScope.assetsTitle(count: Int) { +private fun LazyListScope.assetsTitle(count: Int, showCount: Boolean) { item(key = "assets_title") { Text( text = buildAnnotatedString { append(stringResourceSafe(R.string.swap_your_assets_title)) - withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { - append(" $count") + if (showCount) { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(" $count") + } } }, style = TangemTheme.typography.h3, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt index 0f69e31f8b..ced4fbda3e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/market/SwapMarketsListLazyColumn.kt @@ -14,6 +14,7 @@ import com.tangem.common.ui.markets.MarketsListItem import com.tangem.common.ui.markets.MarketsListItemPlaceholder import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.decorations.roundedShapeItemDecoration +import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.market.state.SwapMarketState @@ -24,7 +25,7 @@ internal fun LazyListScope.swapMarketsListItems(state: SwapMarketState) { val totalCount = (state as? SwapMarketState.Content)?.total Text( text = buildAnnotatedString { - append(stringResourceSafe(R.string.markets_common_title)) + append(state.marketsTitle.resolveReference()) if (totalCount != null) { withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { append(" $totalCount") diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt index b9a6ab2d40..8c03c69176 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/preview/SwapSelectTokenPreviewProvider.kt @@ -4,8 +4,10 @@ import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.core.ui.R import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenListUMData @@ -35,6 +37,8 @@ internal class SwapSelectTokenPreviewProvider { onItemClick = { }, visibleIdsChanged = { }, total = TOTAL_ITEMS, + marketsTitle = TextReference.Res(R.string.feed_trending_now), + shouldAssetsCount = false, ) private fun createPreviewMarketItems() = listOf( From fafb6ed65ceb0a74aac735b48d33a2e99db18a81 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Feb 2026 18:09:01 +0500 Subject: [PATCH 07/97] Updated on 2026-08-14 --- .../wallet/state/model/WalletBalanceUM.kt | 62 +++ .../state/model/WalletNotificationUM.kt | 434 ++++++++++++++++++ .../wallet/state/model/WalletState.kt | 10 +- .../wallet/state/model/WalletTokensListUM.kt | 79 ++++ .../wallet/state/model/WalletUM.kt | 50 ++ 5 files changed, 630 insertions(+), 5 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt new file mode 100644 index 0000000000..f55919de64 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt @@ -0,0 +1,62 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.wallet.UserWalletId + +/** Wallet card state */ +@Immutable +internal sealed interface WalletBalanceUM { + + /** Wallet Id */ + val id: UserWalletId + + /** Wallet Name */ + val name: String + + /** + * Wallet card content state + * + * @property id wallet id + * @property name wallet name + * @property balance wallet balance + */ + data class Content( + override val id: UserWalletId, + override val name: String, + val balance: TextReference, + val isBalanceFlickering: Boolean, + val isZeroBalance: Boolean?, + + ) : WalletBalanceUM + + /** + * Wallet card error state + * + * @property id wallet id + * @property name wallet name + */ + data class Error( + override val id: UserWalletId, + override val name: String, + ) : WalletBalanceUM + + /** + * Wallet card loading state + * + * @property id wallet id + * @property name wallet name + */ + data class Loading( + override val id: UserWalletId, + override val name: String, + ) : WalletBalanceUM + + fun copySealed(name: String): WalletBalanceUM { + return when (this) { + is Content -> copy(name = name) + is Error -> copy(name = name) + is Loading -> copy(name = name) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt new file mode 100644 index 0000000000..3111dd0c27 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -0,0 +1,434 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.annotation.DrawableRes +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.message.TangemMessageButtonUM +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.ds.message.TangemMessageUM +import com.tangem.core.ui.extensions.pluralReference +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.res.TangemTheme +import com.tangem.feature.wallet.impl.R +import kotlinx.collections.immutable.persistentListOf + +/** + * Wallet notification types + */ +internal enum class WalletNotificationType { + Status, + Critical, + Warning, + Promo, + Survey, + Informational, +} + +/** + * Wallet notification UI model + * + * @property messageUM - message to show in notification + * @property type - type of notification, affects design and priority + */ +internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val type: WalletNotificationType) { + + data object DevCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DevCardNotification", + title = resourceReference(id = R.string.warning_developer_card_title), + subtitle = resourceReference(id = R.string.warning_developer_card_message), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Warning, + ) + + data object FailedCardValidation : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "FailedCardValidationNotification", + title = resourceReference(id = R.string.warning_failed_to_verify_card_title), + subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), + messageEffect = TangemMessageEffect.Warning, + ), + type = WalletNotificationType.Status, + ) + + data class BackupError(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "BackupErrorNotification", + title = resourceReference(id = R.string.warning_backup_errors_title), + subtitle = resourceReference(id = R.string.warning_backup_errors_message), + messageEffect = TangemMessageEffect.Warning, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_contact_support), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Warning, + ) + + data class SeedPhraseNotification( + val onDeclineClick: () -> Unit, + val onConfirmClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SeedPhraseIssueNotification", + title = resourceReference(id = R.string.warning_seedphrase_issue_title), + subtitle = resourceReference(id = R.string.warning_seedphrase_issue_message), + messageEffect = TangemMessageEffect.Warning, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_no), + type = TangemButtonType.PrimaryInverse, + onClick = onDeclineClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_yes), + type = TangemButtonType.PrimaryInverse, + onClick = onConfirmClick, + ), + ), + ), + type = WalletNotificationType.Critical, + ) + + data class SeedPhraseSecondNotification( + val onDeclineClick: () -> Unit, + val onConfirmClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SeedPhraseSecondIssueNotification", + title = resourceReference(id = R.string.warning_seedphrase_action_required_title), + subtitle = resourceReference(id = R.string.warning_seedphrase_contacted_support), + messageEffect = TangemMessageEffect.Warning, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.seed_warning_no), + type = TangemButtonType.PrimaryInverse, + onClick = onDeclineClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.seed_warning_yes), + type = TangemButtonType.PrimaryInverse, + onClick = onConfirmClick, + ), + ), + ), + type = WalletNotificationType.Critical, + ) + + data class MissingBackup(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "MissingBackupNotification", + title = resourceReference(id = R.string.warning_no_backup_title), + subtitle = resourceReference(id = R.string.warning_no_backup_message), + messageEffect = TangemMessageEffect.Warning, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.button_start_backup_process), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Critical, + ) + + data object SomeNetworksUnreachable : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "SomeNetworksUnreachableNotification", + title = resourceReference(id = R.string.warning_some_networks_unreachable_title), + subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Status, + ) + + data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NumberOfSignedHashesIncorrectNotification", + title = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_title), + subtitle = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_message), + messageEffect = TangemMessageEffect.Warning, + onCloseClick = onCloseClick, + ), + type = WalletNotificationType.Warning, + ) + + data object TestnetCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "TestnetCardNotification", + title = resourceReference(id = R.string.warning_testnet_card_title), + subtitle = resourceReference(id = R.string.warning_testnet_card_message), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Warning, + ) + + data class LowSignatures(val count: Int) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "LowSignaturesNotification", + title = resourceReference(id = R.string.warning_low_signatures_title), + subtitle = resourceReference( + id = R.string.warning_low_signatures_message, + formatArgs = wrappedList(count.toString()), + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Critical, + ) + + data class MissingAddresses( + @DrawableRes val tangemIcon: Int?, + val missingAddressesCount: Int, + val onGenerateClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "MissingAddressesNotification", + title = resourceReference(id = R.string.warning_missing_derivation_title), + subtitle = pluralReference( + id = R.plurals.warning_missing_derivation_message, + count = missingAddressesCount, + formatArgs = wrappedList(missingAddressesCount), + ), + isCentered = true, + iconUM = tangemIcon?.let { TangemIconUM.Icon(it) }, + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.common_generate_addresses), + type = TangemButtonType.PrimaryInverse, + iconRes = tangemIcon, + onClick = onGenerateClick, + ), + ), + ), + type = WalletNotificationType.Warning, + ) + + data class NoAccount(val network: String, val symbol: String, val amount: String) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NoAccountNotification", + title = resourceReference(id = R.string.warning_no_account_title), + subtitle = resourceReference( + id = R.string.no_account_generic, + wrappedList(network, amount, symbol), + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Warning, + ) + + data object DemoCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DemoCardNotification", + title = resourceReference(id = R.string.warning_demo_mode_title), + subtitle = resourceReference(id = R.string.warning_demo_mode_title), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Warning, + ) + + data class NoteMigration(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NoteMigrationNotification", + title = resourceReference(R.string.wallet_promo_banner_title), + subtitle = resourceReference(R.string.wallet_promo_banner_description), + messageEffect = TangemMessageEffect.None, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.wallet_promo_banner_button_title), + onClick = onClick, + type = TangemButtonType.PrimaryInverse, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class UnlockWallets(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "UnlockWalletsNotification", + title = resourceReference(id = R.string.common_access_denied), + subtitle = resourceReference( + id = R.string.warning_access_denied_message, + formatArgs = wrappedList( + resourceReference(R.string.common_biometrics), + ), + ), + onClick = onClick, + messageEffect = TangemMessageEffect.Magic, + isCentered = true, + ), + type = WalletNotificationType.Warning, + ) + + data class RateApp( + val onLikeClick: () -> Unit, + val onDislikeClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "RateAppNotification", + title = resourceReference(id = R.string.warning_rate_app_title), + subtitle = resourceReference(id = R.string.warning_rate_app_message), + isCentered = true, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(id = R.string.warning_button_could_be_better), + type = TangemButtonType.PrimaryInverse, + onClick = onDislikeClick, + ), + TangemMessageButtonUM( + text = resourceReference(id = R.string.warning_button_like_it), + type = TangemButtonType.Primary, + onClick = onLikeClick, + ), + ), + messageEffect = TangemMessageEffect.None, + onCloseClick = onCloseClick, + ), + type = WalletNotificationType.Survey, + ) + + data object UsedOutdatedData : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "UsedOutdatedDataNotification", + title = stringReference("Missing some token balances"), // todo redesign main lokalise + subtitle = stringReference("Will be updated as soon as possible"), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Status, + ) + + data class FinishWalletActivation( + val messageEffect: TangemMessageEffect, + val isBackupExists: Boolean, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "FinishWalletActivationNotification", + title = resourceReference(R.string.hw_activation_need_title), + subtitle = if (isBackupExists) { + resourceReference(R.string.hw_activation_need_warning_description) + } else { + resourceReference(R.string.hw_activation_need_description) + }, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = messageEffect, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.hw_activation_need_finish), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = when (messageEffect) { + TangemMessageEffect.Card -> WalletNotificationType.Critical + else -> WalletNotificationType.Warning + }, + ) + + data class PushNotifications( + val onCloseClick: () -> Unit, + val onEnabledClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "PushNotificationsNotification", + title = resourceReference(R.string.user_push_notification_banner_title), + subtitle = resourceReference(R.string.user_push_notification_banner_subtitle), + onCloseClick = onCloseClick, + messageEffect = TangemMessageEffect.Card, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.common_later), + type = TangemButtonType.PrimaryInverse, + onClick = onCloseClick, + ), + TangemMessageButtonUM( + text = resourceReference(R.string.common_enable), + type = TangemButtonType.Primary, + onClick = onEnabledClick, + ), + ), + ), + type = WalletNotificationType.Informational, + ) + + data class CloreMigration( + val onStartMigrationClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "CloreMigrationNotification", + title = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_title), + subtitle = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_description), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(com.tangem.core.res.R.string.warning_clore_migration_button), + onClick = onStartMigrationClick, + type = TangemButtonType.PrimaryInverse, + ), + ), + ), + type = WalletNotificationType.Informational, + ) + + data class OnePlusOnePromo( + val onCloseClick: () -> Unit, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "OnePlusOnePromoNotification", + title = resourceReference(R.string.notification_one_plus_one_title), + subtitle = resourceReference(R.string.notification_one_plus_one_text), + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_one_plus_one_button), + type = TangemButtonType.PrimaryInverse, + onClick = onCloseClick, + ), + TangemMessageButtonUM( + text = resourceReference(R.string.notification_one_plus_one_button), + type = TangemButtonType.Primary, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class YieldPromo( + val onCloseClick: () -> Unit, + val onTermsAndConditionsClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "YieldPromoNotification", + title = resourceReference(R.string.notification_yield_promo_title), + subtitle = resourceReference(R.string.notification_yield_promo_text), + onCloseClick = onCloseClick, + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_yield_promo_button), + type = TangemButtonType.Primary, + onClick = onTermsAndConditionsClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 20023e6233..12261f3059 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -55,11 +55,6 @@ internal sealed interface WalletState : WalletStateHolder { override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden override val tangemPayState: TangemPayState = TangemPayState.Empty } - - enum class WalletType { - Hot, - Cold, - } } sealed class SingleCurrency : WalletState, TxHistoryStateHolder { @@ -96,4 +91,9 @@ internal sealed interface WalletState : WalletStateHolder { override val marketPriceBlockState: MarketPriceBlockState? = null } } +} + +enum class WalletType { + Hot, + Cold, } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt new file mode 100644 index 0000000000..91ad3be6ab --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListUM.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.row.TangemRowUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +/** + * State of the tokens list in the wallet screen + * + * @property tokenList list of tokens to display + * @property organizeButtonUM configuration for the "Organize Tokens" button, if it should + */ +@Immutable +internal sealed class WalletTokensListUM { + + abstract val tokenList: ImmutableList + abstract val organizeButtonUM: TangemButtonUM? + + data object Empty : WalletTokensListUM() { + override val tokenList: ImmutableList = persistentListOf() + override val organizeButtonUM: TangemButtonUM? = null + } + + data object Loading : WalletTokensListUM() { + override val tokenList: ImmutableList = persistentListOf( + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "0"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "1"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + TokensListItemUM2.Portfolio( + tokenRowUM = TangemTokenRowUM.Loading(id = "2"), + tokenList = persistentListOf(), + isExpanded = false, + isCollapsable = true, + ), + ) + override val organizeButtonUM: TangemButtonUM? = null + } + + data class Content( + override val tokenList: ImmutableList, + override val organizeButtonUM: TangemButtonUM?, + ) : WalletTokensListUM() +} + +/** + * State of token list item in the wallet screen + */ +@Immutable +internal sealed interface TokensListItemUM2 { + val tokenRowUM: TangemRowUM + + data class GroupTitle( + override val tokenRowUM: TangemHeaderRowUM, + ) : TokensListItemUM2 + + data class Token( + override val tokenRowUM: TangemTokenRowUM, + ) : TokensListItemUM2 + + data class Portfolio( + override val tokenRowUM: TangemTokenRowUM, + val tokenList: ImmutableList, + val isExpanded: Boolean, + val isCollapsable: Boolean, + ) : TokensListItemUM2 +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt new file mode 100644 index 0000000000..896c34b9aa --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt @@ -0,0 +1,50 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.ds.button.TangemButtonUM +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +internal sealed interface WalletUM { + + val pullToRefreshConfig: PullToRefreshConfig + val walletsBalanceUM: WalletBalanceUM + + val buttons: PersistentList + val notifications: ImmutableList + + val tokensListUM: WalletTokensListUM + + val nftState: WalletNFTItemUM + + val type: WalletType + + val tangemPayState: TangemPayState + + data class Content( + override val pullToRefreshConfig: PullToRefreshConfig, + override val walletsBalanceUM: WalletBalanceUM, + override val buttons: PersistentList, + override val notifications: ImmutableList, + override val tokensListUM: WalletTokensListUM, + override val nftState: WalletNFTItemUM, + override val type: WalletType, + override val tangemPayState: TangemPayState, + val stackableNotifications: ImmutableList, + ) : WalletUM + + data class Locked( + override val walletsBalanceUM: WalletBalanceUM, + override val buttons: PersistentList, + override val type: WalletType, + override val notifications: ImmutableList = persistentListOf(), + ) : WalletUM { + override val pullToRefreshConfig = PullToRefreshConfig(false, {}) + override val tokensListUM: WalletTokensListUM = WalletTokensListUM.Empty // todo redesign main locked state + override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden + override val tangemPayState: TangemPayState = TangemPayState.Empty + } +} \ No newline at end of file From 8a31a90b027fcee1d9725566677965dfccbd70bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 15:47:36 +0400 Subject: [PATCH 08/97] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 3 + .../java/com/tangem/tap/TangemApplication.kt | 6 +- .../domain/tasks/product/DerivationsFinder.kt | 49 +-- .../DefaultUserWalletsListRepository.kt | 12 +- .../tap/domain/userWalletList/utils/Mapper.kt | 20 +- .../tasks/product/DerivationsFinderTest.kt | 301 ++++++++++++++++++ 6 files changed, 362 insertions(+), 29 deletions(-) create mode 100644 app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 97f825c6ba..611d417b2a 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -17,6 +17,7 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory +import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage @@ -154,4 +155,6 @@ interface ApplicationEntryPoint { fun getABTestsManager(): ABTestsManager fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory + + fun getWalletAccountsFetcher(): WalletAccountsFetcher } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index a22cc0dd22..327b223c07 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -31,6 +31,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory +import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor @@ -246,6 +247,9 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appsFlyerClientFactory: AppsFlyerClient.Factory get() = entryPoint.getAppsFlyerClientFactory() + private val walletAccountsFetcher: WalletAccountsFetcher + get() = entryPoint.getWalletAccountsFetcher() + // endregion private val appScope = MainScope() @@ -349,7 +353,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. } derivationsFinder = DerivationsFinder( - userTokensResponseStore = userTokensResponseStore, + walletAccountsFetcher = walletAccountsFetcher, dispatchers = dispatchers, ) diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt index eb99c14c88..dd5945533b 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt @@ -5,12 +5,12 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.tap.features.demo.DemoHelper import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -22,7 +22,7 @@ internal data class BlockchainToDerive( // FIXME: May be move to DI, currently unnecessary internal class DerivationsFinder( - private val userTokensResponseStore: UserTokensResponseStore, + private val walletAccountsFetcher: WalletAccountsFetcher, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -54,19 +54,19 @@ internal class DerivationsFinder( } // pay attention to this - if (!card.hasOldStyleDerivation) { - blockchains.removeUnnecessaryBlockchains() + return if (!card.hasOldStyleDerivation) { + blockchains.removeUnnecessaryBlockchains(derivationStyle) + } else { + blockchains } - - return blockchains } private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - val responseTokens = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId)?.tokens - ?: return hashSetOf() - - return responseTokens.asSequence() - .filter { it.contractAddress == null } + return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty() + .flatMap { accountDTO -> + accountDTO.tokens.orEmpty() + .filter { it.contractAddress == null } + } .mapNotNull { coin -> val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null val derivationPath = coin.derivationPath?.let(::DerivationPath) @@ -90,22 +90,29 @@ internal class DerivationsFinder( } private fun MutableSet.addEthereumBlockchains(derivationStyle: DerivationStyle?) { - val ethereumBlockchains = setOf(Blockchain.Ethereum, Blockchain.EthereumTestnet) + val ethereumBlockchains = setOf(Blockchain.Ethereum) .mapToBlockchainsWithDerivations(derivationStyle) addAll(ethereumBlockchains) } -private fun MutableSet.removeUnnecessaryBlockchains() { - val unnecessaryBlockchains = listOf( - Blockchain.BSC, Blockchain.BSCTestnet, - Blockchain.Polygon, Blockchain.PolygonTestnet, - Blockchain.RSK, - Blockchain.Fantom, Blockchain.FantomTestnet, - Blockchain.Avalanche, Blockchain.AvalancheTestnet, +private fun Set.removeUnnecessaryBlockchains( + derivationStyle: DerivationStyle?, +): Set { + val defaultEthereum = BlockchainToDerive( + blockchain = Blockchain.Ethereum, + derivationPath = Blockchain.Ethereum.derivationPath(derivationStyle), ) - removeAll { it.blockchain in unnecessaryBlockchains } + val addedEthereum = this.firstOrNull { it == defaultEthereum } + + return if (addedEthereum != null) { + filterNot { it.derivationPath == defaultEthereum.derivationPath && it.blockchain != Blockchain.Ethereum } + } else { + // Impossible case because Ethereum was added at the last stage + distinctBy(BlockchainToDerive::derivationPath) + } + .toSet() } private fun MutableSet.addSecondCardanoDerivationIfPresent() { diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt index a45ca9387a..2f49d686db 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/DefaultUserWalletsListRepository.kt @@ -88,7 +88,7 @@ internal class DefaultUserWalletsListRepository( .map { wallets.updateWith(it) } } .doOnSuccess { loadedWallets -> - userWallets.update { toUpdate -> + userWallets.update { _ -> val selectedUserWalletId = selectedUserWalletRepository.get() selectedUserWallet.value = loadedWallets.firstOrNull { it.walletId == selectedUserWalletId } ?: loadedWallets.firstOrNull()?.also { @@ -240,7 +240,7 @@ internal class DefaultUserWalletsListRepository( } } - @Suppress("CyclomaticComplexMethod") + @Suppress("CyclomaticComplexMethod", "LongMethod") override suspend fun unlock( userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod, @@ -315,7 +315,13 @@ internal class DefaultUserWalletsListRepository( sensitiveInformationRepository.getAll(listOf(encryptionKey)) .doOnSuccess { sensitiveInfo -> - updateWallets { it?.updateWith(sensitiveInfo) } + updateWallets { wallets -> + // It is necessary to update derivations because when scanning we obtain the missing keys + wallets?.updateWith( + walletIdToSensitiveInformation = sensitiveInfo, + walletIdToDerivedKeys = mapOf(userWallet.walletId to scanResponse.derivedKeys), + ) + } trackSignInEvent(userWallet, Basic.SignedIn.SignInType.Card) } .doOnFailure { error -> raise(UnlockWalletError.UnableToUnlock.RawException(error)) } diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index c9184f1335..51aefb10ab 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -1,8 +1,10 @@ package com.tangem.tap.domain.userWalletList.utils +import com.tangem.domain.models.scan.KeyWalletPublicKey import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isMultiCurrency +import com.tangem.operations.derivation.ExtendedPublicKeysMap import com.tangem.tap.domain.userWalletList.model.UserWalletPublicInformation import com.tangem.tap.domain.userWalletList.model.UserWalletSensitiveInformation @@ -72,7 +74,10 @@ internal fun List.toUserWallets(): List return this.map { it.toUserWallet() } } -internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInformation): UserWallet { +internal fun UserWallet.updateWith( + sensitiveInformation: UserWalletSensitiveInformation, + derivedKeys: Map?, +): UserWallet { return when (this) { is UserWallet.Cold -> { copy( @@ -80,6 +85,7 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo card = scanResponse.card.copy( wallets = requireNotNull(sensitiveInformation.wallets), ), + derivedKeys = derivedKeys ?: scanResponse.derivedKeys, // visaCardActivationStatus = sensitiveInformation.visaCardActivationStatus, ), ) @@ -92,14 +98,20 @@ internal fun UserWallet.updateWith(sensitiveInformation: UserWalletSensitiveInfo internal fun List.updateWith( walletIdToSensitiveInformation: Map, + walletIdToDerivedKeys: Map>? = null, ): List { return if (walletIdToSensitiveInformation.isEmpty()) { this } else { this.map { wallet -> - walletIdToSensitiveInformation[wallet.walletId] - ?.let(wallet::updateWith) - ?: wallet + val sensitiveInformation = walletIdToSensitiveInformation[wallet.walletId] + val derivedKeys = walletIdToDerivedKeys?.get(wallet.walletId) + + if (sensitiveInformation != null) { + wallet.updateWith(sensitiveInformation, derivedKeys) + } else { + wallet + } } } } diff --git a/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt new file mode 100644 index 0000000000..64e3006815 --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt @@ -0,0 +1,301 @@ +package com.tangem.tap.domain.tasks.product + +import com.google.common.truth.Truth +import com.tangem.blockchain.blockchains.cardano.CardanoUtils +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.toNetworkId +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.account.WalletAccountsFetcher +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.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.* +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +/** +[REDACTED_AUTHOR] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class DerivationsFinderTest { + + private val walletAccountsFetcher = mockk() + private val finder = DerivationsFinder( + walletAccountsFetcher = walletAccountsFetcher, + dispatchers = TestingCoroutineDispatcherProvider(), + ) + + private val derivationStyleProvider = mockk() + + @AfterEach + fun tearDown() { + clearMocks(walletAccountsFetcher, derivationStyleProvider) + } + + @Test + fun `GIVEN card is not HD wallet THEN return empty set`() = runTest { + // Arrange + val card = mockk { + every { this@mockk.settings.isHDWalletAllowed } returns false + } + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = mockk()) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `GIVEN card has empty wallets THEN return empty set`() = runTest { + // Arrange + val card = mockk { + every { this@mockk.settings.isHDWalletAllowed } returns true + every { this@mockk.wallets } returns emptyList() + } + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = mockk()) + + // Assert + Truth.assertThat(actual).isEmpty() + } + + @Test + fun `GIVEN saved bitcoin THEN return bitcoin and ethereum`() = runTest { + // Arrange + val card = createCardDTO() + + every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 + + val response = createResponse(Blockchain.Bitcoin) + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store and common demo card THEN return demo blockchains`() = runTest { + // Arrange + val demoCardId = "AC01000000045754" + val card = createCardDTO(cardId = demoCardId) + + every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + createExpected(Blockchain.Dogecoin), + createExpected(Blockchain.Solana), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store and DE00 demo card THEN return demo blockchains`() = runTest { + // Arrange + val demoCardId = "DE00" + val card = createCardDTO(cardId = demoCardId) + + every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + createExpected(Blockchain.Dogecoin), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN empty store THEN return default blockchains`() = runTest { + // Arrange + val card = createCardDTO() + + every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + + // Assert + val expected = setOf( + createExpected(Blockchain.Bitcoin), + createExpected(Blockchain.Ethereum), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN saved cardano THEN return cardano and ethereum`() = runTest { + // Arrange + val card = createCardDTO() + + every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 + + val response = createResponse(Blockchain.Cardano) + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + + // Assert + val expected = setOf( + createExpected(Blockchain.Cardano), + createExpected( + blockchain = Blockchain.Cardano, + derivationPath = CardanoUtils.extendedDerivationPath(Blockchain.Cardano.getDerivationPath()) + ), + createExpected(Blockchain.Ethereum), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN saved eth-like blockchains for v3 config wallet THEN return only unique evm derivations`() = runTest { + // Arrange + val card = createCardDTO() + + every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 + + val blockchains = Blockchain.entries + .filter { it.isEvm() && !it.isTestnet() } + .toTypedArray() + + val response = createResponse(*blockchains) + + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + + // Assert + val expected = setOf( + createExpected(Blockchain.Ethereum), + createExpected(Blockchain.EthereumClassic), + createExpected(Blockchain.Quai), + createExpected(Blockchain.XDC), + ) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + @Test + fun `GIVEN card has old style derivation (v1 config) THEN return all eth-like blockchains`() = runTest { + // Arrange + val oldBatchId = "AC01" + val card = createCardDTO(batchId = oldBatchId) + + every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V1 + + val blockchains = Blockchain.entries + .filter { it.isEvm() && !it.isTestnet() } + .toTypedArray() + + val response = createResponse(*blockchains) + coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response + + // Act + val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + + // Assert + val expected = blockchains.mapTo(hashSetOf(), ::createExpected) + + Truth.assertThat(actual).containsExactlyElementsIn(expected) + + coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } + } + + private fun createCardDTO(cardId: String = "0001", batchId: String = "AC10"): CardDTO { + val wallet = mockk { + every { this@mockk.publicKey } returns byteArrayOf(0) + } + + return mockk { + every { this@mockk.cardId } returns cardId + every { this@mockk.batchId } returns batchId + every { this@mockk.settings.isHDWalletAllowed } returns true + every { this@mockk.wallets } returns listOf(wallet) + } + } + + private fun createResponse(vararg blockchains: Blockchain): GetWalletAccountsResponse { + val tokens = blockchains.map { blockchain -> + mockk { + every { this@mockk.networkId } returns blockchain.toNetworkId() + every { this@mockk.derivationPath } returns blockchain.getDerivationPath().rawPath + every { this@mockk.contractAddress } returns null + } + } + + val account = mockk { + every { this@mockk.tokens } returns tokens + } + + return mockk { + every { this@mockk.accounts } returns listOf(account) + } + } + + private fun createExpected( + blockchain: Blockchain, + derivationPath: DerivationPath = blockchain.getDerivationPath(), + ): BlockchainToDerive { + return BlockchainToDerive(blockchain = blockchain, derivationPath = derivationPath) + } + + private fun Blockchain.getDerivationPath(): DerivationPath { + return derivationPath(derivationStyleProvider.getDerivationStyle())!! + } + + private companion object { + + // for byteArrayOf(0) + val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7") + } +} \ No newline at end of file From 1f38b69c294ddd191b8ae1bf993e20e572d788c6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 15:16:18 +0500 Subject: [PATCH 09/97] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 6d72aefb0d..0f1801a9b1 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "develop-1425" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-577" +tangemCardSdk = "develop-578" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From ccad5a6c81b61cb1d02529698b4c177f53dc0060 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 18:20:20 +0500 Subject: [PATCH 10/97] Updated on 2026-08-14 --- ...ltHotWalletAccessCodeAttemptsRepository.kt | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt index 8a77f2f70a..d3887a70c3 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/hot/DefaultHotWalletAccessCodeAttemptsRepository.kt @@ -47,18 +47,11 @@ class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( hotWalletId = hotWalletId, auth = true, ) - val noAuthAttemptId = HotWalletAccessCodeAttemptsRepository.AttemptId( - hotWalletId = hotWalletId, - auth = false, - ) - appPreferencesStore.editData { - it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(noAuthAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockBootKey(noAuthAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) - it.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(noAuthAttemptId.attemptIdKey())) + appPreferencesStore.editData { data -> + data.remove(PreferencesKeys.getHotWalletUnlockAttemptsKey(authAttemptId.attemptIdKey())) + data.remove(PreferencesKeys.getHotWalletUnlockBootKey(authAttemptId.attemptIdKey())) + data.remove(PreferencesKeys.getHotWalletUnlockDeadlineKey(authAttemptId.attemptIdKey())) } } @@ -119,13 +112,21 @@ class DefaultHotWalletAccessCodeAttemptsRepository @Inject constructor( } else -> { val remaining = remainingSeconds(deadlineElapsed, bootStored) - Attempts.WithDelay(count, remaining) + val newCount = if (id.auth) { + count + } else { + MAX_FAST_FORWARD_ATTEMPTS + } + Attempts.WithDelay(newCount, remaining) } } } private fun HotWalletAccessCodeAttemptsRepository.AttemptId.attemptIdKey(): String { - return "${hotWalletId.value}_$auth" + // Regarding [REDACTED_TASK_KEY], the attempts counter must be shared between modes (auth vs signing). + // To provide backward compatibility, we use the same keys but read attempts in auth mode for security reasons. + val isAuthMode = true + return "${hotWalletId.value}_$isAuthMode" } private fun currentBootCount(): Int = Settings.Global.getInt(context.contentResolver, Settings.Global.BOOT_COUNT, 0) From 0001d4643552fe87185362a1a8699da337408d9d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 13 Feb 2026 18:09:16 +0500 Subject: [PATCH 11/97] Updated on 2026-08-14 --- .../model/WalletsUpdateActionResolver.kt | 5 ++- .../common/preview/WalletScreenPreviewData.kt | 3 +- .../wallet/state/WalletStateController.kt | 43 ++++++++++++++++--- .../state/model/WalletNotificationUM.kt | 7 +-- .../wallet/state/model/WalletScreenState.kt | 1 + .../CloseBottomSheetTransformer.kt | 5 +++ .../InitializeWalletsTransformer.kt | 1 - .../OpenBottomSheetTransformer.kt | 5 +++ .../ReinitializeWalletTransformer.kt | 5 +++ .../RemoveNFTCollectionsTransformer.kt | 5 +++ .../SetCryptoCurrencyActionsTransformer.kt | 5 +++ .../SetExpressStatusesTransformer.kt | 5 +++ .../SetNFTCollectionsTransformer.kt | 5 +++ .../SetPrimaryCurrencyTransformer.kt | 5 +++ .../SetRefreshStateTransformer.kt | 5 +++ .../SetTokenListErrorTransformer.kt | 5 +++ .../transformers/SetTokenListTransformer.kt | 5 +++ .../SetTxHistoryCountErrorTransformer.kt | 5 +++ .../SetTxHistoryCountTransformer.kt | 5 +++ .../SetTxHistoryItemsErrorTransformer.kt | 5 +++ .../SetTxHistoryItemsTransformer.kt | 5 +++ .../transformers/SetWarningsTransformer.kt | 5 +++ .../TangemPayExposedDeviceTransformer.kt | 5 +++ .../TangemPayHiddenStateTransformer.kt | 5 +++ ...TangemPayHideOnboardingStateTransformer.kt | 5 +++ .../TangemPayLoadingStateTransformer.kt | 5 +++ ...ngemPayOnboardingBannerStateTransformer.kt | 5 +++ .../TangemPayRefreshNeededStateTransformer.kt | 5 +++ ...TangemPayRefreshShowProgressTransformer.kt | 5 +++ .../TangemPayUnavailableStateTransformer.kt | 5 +++ .../TangemPayUpdateInfoStateTransformer.kt | 5 +++ ...MultiWalletActionButtonBadgeTransformer.kt | 5 +++ .../UpdateWalletCardsCountTransformer.kt | 5 +++ .../transformers/WalletStateTransformer.kt | 8 ++++ .../state/utils/WalletLoadingStateFactory.kt | 4 +- 35 files changed, 188 insertions(+), 19 deletions(-) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt index 1f63082dad..fccfccfd12 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletsUpdateActionResolver.kt @@ -12,6 +12,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletType import timber.log.Timber import javax.inject.Inject @@ -110,7 +111,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( when (walletState) { is WalletState.MultiCurrency -> { val wallet = wallets.firstOrNull { it.walletId == walletState.walletCardState.id } - walletState.type == WalletState.MultiCurrency.WalletType.Hot && wallet is UserWallet.Cold + walletState.type == WalletType.Hot && wallet is UserWallet.Cold } else -> false } @@ -212,7 +213,7 @@ internal class WalletsUpdateActionResolver @Inject constructor( val previousState = state.wallets.firstOrNull { it.walletCardState.id == wallet.walletId } ?: return@filter false wallet is UserWallet.Cold && previousState is WalletState.MultiCurrency && - previousState.type == WalletState.MultiCurrency.WalletType.Hot + previousState.type == WalletType.Hot } return Action.ReinitializeWallets(selectedWallet, walletsToUpdate) } 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 59f9ca6314..78b73ebfa5 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 @@ -186,7 +186,7 @@ internal object WalletScreenPreviewData { onItemClick = { }, ), tangemPayState = TangemPayState.Empty, - type = WalletState.MultiCurrency.WalletType.Cold, + type = WalletType.Cold, ) } @@ -218,6 +218,7 @@ internal object WalletScreenPreviewData { singleWalletLockedState, multiWalletState, ), + wallets2 = persistentListOf(), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index a39274838f..8f877c0751 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -1,12 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.core.ui.event.consumedEvent import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.WalletScreenStateTransformer @@ -25,7 +23,9 @@ import javax.inject.Singleton [REDACTED_AUTHOR] */ @Singleton -internal class WalletStateController @Inject constructor() { +internal class WalletStateController @Inject constructor( + private val designFeatureToggles: DesignFeatureToggles, +) { val uiState: StateFlow get() = mutableUiState @@ -53,6 +53,10 @@ internal class WalletStateController @Inject constructor() { return value.wallets.firstOrNull { it.walletCardState.id == userWalletId } } + fun getWalletUM(userWalletId: UserWalletId): WalletUM? { + return value.wallets2.firstOrNull { it.walletsBalanceUM.id == userWalletId } + } + fun getWalletStateIfSelected(walletId: UserWalletId): WalletState? { val selectedWalletId = getSelectedWalletId() @@ -61,16 +65,40 @@ internal class WalletStateController @Inject constructor() { } } + fun getWalletUMIfSelected(walletId: UserWalletId): WalletUM? { + val selectedWalletId = getSelectedWalletId() + + return value.wallets2.firstOrNull { + it.walletsBalanceUM.id == walletId && it.walletsBalanceUM.id == selectedWalletId + } + } + fun getSelectedWallet(): WalletState { return with(value) { wallets[selectedWalletIndex] } } + fun getSelectedWalletUM(): WalletUM { + return with(value) { wallets2[selectedWalletIndex] } + } + fun getSelectedWalletId(): UserWalletId { - return with(value) { wallets[selectedWalletIndex].walletCardState.id } + return with(value) { + if (designFeatureToggles.isRedesignEnabled) { + wallets2[selectedWalletIndex].walletsBalanceUM.id + } else { + wallets[selectedWalletIndex].walletCardState.id + } + } } fun getWalletIndexByWalletId(userWalletId: UserWalletId): Int? { - return with(value) { wallets.indexOfFirstOrNull { it.walletCardState.id == userWalletId } } + return with(value) { + if (designFeatureToggles.isRedesignEnabled) { + wallets2.indexOfFirstOrNull { it.walletsBalanceUM.id == userWalletId } + } else { + wallets.indexOfFirstOrNull { it.walletCardState.id == userWalletId } + } + } } fun showBottomSheet( @@ -105,6 +133,7 @@ internal class WalletStateController @Inject constructor() { topBarConfig = WalletTopBarConfig(onDetailsClick = {}), selectedWalletIndex = NOT_INITIALIZED_WALLET_INDEX, wallets = persistentListOf(), + wallets2 = persistentListOf(), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt index 3111dd0c27..eba6f7dbbe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -227,7 +227,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t messageUM = TangemMessageUM( id = "DemoCardNotification", title = resourceReference(id = R.string.warning_demo_mode_title), - subtitle = resourceReference(id = R.string.warning_demo_mode_title), + subtitle = resourceReference(id = R.string.warning_demo_mode_message), messageEffect = TangemMessageEffect.None, ), type = WalletNotificationType.Warning, @@ -401,11 +401,6 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t type = TangemButtonType.PrimaryInverse, onClick = onCloseClick, ), - TangemMessageButtonUM( - text = resourceReference(R.string.notification_one_plus_one_button), - type = TangemButtonType.Primary, - onClick = onClick, - ), ), ), type = WalletNotificationType.Promo, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index 9c6f2658ae..fa655d1daf 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -9,6 +9,7 @@ internal data class WalletScreenState( val topBarConfig: WalletTopBarConfig, val selectedWalletIndex: Int, val wallets: ImmutableList, + val wallets2: ImmutableList, val onWalletChange: (index: Int, onlyState: Boolean) -> Unit, val event: StateEvent, val isHidingMode: Boolean, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt index a76c101e75..3380c53a2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/CloseBottomSheetTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { @@ -22,6 +23,10 @@ internal class CloseBottomSheetTransformer(userWalletId: UserWalletId) : WalletS } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun updateConfig(prevState: WalletState) = prevState.bottomSheetConfig?.copy( isShown = false, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 559a45db9d..b7594959d3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -7,7 +7,6 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState.MultiCurrency.WalletType import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.state.utils.createStateByWalletType import kotlinx.collections.immutable.PersistentList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt index 74aaa23e95..b0bf9f08c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/OpenBottomSheetTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class OpenBottomSheetTransformer( userWalletId: UserWalletId, @@ -28,6 +29,10 @@ internal class OpenBottomSheetTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun updateConfig() = TangemBottomSheetConfig( isShown = true, onDismissRequest = onDismissBottomSheet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 4459476877..bcfae039a0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory /** @@ -26,6 +27,10 @@ internal class ReinitializeWalletTransformer( ) } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + override fun transform(prevState: WalletState): WalletState { return walletLoadingStateFactory.create( userWallet = userWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt index 0ccb1652e4..9308e8370f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RemoveNFTCollectionsTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class RemoveNFTCollectionsTransformer( userWalletId: UserWalletId, @@ -17,4 +18,8 @@ internal class RemoveNFTCollectionsTransformer( is WalletState.SingleCurrency.Locked, -> prevState } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file 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 f414b696e9..df4942ff07 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 @@ -8,6 +8,7 @@ import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList import timber.log.Timber @@ -35,6 +36,10 @@ internal class SetCryptoCurrencyActionsTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TokenActionsState.toManageButtons(): PersistentList { return states .filterIfS2C() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt index d41cf131aa..9d0afb1eff 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetExpressStatusesTransformer.kt @@ -10,6 +10,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.onramp.model.cache.OnrampTransaction import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletOnrampTransactionConverter import kotlinx.collections.immutable.toPersistentList import timber.log.Timber @@ -56,6 +57,10 @@ internal class SetExpressStatusesTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TangemBottomSheetConfig.updateStateWithExpressStatusBottomSheet( expressState: ExpressTransactionStateUM?, ): TangemBottomSheetConfig { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt index 4d73f70ed2..10a816e576 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.allLoadedCollectionsEmpty import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toPersistentList internal class SetNFTCollectionsTransformer( @@ -28,6 +29,10 @@ internal class SetNFTCollectionsTransformer( -> prevState } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun createContentNFTItemUM(onItemClick: () -> Unit): WalletNFTItemUM.Content { val collectionsContent = nftCollections .map { it.content } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt index b09dc47d03..ab69b7a4ce 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletMarketPriceConverter import timber.log.Timber @@ -35,6 +36,10 @@ internal class SetPrimaryCurrencyTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun WalletCardState.toLoadedSingleCurrencyState(): WalletCardState { return SingleWalletCardStateConverter(status.value, userWallet, appCurrency).convert(value = this) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 1ad7d9cb18..985e877682 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton 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.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate @@ -33,6 +34,10 @@ internal class SetRefreshStateTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun PullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): PullToRefreshConfig { return copy(isRefreshing = isRefreshing) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 41d72e9aeb..9ecb89cabb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -10,6 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState 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.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons import timber.log.Timber import java.math.BigDecimal @@ -51,6 +52,10 @@ internal class SetTokenListErrorTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun WalletCardState.toLoadedState(): WalletCardState { return WalletCardState.Content( id = id, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 8a7a452474..27a1171ace 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -8,6 +8,7 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState 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.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons @@ -45,6 +46,10 @@ internal class SetTokenListTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun WalletCardState.toLoadedState(): WalletCardState { val fiatBalance = when (params) { is TokenConverterParams.Account -> params.accountList.totalFiatBalance diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt index f4fcbf57fc..9a2b1164f9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountErrorTransformer.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -35,6 +36,10 @@ internal class SetTxHistoryCountErrorTransformer( ) } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.SingleCurrency.Content -> prevState.copy(txHistoryState = createErrorState()) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt index f89b8af13e..cba6ac874b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryCountTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update import timber.log.Timber @@ -33,6 +34,10 @@ internal class SetTxHistoryCountTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TxHistoryState.toLoadingState(): TxHistoryState { return if (this is TxHistoryState.Content) { Timber.d("Load transactions history: $transactionsCount") diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt index d16f26c066..b8af717c52 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsErrorTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import timber.log.Timber internal class SetTxHistoryItemsErrorTransformer( @@ -27,6 +28,10 @@ internal class SetTxHistoryItemsErrorTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun createErrorState(): TxHistoryState.Error = when (error) { is TxHistoryListError.DataError -> { TxHistoryState.Error( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt index d94f18179b..289daa4514 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTxHistoryItemsTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemFlowConverter import kotlinx.coroutines.flow.Flow import timber.log.Timber @@ -32,6 +33,10 @@ internal class SetTxHistoryItemsTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun TxHistoryState.toContentState(): TxHistoryState { val converter = TxHistoryItemFlowConverter( currentState = this, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt index 45b0aabd50..3a20a52812 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.ImmutableList import timber.log.Timber @@ -23,4 +24,8 @@ internal class SetWarningsTransformer( } } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt index b8a735a4f4..cd344f9f4f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayExposedDeviceTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayExposedDeviceTransformer( userWalletId: UserWalletId, @@ -14,4 +15,8 @@ internal class TangemPayExposedDeviceTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt index 171cb8aaed..bbb5035c07 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHiddenStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayHiddenStateTransformer( userWalletId: UserWalletId, @@ -15,4 +16,8 @@ internal class TangemPayHiddenStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt index 9e4e6bafd7..8c1f641c38 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayHideOnboardingStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayHideOnboardingStateTransformer( userWalletId: UserWalletId, @@ -15,4 +16,8 @@ internal class TangemPayHideOnboardingStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt index f731cb119d..6404796e7d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayLoadingStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayLoadingStateTransformer(userWalletId: UserWalletId) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -12,4 +13,8 @@ internal class TangemPayLoadingStateTransformer(userWalletId: UserWalletId) : Wa prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt index 15b2ad4dee..4659e5f486 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayOnboardingBannerStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayOnboardingBannerStateTransformer( userWalletId: UserWalletId, @@ -22,4 +23,8 @@ internal class TangemPayOnboardingBannerStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt index 98a3ee0caa..e2c4d1a6c4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshNeededStateTransformer.kt @@ -7,6 +7,7 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayRefreshNeededStateTransformer( userWalletId: UserWalletId, @@ -32,4 +33,8 @@ internal class TangemPayRefreshNeededStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt index cd5885b406..b37a6f916f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayRefreshShowProgressTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayRefreshShowProgressTransformer( userWalletId: UserWalletId, @@ -21,4 +22,8 @@ internal class TangemPayRefreshShowProgressTransformer( ), ) } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt index 5cdc1926c9..5b2a7765a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUnavailableStateTransformer.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM internal class TangemPayUnavailableStateTransformer( userWalletId: UserWalletId, @@ -20,4 +21,8 @@ internal class TangemPayUnavailableStateTransformer( prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index c995e3dd44..c07d25bdfc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -18,6 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.TangemPayState. import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.domain.pay.model.CustomerInfo.KycStatus.APPROVED import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import java.util.Currency /** @@ -42,6 +43,10 @@ internal class TangemPayUpdateInfoStateTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun createInitialState(): TangemPayState { val cardInfo = value.info.cardInfo val productInstance = value.info.productInstance diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt index b723034b32..b6ad62458f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateMultiWalletActionButtonBadgeTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.showSwapBadge internal class UpdateMultiWalletActionButtonBadgeTransformer( @@ -16,4 +17,8 @@ internal class UpdateMultiWalletActionButtonBadgeTransformer( else -> prevState } } + + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt index 3d29e45a4a..72c403ae19 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UpdateWalletCardsCountTransformer.kt @@ -6,6 +6,7 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import timber.log.Timber internal class UpdateWalletCardsCountTransformer( @@ -30,6 +31,10 @@ internal class UpdateWalletCardsCountTransformer( } } + override fun transform(walletUM: WalletUM): WalletUM { + return walletUM // todo redesign main + } + private fun WalletCardState.toUpdatedState(): WalletCardState { return when (this) { is WalletCardState.Content -> copy( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt index be06139bf9..5aa3c34900 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/WalletStateTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList internal abstract class WalletStateTransformer( @@ -11,6 +12,8 @@ internal abstract class WalletStateTransformer( abstract fun transform(prevState: WalletState): WalletState + abstract fun transform(walletUM: WalletUM): WalletUM + final override fun transform(prevState: WalletScreenState): WalletScreenState { return prevState.copy( wallets = prevState.wallets @@ -18,6 +21,11 @@ internal abstract class WalletStateTransformer( if (state.walletCardState.id == userWalletId) transform(state) else state } .toImmutableList(), + wallets2 = prevState.wallets2 + .map { walletUM -> + if (walletUM.walletsBalanceUM.id == userWalletId) transform(walletUM) else walletUM + } + .toImmutableList(), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index b38da828a2..a7c1027dab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -52,7 +52,7 @@ internal class WalletLoadingStateFactory( bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, nftState = WalletNFTItemUM.Hidden, - type = WalletState.MultiCurrency.WalletType.Hot, + type = WalletType.Hot, tangemPayState = TangemPayState.Empty, ) } @@ -66,7 +66,7 @@ internal class WalletLoadingStateFactory( bottomSheetConfig = null, tokensListState = WalletTokensListState.ContentState.Loading, nftState = WalletNFTItemUM.Hidden, - type = WalletState.MultiCurrency.WalletType.Cold, + type = WalletType.Cold, tangemPayState = TangemPayState.Empty, ) } From 7bee3e60cb9eed080dd3c556a61aba74733782a1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 08:52:01 +0100 Subject: [PATCH 12/97] Updated on 2026-08-14 --- .../feed/model/earn/EarnTokensListConfigFactory.kt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt index 08845ff277..68a5a99bfe 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnTokensListConfigFactory.kt @@ -23,7 +23,9 @@ internal fun createEarnTokensListConfig( earnNetworks.fold( ifLeft = { null }, ifRight = { networks -> - networks.filter(EarnNetwork::isAdded).map(EarnNetwork::networkId) + networks.filter(EarnNetwork::isAdded) + .map(EarnNetwork::networkId) + .ifEmpty { listOf(NO_ONE_NETWORK) } }, ) } @@ -34,4 +36,9 @@ internal fun createEarnTokensListConfig( networks = networks, isForEarn = isForEarn, ) -} \ No newline at end of file +} + +/** + * This id means that backend has to return empty result + */ +private const val NO_ONE_NETWORK = "-1" \ No newline at end of file From c0ae099b6b9eb5b3984848b76600d902e2db9c94 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 08:52:29 +0100 Subject: [PATCH 13/97] Updated on 2026-08-14 --- .../features/feed/model/earn/EarnModel.kt | 20 +++++++++++++------ .../EarnFilterSelectedStateTransformer.kt | 7 ++----- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 822e18aee3..2da2a4a8e1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -32,11 +32,7 @@ import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConv import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeConverter import com.tangem.features.feed.model.earn.filters.state.EarnFilterTypeUMConverter import com.tangem.features.feed.model.earn.state.EarnStateController -import com.tangem.features.feed.model.earn.state.transformers.EarnFilterSelectedStateTransformer -import com.tangem.features.feed.model.earn.state.transformers.UpdateBestOpportunitiesStateTransformer -import com.tangem.features.feed.model.earn.state.transformers.UpdateEarnUMInitialStateTransformer -import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateLoadingTransformer -import com.tangem.features.feed.model.earn.state.transformers.UpdateMostlyUsedStateTransformer +import com.tangem.features.feed.model.earn.state.transformers.* import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM @@ -126,7 +122,10 @@ internal class EarnModel @Inject constructor( error = error, paginationStatus = paginationStatus, hasActiveFilters = hasActiveFilters, - onRetryClick = { batchFlowManager.reload() }, + onRetryClick = { + batchFlowManager.reload() + reloadEarnNetworks() + }, onLoadMore = { batchFlowManager.loadMore() }, onClearFiltersClick = ::onClearFiltersClick, ) @@ -188,6 +187,14 @@ internal class EarnModel @Inject constructor( } } + private fun reloadEarnNetworks() { + modelScope.launch(dispatchers.default) { + if (earnNetworks.value.isLeft()) { + fetchEarnNetworks() + } + } + } + /* start of clicks area */ private fun onTypeFilterClick() { val currentState = state.value @@ -289,6 +296,7 @@ internal class EarnModel @Inject constructor( ), ) bottomSheetNavigation.dismiss() + reloadEarnNetworks() } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt index 17cd292a95..3258d8f251 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt @@ -1,7 +1,6 @@ package com.tangem.features.feed.model.earn.state.transformers import com.tangem.domain.models.earn.EarnNetworks -import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnUM @@ -13,14 +12,12 @@ internal class EarnFilterSelectedStateTransformer( ) : EarnUMTransformer { override fun transform(prevState: EarnUM): EarnUM { - val isFiltersApplicable = prevState.bestOpportunities !is EarnBestOpportunitiesUM.Error - val isNetworkFilterEnabled = earnNetworks.isRight() return prevState.copy( earnFilterUM = prevState.earnFilterUM.copy( selectedTypeFilter = filterType, selectedNetworkFilter = filterNetwork, - isNetworkFilterEnabled = isFiltersApplicable && isNetworkFilterEnabled, - isTypeFilterEnabled = isFiltersApplicable, + isNetworkFilterEnabled = earnNetworks.isRight(), + isTypeFilterEnabled = true, ), ) } From c1fbec8dab4e5d70b9b400b57e84ac882f052e84 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 09:15:55 +0100 Subject: [PATCH 14/97] Updated on 2026-08-14 --- .../main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 2da2a4a8e1..d5f3cd4918 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -101,7 +101,6 @@ internal class EarnModel @Inject constructor( init { updateInitialState() fetchEarnNetworks() - fetchTopEarnTokens() subscribeOnStoredFilters() subscribeOnNetworks() subscribeOnBatchFlow() From b6ab30c56efa8e408ff71ef8e4479c69fe4ce555 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 09:17:46 +0100 Subject: [PATCH 15/97] Updated on 2026-08-14 --- .../tangem/features/feed/model/earn/EarnModel.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index d5f3cd4918..6d74dd4195 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -35,6 +35,7 @@ import com.tangem.features.feed.model.earn.state.EarnStateController import com.tangem.features.feed.model.earn.state.transformers.* import com.tangem.features.feed.model.earn.statemanager.EarnListBatchFlowManager import com.tangem.features.feed.model.earn.statemanager.EarnListStateManager +import com.tangem.features.feed.ui.earn.state.EarnBestOpportunitiesUM import com.tangem.features.feed.ui.earn.state.EarnFilterNetworkUM import com.tangem.features.feed.ui.earn.state.EarnFilterTypeUM import com.tangem.features.feed.ui.earn.state.EarnUM @@ -333,11 +334,13 @@ internal class EarnModel @Inject constructor( is ApiResponseError.HttpException -> error.code.numericCode to error.message.orEmpty() else -> null to "" } - analyticsEventHandler.send( - EarnAnalyticsEvent.BestOpportunitiesLoadError( - code = code, - message = message, - ), - ) + if (state.value.bestOpportunities !is EarnBestOpportunitiesUM.Error) { + analyticsEventHandler.send( + EarnAnalyticsEvent.BestOpportunitiesLoadError( + code = code, + message = message, + ), + ) + } } } \ No newline at end of file From 040b5f0cf6b396659a9908e16890f49243b75fb6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sun, 15 Feb 2026 11:18:11 +0400 Subject: [PATCH 16/97] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + .../tap/data/DefaultOfframpRepository.kt | 29 ++++ .../tap/di/domain/OnrampDomainModule.kt | 16 +++ .../tap/data/DefaultOfframpRepositoryTest.kt | 134 ++++++++++++++++++ .../models/event/OfframpAnalyticsEvent.kt | 17 +++ domain/offramp/.gitignore | 1 + domain/offramp/build.gradle.kts | 18 +++ .../domain/offramp/GetOfframpUrlUseCase.kt | 41 ++++++ .../offramp/repository/OfframpRepository.kt | 19 +++ .../offramp/GetOfframpUrlUseCaseTest.kt | 127 +++++++++++++++++ settings.gradle.kts | 1 + 11 files changed, 404 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt create mode 100644 app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt create mode 100644 core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt create mode 100644 domain/offramp/.gitignore create mode 100644 domain/offramp/build.gradle.kts create mode 100644 domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt create mode 100644 domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt create mode 100644 domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a1514e89d5..7b732b629d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -152,6 +152,7 @@ dependencies { implementation(projects.domain.manageTokens) implementation(projects.domain.nft) implementation(projects.domain.nft.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.promo) implementation(projects.domain.promo.models) diff --git a/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt b/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt new file mode 100644 index 0000000000..89c89aef25 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultOfframpRepository.kt @@ -0,0 +1,29 @@ +package com.tangem.tap.data + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.offramp.repository.OfframpRepository +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.network.exchangeServices.SellService + +/** + * Default implementation of [OfframpRepository] + * + * @property sellService sell service for getting offramp URL + */ +internal class DefaultOfframpRepository( + private val sellService: SellService, +) : OfframpRepository { + + override fun getOfframpUrl( + cryptoCurrency: CryptoCurrency, + fiatCurrencyCode: String, + walletAddress: String, + ): String? { + return sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt index 34ffd4aaba..ad1ba0be09 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/OnrampDomainModule.kt @@ -1,9 +1,13 @@ package com.tangem.tap.di.domain +import com.tangem.domain.offramp.GetOfframpUrlUseCase +import com.tangem.domain.offramp.repository.OfframpRepository import com.tangem.domain.onramp.* import com.tangem.domain.onramp.repositories.* import com.tangem.domain.promo.PromoRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.tap.data.DefaultOfframpRepository +import com.tangem.tap.network.exchangeServices.SellService import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -275,4 +279,16 @@ internal object OnrampDomainModule { promoRepository = promoRepository, ) } + + @Provides + @Singleton + fun provideOfframpRepository(sellService: SellService): OfframpRepository { + return DefaultOfframpRepository(sellService) + } + + @Provides + @Singleton + fun provideGetOfframpUrlUseCase(offrampRepository: OfframpRepository): GetOfframpUrlUseCase { + return GetOfframpUrlUseCase(offrampRepository) + } } \ No newline at end of file diff --git a/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt b/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt new file mode 100644 index 0000000000..16ba7e686f --- /dev/null +++ b/app/src/test/kotlin/com/tangem/tap/data/DefaultOfframpRepositoryTest.kt @@ -0,0 +1,134 @@ +package com.tangem.tap.data + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder +import com.tangem.tap.network.exchangeServices.SellService +import io.mockk.* +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) +internal class DefaultOfframpRepositoryTest { + + private val sellService: SellService = mockk() + private val repository = DefaultOfframpRepository(sellService) + + private val cryptoCurrency: CryptoCurrency = mockk() + private val fiatCurrencyCode = "USD" + private val walletAddress = "0x1234567890abcdef" + + @BeforeEach + fun setUp() { + mockkObject(MutableAppThemeModeHolder) + } + + @AfterEach + fun tearDown() { + clearMocks(sellService) + unmockkObject(MutableAppThemeModeHolder) + } + + @Test + fun `getOfframpUrl should return url when sellService returns url with light theme`() { + // Arrange + val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=light" + every { MutableAppThemeModeHolder.isDarkThemeActive } returns false + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } returns expectedUrl + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isEqualTo(expectedUrl) + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } + } + + @Test + fun `getOfframpUrl should return url when sellService returns url with dark theme`() { + // Arrange + val expectedUrl = "https://moonpay.com/sell?address=$walletAddress&theme=dark" + every { MutableAppThemeModeHolder.isDarkThemeActive } returns true + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = true, + ) + } returns expectedUrl + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isEqualTo(expectedUrl) + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = true, + ) + } + } + + @Test + fun `getOfframpUrl should return null when sellService returns null`() { + // Arrange + every { MutableAppThemeModeHolder.isDarkThemeActive } returns false + every { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } returns null + + // Act + val result = repository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = fiatCurrencyCode, + walletAddress = walletAddress, + ) + + // Assert + assertThat(result).isNull() + + verify(exactly = 1) { + sellService.getUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyName = fiatCurrencyCode, + walletAddress = walletAddress, + isDarkTheme = false, + ) + } + } +} diff --git a/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt new file mode 100644 index 0000000000..e0271457e7 --- /dev/null +++ b/core/analytics/models/src/main/java/com/tangem/core/analytics/models/event/OfframpAnalyticsEvent.kt @@ -0,0 +1,17 @@ +package com.tangem.core.analytics.models.event + +import com.tangem.core.analytics.models.AnalyticsEvent + +/** + * Offramp (withdraw/sell) analytics events + */ +sealed class OfframpAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Token / Withdraw", event = event, params = params) { + + /** + * Withdraw screen opened event + */ + data object ScreenOpened : OfframpAnalyticsEvent("Withdraw Screen Opened") +} \ No newline at end of file diff --git a/domain/offramp/.gitignore b/domain/offramp/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/domain/offramp/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/domain/offramp/build.gradle.kts b/domain/offramp/build.gradle.kts new file mode 100644 index 0000000000..c2d05ca8fe --- /dev/null +++ b/domain/offramp/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + id("configuration") +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +dependencies { + /** Domain modules */ + api(projects.domain.core) + api(projects.domain.models) + + /** Test libraries */ + testImplementation(projects.test.core) + testRuntimeOnly(deps.test.junit5.engine) +} diff --git a/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt b/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt new file mode 100644 index 0000000000..75b764ac2d --- /dev/null +++ b/domain/offramp/src/main/java/com/tangem/domain/offramp/GetOfframpUrlUseCase.kt @@ -0,0 +1,41 @@ +package com.tangem.domain.offramp + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.offramp.repository.OfframpRepository + +/** + * Use case for getting offramp (sell crypto) URL + * + * @property offrampRepository repository for offramp operations + */ +class GetOfframpUrlUseCase( + private val offrampRepository: OfframpRepository, +) { + + operator fun invoke(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrencyCode: String): Either = + either { + val walletAddress = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value + ensure(walletAddress != null) { Error.WalletAddressNotFound } + + val url = offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrencyStatus.currency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + ensure(url != null) { Error.UrlNotAvailable } + + url + } + + /** Offramp use case errors */ + sealed class Error { + /** Wallet address not found in currency status */ + data object WalletAddressNotFound : Error() + + /** Offramp URL is not available for this currency */ + data object UrlNotAvailable : Error() + } +} \ No newline at end of file diff --git a/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt b/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt new file mode 100644 index 0000000000..0fdfca218b --- /dev/null +++ b/domain/offramp/src/main/java/com/tangem/domain/offramp/repository/OfframpRepository.kt @@ -0,0 +1,19 @@ +package com.tangem.domain.offramp.repository + +import com.tangem.domain.models.currency.CryptoCurrency + +/** + * Repository for offramp (sell crypto) operations + */ +interface OfframpRepository { + + /** + * Get offramp (sell) URL for the given cryptocurrency + * + * @param cryptoCurrency crypto currency to sell + * @param fiatCurrencyCode fiat currency code (e.g., "USD", "EUR") + * @param walletAddress wallet address for the refund + * @return URL for offramp service or null if not available + */ + fun getOfframpUrl(cryptoCurrency: CryptoCurrency, fiatCurrencyCode: String, walletAddress: String): String? +} \ No newline at end of file diff --git a/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt b/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt new file mode 100644 index 0000000000..2ae52dafbe --- /dev/null +++ b/domain/offramp/src/test/kotlin/com/tangem/domain/offramp/GetOfframpUrlUseCaseTest.kt @@ -0,0 +1,127 @@ +package com.tangem.domain.offramp + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.NetworkAddress +import com.tangem.domain.offramp.repository.OfframpRepository +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class GetOfframpUrlUseCaseTest { + + private val offrampRepository: OfframpRepository = mockk() + private val useCase = GetOfframpUrlUseCase(offrampRepository) + + private val cryptoCurrency: CryptoCurrency = mockk() + private val appCurrencyCode = "USD" + private val walletAddress = "0x1234567890abcdef" + private val expectedUrl = "https://moonpay.com/sell?address=$walletAddress" + + @BeforeEach + fun resetMocks() { + clearMocks(offrampRepository) + } + + @Test + fun `invoke should return url when wallet address and url are available`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(walletAddress = walletAddress) + every { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } returns expectedUrl + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isRight()).isTrue() + assertThat(result.getOrNull()).isEqualTo(expectedUrl) + + verify(exactly = 1) { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } + } + + @Test + fun `invoke should return WalletAddressNotFound error when network address is null`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(networkAddress = null) + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isLeft()).isTrue() + assertThat(result.leftOrNull()).isEqualTo(GetOfframpUrlUseCase.Error.WalletAddressNotFound) + + verify(exactly = 0) { + offrampRepository.getOfframpUrl(any(), any(), any()) + } + } + + @Test + fun `invoke should return UrlNotAvailable error when repository returns null`() { + // Arrange + val cryptoCurrencyStatus = createCryptoCurrencyStatus(walletAddress = walletAddress) + every { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } returns null + + // Act + val result = useCase(cryptoCurrencyStatus, appCurrencyCode) + + // Assert + assertThat(result.isLeft()).isTrue() + assertThat(result.leftOrNull()).isEqualTo(GetOfframpUrlUseCase.Error.UrlNotAvailable) + + verify(exactly = 1) { + offrampRepository.getOfframpUrl( + cryptoCurrency = cryptoCurrency, + fiatCurrencyCode = appCurrencyCode, + walletAddress = walletAddress, + ) + } + } + + private fun createCryptoCurrencyStatus( + walletAddress: String? = null, + networkAddress: NetworkAddress? = null, + ): CryptoCurrencyStatus { + val resolvedNetworkAddress = networkAddress ?: walletAddress?.let { address -> + mockk { + every { defaultAddress } returns mockk { + every { value } returns address + } + } + } + + val statusValue: CryptoCurrencyStatus.Value = mockk { + every { this@mockk.networkAddress } returns resolvedNetworkAddress + } + + return mockk { + every { currency } returns cryptoCurrency + every { value } returns statusValue + } + } +} + diff --git a/settings.gradle.kts b/settings.gradle.kts index d71fbf5bea..88b32b89bb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -349,6 +349,7 @@ include(":domain:manage-tokens") include(":domain:manage-tokens:models") include(":domain:onramp") include(":domain:onramp:models") +include(":domain:offramp") include(":domain:promo") include(":domain:promo:models") include(":domain:nft") From 505df4db90464f5d4678903776856f25d10cc4e7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 14 Feb 2026 23:42:14 +0400 Subject: [PATCH 17/97] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 6 - .../java/com/tangem/tap/TangemApplication.kt | 15 -- .../tangem/tap/di/TangemSdkManagerModule.kt | 3 + .../sdk/impl/DefaultTangemSdkManager.kt | 9 +- .../tasks/product/BlockchainToDeriveFinder.kt | 74 ++++++++ .../domain/tasks/product/DerivationsFinder.kt | 138 --------------- .../domain/tasks/product/ScanProductTask.kt | 46 ++--- .../tap/domain/twins/FinalizeTwinTask.kt | 2 +- ...est.kt => BlockchainToDeriveFinderTest.kt} | 101 +++-------- .../domain/card/MockScanResponseFactory.kt | 2 +- .../wallets/derivations/DerivationsSource.kt | 101 +++++++++++ .../derivations/MissedDerivationsFinder.kt | 167 ++++++++++-------- .../MissedDerivationsFinderTest.kt | 14 +- 13 files changed, 326 insertions(+), 352 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt delete mode 100644 app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt rename app/src/test/kotlin/com/tangem/tap/domain/tasks/product/{DerivationsFinderTest.kt => BlockchainToDeriveFinderTest.kt} (63%) create mode 100644 data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index 611d417b2a..da98e59b92 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -17,7 +17,6 @@ import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory -import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage @@ -49,7 +48,6 @@ import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles import com.tangem.tap.proxy.AppStateHolder -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @@ -121,8 +119,6 @@ interface ApplicationEntryPoint { fun getOnboardingRepository(): OnboardingRepository - fun getCoroutineDispatcherProvider(): CoroutineDispatcherProvider - fun getExcludedBlockchains(): ExcludedBlockchains fun getAppLogsStore(): AppLogsStore @@ -155,6 +151,4 @@ interface ApplicationEntryPoint { fun getABTestsManager(): ABTestsManager fun getAppsFlyerClientFactory(): AppsFlyerClient.Factory - - fun getWalletAccountsFetcher(): WalletAccountsFetcher } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 327b223c07..de3252872f 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -31,7 +31,6 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory -import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.datasource.api.common.MoshiConverter import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor @@ -74,10 +73,8 @@ import com.tangem.tap.common.log.TangemAppLoggerInitializer import com.tangem.tap.common.redux.AppState import com.tangem.tap.common.redux.appReducer import com.tangem.tap.domain.scanCard.CardScanningFeatureToggles -import com.tangem.tap.domain.tasks.product.DerivationsFinder import com.tangem.tap.proxy.AppStateHolder import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.wallet.BuildConfig import dagger.hilt.EntryPoints import kotlinx.coroutines.Dispatchers @@ -90,7 +87,6 @@ import timber.log.Timber lateinit var store: Store val foregroundActivityObserver = ForegroundActivityObserver -internal lateinit var derivationsFinder: DerivationsFinder open class TangemApplication : Application(), ImageLoaderFactory, Configuration.Provider { @@ -191,9 +187,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val onboardingRepository: OnboardingRepository get() = entryPoint.getOnboardingRepository() - private val dispatchers: CoroutineDispatcherProvider - get() = entryPoint.getCoroutineDispatcherProvider() - private val excludedBlockchains: ExcludedBlockchains get() = entryPoint.getExcludedBlockchains() @@ -247,9 +240,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appsFlyerClientFactory: AppsFlyerClient.Factory get() = entryPoint.getAppsFlyerClientFactory() - private val walletAccountsFetcher: WalletAccountsFetcher - get() = entryPoint.getWalletAccountsFetcher() - // endregion private val appScope = MainScope() @@ -352,11 +342,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. ) } - derivationsFinder = DerivationsFinder( - walletAccountsFetcher = walletAccountsFetcher, - dispatchers = dispatchers, - ) - appStateHolder.mainStore = store wcInitializeUseCase.init( diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index 38e66db21b..21dfcaa7f1 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -12,6 +12,7 @@ import com.tangem.features.onboarding.v2.OnboardingV2FeatureToggles import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager +import com.tangem.tap.domain.tasks.product.BlockchainToDeriveFinder import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask import com.tangem.tap.domain.visa.VisaCardScanHandler @@ -40,6 +41,7 @@ internal class TangemSdkManagerModule { appFinisher: AppFinisher, sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, analyticsExceptionHandler: AnalyticsExceptionHandler, + blockchainToDeriveFinder: BlockchainToDeriveFinder, dispatchers: CoroutineDispatcherProvider, ): TangemSdkManager { return if (BuildConfig.MOCK_DATA_SOURCE) { @@ -56,6 +58,7 @@ internal class TangemSdkManagerModule { appFinisher = appFinisher, sendFeedbackEmailUseCase = sendFeedbackEmailUseCase, analyticsExceptionHandler = analyticsExceptionHandler, + blockchainToDeriveFinder = blockchainToDeriveFinder, dispatchers = dispatchers, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index 8639981110..3e6b36a6a5 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -53,11 +53,7 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.tap.common.analytics.events.TangemSdkErrorEvent -import com.tangem.tap.derivationsFinder -import com.tangem.tap.domain.tasks.product.CreateProductWalletTask -import com.tangem.tap.domain.tasks.product.ResetBackupCardTask -import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask -import com.tangem.tap.domain.tasks.product.ScanProductTask +import com.tangem.tap.domain.tasks.product.* import com.tangem.tap.domain.tasks.visa.TangemPayGenerateAddressAndSignChallengeTask import com.tangem.tap.domain.tasks.visa.TangemPaySignWithdrawalHashTask import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask @@ -85,6 +81,7 @@ internal class DefaultTangemSdkManager( private val appFinisher: AppFinisher, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, private val analyticsExceptionHandler: AnalyticsExceptionHandler, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder, dispatchers: CoroutineDispatcherProvider, ) : TangemSdkManager { @@ -162,7 +159,7 @@ internal class DefaultTangemSdkManager( runTaskAsyncReturnOnMain( runnable = ScanProductTask( card = null, - derivationsFinder = derivationsFinder, + blockchainToDeriveFinder = blockchainToDeriveFinder, allowsRequestAccessCodeFromRepository = allowsRequestAccessCodeFromRepository, visaCardScanHandler = visaCardScanHandler, visaCoroutineScope = this, diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt new file mode 100644 index 0000000000..9118279751 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinder.kt @@ -0,0 +1,74 @@ +package com.tangem.tap.domain.tasks.product + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.derivation.DerivationStyle +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.wallets.derivations.BlockchainToDerive +import com.tangem.domain.models.scan.CardDTO +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.builder.UserWalletIdBuilder +import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.tap.features.demo.DemoHelper +import javax.inject.Inject + +/** + * Finder of blockchains to derive. + * Returns only saved, default or demo blockchains without any additional logic + * (no cardano/ethereum additions or unnecessary blockchain removals). + */ +class BlockchainToDeriveFinder @Inject constructor( + private val walletAccountsFetcher: WalletAccountsFetcher, +) { + + suspend fun find(card: CardDTO): Set { + if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() + val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() + + val derivationStyle = card.derivationStyleProvider.getDerivationStyle() + + val blockchains = getBlockchains(userWalletId).ifEmpty { + if (DemoHelper.isDemoCardId(card.cardId)) { + getDemoBlockchains(derivationStyle, card.cardId) + } else { + getDefaultBlockchains(derivationStyle) + } + } + + return blockchains + } + + private suspend fun getBlockchains(userWalletId: UserWalletId): Set { + return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty() + .flatMap { accountDTO -> + accountDTO.tokens.orEmpty() + .filter { it.contractAddress == null } + } + .mapNotNull { coin -> + val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null + val derivationPath = coin.derivationPath?.let(::DerivationPath) ?: return@mapNotNull null + + BlockchainToDerive(blockchain, derivationPath) + } + .toSet() + } + + private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): Set { + return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) + } + + private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): Set { + val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) + return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) + } + + private fun Set.mapToBlockchainsWithDerivations( + derivationStyle: DerivationStyle?, + ): Set { + return mapNotNullTo(hashSetOf()) { blockchain -> + val derivationPath = blockchain.derivationPath(derivationStyle) ?: return@mapNotNullTo null + BlockchainToDerive(blockchain, derivationPath) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt deleted file mode 100644 index dd5945533b..0000000000 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/DerivationsFinder.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.tap.domain.tasks.product - -import com.tangem.blockchain.blockchains.cardano.CardanoUtils -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.derivation.DerivationStyle -import com.tangem.blockchainsdk.utils.fromNetworkId -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.data.common.account.WalletAccountsFetcher -import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation -import com.tangem.domain.models.scan.CardDTO -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.builder.UserWalletIdBuilder -import com.tangem.domain.wallets.derivations.DerivationStyleProvider -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -internal data class BlockchainToDerive( - val blockchain: Blockchain, - val derivationPath: DerivationPath?, -) - -// FIXME: May be move to DI, currently unnecessary -internal class DerivationsFinder( - private val walletAccountsFetcher: WalletAccountsFetcher, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend fun findBlockchainsToDerive( - card: CardDTO, - derivationStyleProvider: DerivationStyleProvider, - ): Set { - if (!card.settings.isHDWalletAllowed || card.wallets.isEmpty()) return emptySet() - val userWalletId = UserWalletIdBuilder.card(card).build() ?: return emptySet() - val derivationStyle = derivationStyleProvider.getDerivationStyle() - - val blockchains = withContext(dispatchers.io) { - getBlockchains(userWalletId) - }.ifEmpty { - if (DemoHelper.isDemoCardId(card.cardId)) { - getDemoBlockchains(derivationStyle, card.cardId) - } else { - getDefaultBlockchains(derivationStyle) - } - } - - // we should generate second key for cardano - // because cardano address generation for wallet2 requires keys from 2 derivations - // https://developers.cardano.org/docs/get-started/cardano-serialization-lib/generating-keys/ - blockchains.addSecondCardanoDerivationIfPresent() - - if (card.settings.isHDWalletAllowed) { - blockchains.addEthereumBlockchains(derivationStyle) - } - - // pay attention to this - return if (!card.hasOldStyleDerivation) { - blockchains.removeUnnecessaryBlockchains(derivationStyle) - } else { - blockchains - } - } - - private suspend fun getBlockchains(userWalletId: UserWalletId): MutableSet { - return walletAccountsFetcher.getSaved(userWalletId)?.accounts.orEmpty() - .flatMap { accountDTO -> - accountDTO.tokens.orEmpty() - .filter { it.contractAddress == null } - } - .mapNotNull { coin -> - val blockchain = Blockchain.fromNetworkId(coin.networkId) ?: return@mapNotNull null - val derivationPath = coin.derivationPath?.let(::DerivationPath) - - BlockchainToDerive(blockchain, derivationPath) - } - .toMutableSet() - } - - // TODO: Move to user wallet config - private fun getDemoBlockchains(derivationStyle: DerivationStyle?, cardId: String): MutableSet { - return DemoHelper.config.getDemoBlockchains(cardId).mapToBlockchainsWithDerivations(derivationStyle) - } - - // TODO: Move to user wallet config - private fun getDefaultBlockchains(derivationStyle: DerivationStyle?): MutableSet { - val defaultBlockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum) - - return defaultBlockchains.mapToBlockchainsWithDerivations(derivationStyle) - } -} - -private fun MutableSet.addEthereumBlockchains(derivationStyle: DerivationStyle?) { - val ethereumBlockchains = setOf(Blockchain.Ethereum) - .mapToBlockchainsWithDerivations(derivationStyle) - - addAll(ethereumBlockchains) -} - -private fun Set.removeUnnecessaryBlockchains( - derivationStyle: DerivationStyle?, -): Set { - val defaultEthereum = BlockchainToDerive( - blockchain = Blockchain.Ethereum, - derivationPath = Blockchain.Ethereum.derivationPath(derivationStyle), - ) - - val addedEthereum = this.firstOrNull { it == defaultEthereum } - - return if (addedEthereum != null) { - filterNot { it.derivationPath == defaultEthereum.derivationPath && it.blockchain != Blockchain.Ethereum } - } else { - // Impossible case because Ethereum was added at the last stage - distinctBy(BlockchainToDerive::derivationPath) - } - .toSet() -} - -private fun MutableSet.addSecondCardanoDerivationIfPresent() { - val cardanoDerivation = this - .firstOrNull { it.blockchain == Blockchain.Cardano } - ?.derivationPath - ?: return - - val secondCardanoBlockchain = BlockchainToDerive( - blockchain = Blockchain.Cardano, - derivationPath = CardanoUtils.extendedDerivationPath(cardanoDerivation), - ) - - add(secondCardanoBlockchain) -} - -private fun Set.mapToBlockchainsWithDerivations( - derivationStyle: DerivationStyle?, -): MutableSet { - return mapTo(hashSetOf()) { blockchain -> - BlockchainToDerive(blockchain, blockchain.derivationPath(derivationStyle)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index 93a1c1f2be..e33ebc491c 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -13,16 +13,14 @@ import com.tangem.common.tlv.Tlv import com.tangem.common.tlv.TlvDecoder import com.tangem.crypto.CryptoUtils import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.data.wallets.derivations.MissedDerivationsFinder import com.tangem.domain.card.common.TapWorkarounds.isExcluded import com.tangem.domain.card.common.TapWorkarounds.isNotSupportedInThatRelease import com.tangem.domain.card.common.TapWorkarounds.isStart2Coin import com.tangem.domain.card.common.TapWorkarounds.isTangemTwins import com.tangem.domain.card.common.TapWorkarounds.isVisa import com.tangem.domain.card.common.TwinsHelper -import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.domain.card.common.visa.VisaUtilities -import com.tangem.domain.card.configs.CardConfig import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX @@ -44,11 +42,10 @@ import com.tangem.tap.scope import com.tangem.tap.store import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import kotlin.collections.set internal class ScanProductTask( private val card: Card?, - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, private val visaCardScanHandler: VisaCardScanHandler?, private val visaCoroutineScope: CoroutineScope?, private val onboardingV2FeatureToggles: OnboardingV2FeatureToggles?, @@ -79,7 +76,7 @@ internal class ScanProductTask( readVisaCard( session = session, cardDto = cardDto, - scanWalletProcessor = ScanWalletProcessor(derivationsFinder), + scanWalletProcessor = ScanWalletProcessor(blockchainToDeriveFinder), callback = callback, ) return @@ -87,7 +84,7 @@ internal class ScanProductTask( val commandProcessor = when { cardDto.isTangemTwins -> ScanTwinProcessor() - else -> ScanWalletProcessor(derivationsFinder) + else -> ScanWalletProcessor(blockchainToDeriveFinder) } commandProcessor.proceed(cardDto, session) { processorResult -> when (processorResult) { @@ -160,7 +157,7 @@ internal class ScanProductTask( } private class ScanWalletProcessor( - private val derivationsFinder: DerivationsFinder?, + private val blockchainToDeriveFinder: BlockchainToDeriveFinder?, ) : ProductCommandProcessor { var primaryCard: PrimaryCard? = null @@ -283,7 +280,6 @@ private class ScanWalletProcessor( callback: (result: CompletionResult) -> Unit, ) { val productType = getWalletProductType(card) - val config = CardConfig.createConfig(card) scope.launch { val scanResponse = ScanResponse( card = card, @@ -291,8 +287,7 @@ private class ScanWalletProcessor( walletData = session.environment.walletData, primaryCard = primaryCard, ) - val derivations = - collectDerivations(card, config, scanResponse.derivationStyleProvider) + val derivations = collectDerivations(card, scanResponse) if (derivations.isEmpty() || !card.settings.isHDWalletAllowed) { callback(CompletionResult.Success(scanResponse)) return@launch @@ -322,32 +317,13 @@ private class ScanWalletProcessor( private suspend fun collectDerivations( card: CardDTO, - config: CardConfig, - derivationStyleProvider: DerivationStyleProvider, + scanResponse: ScanResponse, ): Map> { - val derivations = mutableMapOf>() - val blockchains = derivationsFinder - ?.findBlockchainsToDerive(card, derivationStyleProvider) - ?: return derivations + val blockchains = blockchainToDeriveFinder + ?.find(card) + ?: return emptyMap() - blockchains.forEach { blockchain -> - val curve = config.primaryCurve(blockchain.blockchain) - val wallet = card.wallets.firstOrNull { it.curve == curve } ?: return@forEach - if (wallet.chainCode == null) return@forEach - - val key = wallet.publicKey.toMapKey() - val path = blockchain.derivationPath - if (path != null) { - val addedDerivations = derivations[key] - if (addedDerivations != null) { - derivations[key] = addedDerivations + path - } else { - derivations[key] = listOf(path) - } - } - } - - return derivations + return MissedDerivationsFinder(scanResponse).findByBlockchainsToDerive(blockchains) } } diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt index 2b20696ee9..792b932178 100644 --- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt @@ -25,7 +25,7 @@ class FinalizeTwinTask( is CompletionResult.Success -> ScanProductTask( card = readResult.data, - derivationsFinder = null, + blockchainToDeriveFinder = null, visaCardScanHandler = null, visaCoroutineScope = null, onboardingV2FeatureToggles = null, diff --git a/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt similarity index 63% rename from app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt rename to app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt index 64e3006815..5be7a5b371 100644 --- a/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/DerivationsFinderTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/domain/tasks/product/BlockchainToDeriveFinderTest.kt @@ -1,19 +1,17 @@ package com.tangem.tap.domain.tasks.product import com.google.common.truth.Truth -import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.account.WalletAccountsFetcher +import com.tangem.data.wallets.derivations.BlockchainToDerive 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.models.scan.CardDTO import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.derivations.DerivationStyleProvider -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach @@ -24,19 +22,16 @@ import org.junit.jupiter.api.TestInstance [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class DerivationsFinderTest { +class BlockchainToDeriveFinderTest { private val walletAccountsFetcher = mockk() - private val finder = DerivationsFinder( + private val finder = BlockchainToDeriveFinder( walletAccountsFetcher = walletAccountsFetcher, - dispatchers = TestingCoroutineDispatcherProvider(), ) - private val derivationStyleProvider = mockk() - @AfterEach fun tearDown() { - clearMocks(walletAccountsFetcher, derivationStyleProvider) + clearMocks(walletAccountsFetcher) } @Test @@ -47,7 +42,7 @@ class DerivationsFinderTest { } // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = mockk()) + val actual = finder.find(card) // Assert Truth.assertThat(actual).isEmpty() @@ -62,29 +57,26 @@ class DerivationsFinderTest { } // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = mockk()) + val actual = finder.find(card) // Assert Truth.assertThat(actual).isEmpty() } @Test - fun `GIVEN saved bitcoin THEN return bitcoin and ethereum`() = runTest { + fun `GIVEN saved bitcoin THEN return only bitcoin`() = runTest { // Arrange val card = createCardDTO() - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - val response = createResponse(Blockchain.Bitcoin) coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( createExpected(Blockchain.Bitcoin), - createExpected(Blockchain.Ethereum), ) Truth.assertThat(actual).containsExactlyElementsIn(expected) @@ -98,12 +90,10 @@ class DerivationsFinderTest { val demoCardId = "AC01000000045754" val card = createCardDTO(cardId = demoCardId) - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( @@ -124,12 +114,10 @@ class DerivationsFinderTest { val demoCardId = "DE00" val card = createCardDTO(cardId = demoCardId) - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( @@ -148,12 +136,10 @@ class DerivationsFinderTest { // Arrange val card = createCardDTO() - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns null // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( @@ -167,26 +153,19 @@ class DerivationsFinderTest { } @Test - fun `GIVEN saved cardano THEN return cardano and ethereum`() = runTest { + fun `GIVEN saved cardano THEN return only cardano`() = runTest { // Arrange val card = createCardDTO() - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 - val response = createResponse(Blockchain.Cardano) coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = setOf( createExpected(Blockchain.Cardano), - createExpected( - blockchain = Blockchain.Cardano, - derivationPath = CardanoUtils.extendedDerivationPath(Blockchain.Cardano.getDerivationPath()) - ), - createExpected(Blockchain.Ethereum), ) Truth.assertThat(actual).containsExactlyElementsIn(expected) @@ -195,53 +174,18 @@ class DerivationsFinderTest { } @Test - fun `GIVEN saved eth-like blockchains for v3 config wallet THEN return only unique evm derivations`() = runTest { + fun `GIVEN saved eth-like blockchains THEN return all saved blockchains without filtering`() = runTest { // Arrange val card = createCardDTO() - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V3 + val blockchains = listOf(Blockchain.Ethereum, Blockchain.BSC, Blockchain.Polygon) - val blockchains = Blockchain.entries - .filter { it.isEvm() && !it.isTestnet() } - .toTypedArray() - - val response = createResponse(*blockchains) + val response = createResponse(*blockchains.toTypedArray()) coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) - - // Assert - val expected = setOf( - createExpected(Blockchain.Ethereum), - createExpected(Blockchain.EthereumClassic), - createExpected(Blockchain.Quai), - createExpected(Blockchain.XDC), - ) - - Truth.assertThat(actual).containsExactlyElementsIn(expected) - - coVerify(exactly = 1) { walletAccountsFetcher.getSaved(userWalletId) } - } - - @Test - fun `GIVEN card has old style derivation (v1 config) THEN return all eth-like blockchains`() = runTest { - // Arrange - val oldBatchId = "AC01" - val card = createCardDTO(batchId = oldBatchId) - - every { derivationStyleProvider.getDerivationStyle() } returns DerivationStyle.V1 - - val blockchains = Blockchain.entries - .filter { it.isEvm() && !it.isTestnet() } - .toTypedArray() - - val response = createResponse(*blockchains) - coEvery { walletAccountsFetcher.getSaved(userWalletId) } returns response - - // Act - val actual = finder.findBlockchainsToDerive(card = card, derivationStyleProvider = derivationStyleProvider) + val actual = finder.find(card) // Assert val expected = blockchains.mapTo(hashSetOf(), ::createExpected) @@ -260,6 +204,13 @@ class DerivationsFinderTest { every { this@mockk.cardId } returns cardId every { this@mockk.batchId } returns batchId every { this@mockk.settings.isHDWalletAllowed } returns true + every { this@mockk.settings.isKeysImportAllowed } returns true + every { this@mockk.firmwareVersion } returns CardDTO.FirmwareVersion( + major = 6, + minor = 33, + patch = 0, + type = com.tangem.common.card.FirmwareVersion.FirmwareType.Release, + ) every { this@mockk.wallets } returns listOf(wallet) } } @@ -290,7 +241,7 @@ class DerivationsFinderTest { } private fun Blockchain.getDerivationPath(): DerivationPath { - return derivationPath(derivationStyleProvider.getDerivationStyle())!! + return derivationPath(DerivationStyle.V3)!! } private companion object { @@ -298,4 +249,4 @@ class DerivationsFinderTest { // for byteArrayOf(0) val userWalletId = UserWalletId("41448576B8DA24C7D8F5F0F79863D20D7D8312A7F9E50D3248304136DDB7AAD7") } -} \ No newline at end of file +} diff --git a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt index a4a646a6ab..973f6c6895 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/card/MockScanResponseFactory.kt @@ -73,7 +73,7 @@ object MockScanResponseFactory { CardDTO.Wallet( CardWallet( publicKey = curve.name.toByteArray(), // IMPORTANT: public key must equal to curve name - chainCode = null, + chainCode = ByteArray(32), // chainCode must not be null for HD wallets curve = curve, settings = createSettings(), totalSignedHashes = null, diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt new file mode 100644 index 0000000000..56cfcb4a13 --- /dev/null +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DerivationsSource.kt @@ -0,0 +1,101 @@ +package com.tangem.data.wallets.derivations + +import com.tangem.common.card.EllipticCurve +import com.tangem.domain.card.common.TapWorkarounds.hasOldStyleDerivation +import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.domain.models.scan.ScanResponse +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.config.ColdCurvesConfig +import com.tangem.domain.wallets.config.CurvesConfig +import com.tangem.domain.wallets.config.curvesConfig +import com.tangem.domain.wallets.derivations.DerivationStyleProvider +import com.tangem.domain.wallets.derivations.derivationStyleProvider +import com.tangem.operations.derivation.ExtendedPublicKeysMap + +/** + * Source of derivations data + */ +internal sealed interface DerivationsSource { + + val isHDWalletAllowed: Boolean + val hasOldStyleDerivation: Boolean + val curvesConfig: CurvesConfig + val derivationStyleProvider: DerivationStyleProvider + + fun getWalletPublicKey(curve: EllipticCurve): ByteArray? + fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap + + data class FromUserWallet(val userWallet: UserWallet) : DerivationsSource { + override val isHDWalletAllowed: Boolean + get() = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.settings.isHDWalletAllowed + is UserWallet.Hot -> true + } + + override val hasOldStyleDerivation: Boolean + get() = when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.card.hasOldStyleDerivation + is UserWallet.Hot -> false + } + + override val curvesConfig: CurvesConfig + get() = userWallet.curvesConfig + + override val derivationStyleProvider: DerivationStyleProvider + get() = userWallet.derivationStyleProvider + + override fun getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.getWalletPublicKey(curve) + is UserWallet.Hot -> userWallet.wallets + ?.firstOrNull { it.curve == curve && it.chainCode != null } + ?.publicKey + } + } + + override fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return when (userWallet) { + is UserWallet.Cold -> userWallet.scanResponse.getDerivedKeys(publicKey) + is UserWallet.Hot -> { + val derivedKeys = userWallet.wallets + ?.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) } + ?.derivedKeys + .orEmpty() + + ExtendedPublicKeysMap(derivedKeys) + } + } + } + } + + data class FromScanResponse(val scanResponse: ScanResponse) : DerivationsSource { + override val isHDWalletAllowed: Boolean + get() = scanResponse.card.settings.isHDWalletAllowed + + override val hasOldStyleDerivation: Boolean + get() = scanResponse.card.hasOldStyleDerivation + + override val curvesConfig: CurvesConfig + get() = ColdCurvesConfig(scanResponse.card) + + override val derivationStyleProvider: DerivationStyleProvider + get() = scanResponse.derivationStyleProvider + + override fun getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return scanResponse.getWalletPublicKey(curve) + } + + override fun getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return scanResponse.getDerivedKeys(publicKey) + } + } +} + +private fun ScanResponse.getWalletPublicKey(curve: EllipticCurve): ByteArray? { + return card.wallets.firstOrNull { it.curve == curve && it.chainCode != null } + ?.publicKey +} + +private fun ScanResponse.getDerivedKeys(publicKey: KeyWalletPublicKey): ExtendedPublicKeysMap { + return derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) +} \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt index ce12ab7516..3ea84acd9e 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/MissedDerivationsFinder.kt @@ -3,45 +3,80 @@ package com.tangem.data.wallets.derivations import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.common.card.EllipticCurve import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.KeyWalletPublicKey +import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.wallets.config.curvesConfig -import com.tangem.domain.wallets.derivations.derivationStyleProvider -import com.tangem.operations.derivation.ExtendedPublicKeysMap -import kotlin.collections.forEach private typealias DerivationData = Pair> internal typealias Derivations = Map> +/** + * Data class representing a blockchain with its derivation path + */ +data class BlockchainToDerive( + val blockchain: Blockchain, + val derivationPath: DerivationPath, +) + /** * Finder of missed derivations * - * @property userWallet User wallet to find derivations for + * @property source Source of derivations data (UserWallet or ScanResponse) * [REDACTED_AUTHOR] */ -internal class MissedDerivationsFinder(private val userWallet: UserWallet) { +class MissedDerivationsFinder private constructor(private val source: DerivationsSource) { + + /** + * Secondary constructor for backward compatibility with UserWallet + */ + constructor(userWallet: UserWallet) : this(DerivationsSource.FromUserWallet(userWallet)) + + /** + * Secondary constructor for ScanResponse + */ + constructor(scanResponse: ScanResponse) : this(DerivationsSource.FromScanResponse(scanResponse)) /** Find missed derivations for given currencies [currencies] */ fun find(currencies: List): Derivations { return currencies.map { it.network }.let(::findByNetworks) } + /** Find missed derivations for given [Network] list */ fun findByNetworks(networks: List): Derivations { + val blockchainsToDerive = networks.mapNotNull { network -> + val blockchain = network.toBlockchain() + val derivationPath = network.derivationPath.value?.let(::DerivationPath) + ?: return@mapNotNull null + + BlockchainToDerive(blockchain, derivationPath) + } + return findByBlockchainsToDerive(blockchainsToDerive) + } + + /** Find missed derivations for given [BlockchainToDerive] list */ + fun findByBlockchainsToDerive(blockchainsToDerive: Collection): Derivations { + val enrichedBlockchains = blockchainsToDerive.enrichBlockchains() + return findDerivationsInternal(enrichedBlockchains) + } + + /** + * Common implementation for finding derivations + */ + private fun findDerivationsInternal(items: Collection): Derivations { return buildMap> { - networks - .mapToNewDerivations() + items + .mapNotNull(::mapToNewDerivation) .forEach { data -> val current = this[data.first] if (current != null) { current.addAll(data.second) - current.distinct() + this[data.first] = current.distinct().toMutableList() } else { this[data.first] = data.second.toMutableList() } @@ -49,31 +84,17 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { } } - private fun List.mapToNewDerivations(): List { - return mapNotNull { network -> - val blockchain = network.toBlockchain() - val curve = userWallet.curvesConfig.primaryCurve(blockchain) ?: return@mapNotNull null + /** + * Maps a single BlockchainToDerive to derivation data (public key -> derivation paths) + */ + private fun mapToNewDerivation(input: BlockchainToDerive): DerivationData? { + val curve = source.curvesConfig.primaryCurve(input.blockchain) ?: return null + if (!input.blockchain.getSupportedCurves().contains(curve)) return null - val walletPublicKey = when (userWallet) { - is UserWallet.Cold -> { - val wallet = userWallet.scanResponse.card.wallets.firstOrNull { it.curve == curve } - wallet?.publicKey - } - is UserWallet.Hot -> { - val wallet = userWallet.wallets?.firstOrNull { it.curve == curve } - wallet?.publicKey - } - } + val publicKey = source.getWalletPublicKey(curve) ?: return null - walletPublicKey?.let { - findNewDerivations(curve = curve, publicKey = it, network = network) - } - } - } - - private fun findNewDerivations(curve: EllipticCurve, publicKey: ByteArray, network: Network): DerivationData? { - val derivationCandidates = network - .getDerivationCandidates(curve) + val derivationCandidates = input.blockchain + .getDerivationCandidates(input.derivationPath) .ifEmpty { return null } .filterAlreadyDerivedKeys(publicKey.toMapKey()) .ifEmpty { return null } @@ -81,59 +102,63 @@ internal class MissedDerivationsFinder(private val userWallet: UserWallet) { return publicKey.toMapKey() to derivationCandidates } - private fun Network.getDerivationCandidates(curve: EllipticCurve): List { - val blockchain = this.toBlockchain() - + /** + * Gets all possible derivation paths for a blockchain + */ + private fun Blockchain.getDerivationCandidates(derivationPath: DerivationPath): List { return buildList { - add(blockchain.getDerivationPath(curve = curve)) - add(blockchain.getCustomDerivationPath(curve = curve, network = this@getDerivationCandidates)) - add(blockchain.getCardanoDerivationPathIfNeeded(network = this@getDerivationCandidates)) + // Default derivation path for blockchain + add(getDerivationPath()) + + // The specified derivation path (can be either default or custom) + add(derivationPath) + + // Extended Cardano derivation path if needed + add(getCardanoExtendedDerivationPath(derivationPath)) } .filterNotNull() .distinct() } - private fun Blockchain.getDerivationPath(curve: EllipticCurve): DerivationPath? { - return if (getSupportedCurves().contains(curve)) { - derivationPath(style = userWallet.derivationStyleProvider.getDerivationStyle()) - } else { - null - } + private fun Blockchain.getDerivationPath(): DerivationPath? { + return derivationPath(style = source.derivationStyleProvider.getDerivationStyle()) } - private fun Blockchain.getCustomDerivationPath(curve: EllipticCurve, network: Network): DerivationPath? { - return if (getSupportedCurves().contains(curve)) { - network.derivationPath.value?.let(::DerivationPath) - } else { - null - } - } - - private fun Blockchain.getCardanoDerivationPathIfNeeded(network: Network): DerivationPath? { - return if (this == Blockchain.Cardano) { - network.derivationPath.value?.let { - CardanoUtils.extendedDerivationPath(derivationPath = DerivationPath(it)) - } - } else { - null - } + private fun Blockchain.getCardanoExtendedDerivationPath(customDerivationPath: DerivationPath): DerivationPath? { + if (this != Blockchain.Cardano) return null + return CardanoUtils.extendedDerivationPath(derivationPath = customDerivationPath) } private fun List.filterAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val alreadyDerivedPaths = getAlreadyDerivedKeys(publicKey) + val alreadyDerivedPaths = source.getDerivedKeys(publicKey).keys.toList() return filterNot(alreadyDerivedPaths::contains) } - private fun getAlreadyDerivedKeys(publicKey: KeyWalletPublicKey): List { - val extendedPublicKeysMap = when (userWallet) { - is UserWallet.Cold -> userWallet.scanResponse.derivedKeys[publicKey] ?: ExtendedPublicKeysMap(emptyMap()) - is UserWallet.Hot -> { - val wallets = userWallet.wallets ?: return emptyList() - wallets.firstOrNull { it.publicKey.contentEquals(publicKey.bytes) }?.derivedKeys - ?: ExtendedPublicKeysMap(emptyMap()) - } + // region Blockchain enrichment logic + + /** + * Enriches blockchains collection: + * - Adds Ethereum if HD wallet is allowed + * - Removes unnecessary blockchains that share derivation path with Ethereum (for cards without old style derivation) + */ + private fun Collection.enrichBlockchains(): Collection { + if (!source.isHDWalletAllowed) return this + + val derivationStyle = source.derivationStyleProvider.getDerivationStyle() + val ethereumDerivationPath = Blockchain.Ethereum.derivationPath(derivationStyle) ?: return this + + val withEthereum = this + BlockchainToDerive(Blockchain.Ethereum, ethereumDerivationPath) + + // For cards with old style derivation, keep all blockchains + if (source.hasOldStyleDerivation) { + return withEthereum.distinct() } - return extendedPublicKeysMap.keys.toList() + // For new cards: filter out blockchains with same derivation path as Ethereum (except Ethereum itself) + return withEthereum + .filter { it.derivationPath != ethereumDerivationPath || it.blockchain == Blockchain.Ethereum } + .distinct() } + + // endregion } \ No newline at end of file diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt index 426a1bc144..cd22c4c1f8 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/derivations/MissedDerivationsFinderTest.kt @@ -14,11 +14,13 @@ import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.card.configs.MultiWalletCardConfig import com.tangem.domain.card.configs.Wallet2CardConfig import com.tangem.domain.wallets.derivations.derivationStyleProvider -import org.junit.Test +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance /** [REDACTED_AUTHOR] */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class MissedDerivationsFinderTest { @Test @@ -97,9 +99,8 @@ internal class MissedDerivationsFinderTest { val currencies = MockCryptoCurrencyFactory(userWallet).cardano.let(::listOf) val actual = finder.find(currencies) - Truth.assertThat(actual).containsExactly( - ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()), - listOf( + val expected = mapOf( + ByteArrayKey(EllipticCurve.Ed25519.name.toByteArray()) to listOf( DerivationConfigV2.derivations(Blockchain.Cardano).values.first(), CardanoUtils.extendedDerivationPath( derivationPath = DerivationPath( @@ -108,7 +109,12 @@ internal class MissedDerivationsFinderTest { ), ), ), + ByteArrayKey(EllipticCurve.Secp256k1.name.toByteArray()) to listOf( + DerivationConfigV2.derivations(Blockchain.Ethereum).values.first(), + ), ) + + Truth.assertThat(actual).containsExactlyEntriesIn(expected) } @Test From 074b16c721dc12f900591c65d96dc23db5571f46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 14:02:09 +0400 Subject: [PATCH 18/97] Updated on 2026-08-14 --- .../com/tangem/tap/common/redux/AppState.kt | 2 - .../middlewares/TradeCryptoMiddleware.kt | 66 ------------------- .../network/exchangeServices/SellService.kt | 2 - .../moonpay/MoonPayService.kt | 8 --- .../domain/tokens/legacy/TradeCryptoAction.kt | 14 ---- features/feed/impl/build.gradle.kts | 7 +- .../impl/model/TokenActionsHandler.kt | 23 ++++--- features/markets/impl/build.gradle.kts | 7 +- .../impl/model/TokenActionsHandler.kt | 23 ++++--- features/onramp/impl/build.gradle.kts | 2 +- .../selecttoken/model/OnrampOperationModel.kt | 18 +++-- features/tokendetails/impl/build.gradle.kts | 2 +- .../tokendetails/model/TokenDetailsModel.kt | 21 +++--- features/wallet/impl/build.gradle.kts | 1 + .../WalletCurrencyActionsClickIntents.kt | 21 +++--- 15 files changed, 69 insertions(+), 148 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index b43504d06c..ae347d0807 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt @@ -6,7 +6,6 @@ import com.tangem.tap.common.redux.legacy.LegacyMiddleware import com.tangem.tap.features.details.redux.DetailsMiddleware import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.onboarding.products.wallet.redux.BackupMiddleware -import com.tangem.tap.features.wallet.redux.middlewares.TradeCryptoMiddleware import com.tangem.tap.proxy.redux.DaggerGraphMiddleware import com.tangem.tap.proxy.redux.DaggerGraphState import org.rekotlin.Middleware @@ -29,7 +28,6 @@ data class AppState( AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, LegacyMiddleware.legacyMiddleware, - TradeCryptoMiddleware.middleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt deleted file mode 100644 index 6cbc795c53..0000000000 --- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/TradeCryptoMiddleware.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.tap.features.wallet.redux.middlewares - -import com.tangem.common.routing.AppRouter -import com.tangem.core.analytics.Analytics -import com.tangem.domain.models.network.NetworkAddress -import com.tangem.domain.tokens.legacy.TradeCryptoAction -import com.tangem.tap.common.analytics.events.Token -import com.tangem.tap.common.apptheme.MutableAppThemeModeHolder -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchOpenUrl -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.store -import org.rekotlin.Middleware - -@Deprecated("Will be removed soon") -object TradeCryptoMiddleware { - - val middleware: Middleware = { _, appState -> - { nextDispatch -> - { action -> - if (action is TradeCryptoAction) { - handle(appState, action) - } - nextDispatch(action) - } - } - } - - private fun handle(state: () -> AppState?, action: TradeCryptoAction) { - if (DemoHelper.tryHandle(state)) return - - when (action) { - is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId) - is TradeCryptoAction.Sell -> proceedSellAction(action) - } - } - - private fun proceedSellAction(action: TradeCryptoAction.Sell) { - val networkAddress = action.cryptoCurrencyStatus.value.networkAddress - ?.defaultAddress - ?.let(NetworkAddress.Address::value) - ?: return - val currency = action.cryptoCurrencyStatus.currency - - store.inject(DaggerGraphState::appStateHolder).sellService?.getUrl( - cryptoCurrency = currency, - fiatCurrencyName = action.appCurrencyCode, - walletAddress = networkAddress, - isDarkTheme = MutableAppThemeModeHolder.isDarkThemeActive, - )?.let { url -> - store.dispatchOpenUrl(url) - Analytics.send(Token.Withdraw.ScreenOpened()) - } - } - - private fun openReceiptUrl(transactionId: String) { - store.dispatchNavigationAction(AppRouter::pop) - - val sellService = store.inject(DaggerGraphState::appStateHolder).sellService - sellService?.getSellCryptoReceiptUrl(transactionId = transactionId) - ?.let(store::dispatchOpenUrl) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt index 82aa59fe63..4075e7dd57 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/SellService.kt @@ -21,6 +21,4 @@ interface SellService { walletAddress: String, isDarkTheme: Boolean, ): String? - - fun getSellCryptoReceiptUrl(transactionId: String): String? } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 60491ab4c7..91e45c1573 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -177,14 +177,6 @@ class MoonPayService( return uri.build().toString() } - override fun getSellCryptoReceiptUrl(transactionId: String): String { - return Uri.Builder() - .scheme(SCHEME) - .authority(URL_SELL) - .appendPath("transaction_receipt") - .appendQueryParameter("transactionId", transactionId).build().toString() - } - private fun createSignature(data: String): String { val sha256Hmac = Mac.getInstance("HmacSHA256") val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256") diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt deleted file mode 100644 index 1cf1651c9d..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/legacy/TradeCryptoAction.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.domain.tokens.legacy - -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import org.rekotlin.Action - -sealed class TradeCryptoAction : Action { - - data class FinishSelling(val transactionId: String) : TradeCryptoAction() - - data class Sell( - val cryptoCurrencyStatus: CryptoCurrencyStatus, - val appCurrencyCode: String, - ) : TradeCryptoAction() -} \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index beb8db8937..df759d55a1 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -42,6 +42,7 @@ dependencies { implementation(projects.domain.feedback.models) implementation(projects.domain.manageTokens) implementation(projects.domain.markets) + implementation(projects.domain.offramp) implementation(projects.domain.onramp.models) implementation(projects.domain.staking.models) implementation(projects.domain.tokens) @@ -56,12 +57,6 @@ dependencies { implementation(projects.domain.yieldSupply) implementation(projects.domain.earn) - // FIXME [REDACTED_TASK_KEY] - // Remove the "Buy" and "Sell" actions from the redux middleware. - // Instead, create some kind of interface for such cases. - /* Redux -_- */ - implementation(projects.domain.legacy) - implementation(deps.reKotlin) /* Compose */ implementation(deps.compose.coil) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt index 7bfe5544bd..3f6071b417 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt @@ -2,9 +2,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model import com.tangem.common.routing.AppRoute import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent 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.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage @@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM @@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor( private val router: Router, private val clipboardManager: ClipboardManager, private val uiMessageSender: UiMessageSender, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, @Assisted private val currentAppCurrency: Provider, @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor( } private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyData.status, - appCurrencyCode = currentAppCurrency().code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = cryptoCurrencyData.status, + appCurrencyCode = currentAppCurrency().code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { diff --git a/features/markets/impl/build.gradle.kts b/features/markets/impl/build.gradle.kts index 1928a996ed..86304b9fd5 100644 --- a/features/markets/impl/build.gradle.kts +++ b/features/markets/impl/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { implementation(projects.domain.feedback.models) implementation(projects.domain.manageTokens) implementation(projects.domain.markets) + implementation(projects.domain.offramp) implementation(projects.domain.onramp.models) implementation(projects.domain.staking.models) implementation(projects.domain.staking) @@ -49,12 +50,6 @@ dependencies { implementation(projects.domain.yieldSupply.models) implementation(projects.domain.yieldSupply) - // FIXME [REDACTED_TASK_KEY] - // Remove the "Buy" and "Sell" actions from the redux middleware. - // Instead, create some kind of interface for such cases. - /* Redux -_- */ - implementation(projects.domain.legacy) - implementation(deps.reKotlin) /* Compose */ implementation(deps.compose.coil) diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt index 66ff85116c..3db29adf59 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt @@ -2,9 +2,12 @@ package com.tangem.features.markets.portfolio.impl.model import com.tangem.common.routing.AppRoute import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels +import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent 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.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage @@ -12,9 +15,8 @@ import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.features.markets.impl.R import com.tangem.features.markets.portfolio.impl.loader.PortfolioData @@ -30,7 +32,9 @@ internal class TokenActionsHandler @AssistedInject constructor( private val router: Router, private val clipboardManager: ClipboardManager, private val uiMessageSender: UiMessageSender, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, + private val analyticsEventHandler: AnalyticsEventHandler, @Assisted private val currentAppCurrency: Provider, @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, private val isDemoCardUseCase: IsDemoCardUseCase, @@ -104,12 +108,13 @@ internal class TokenActionsHandler @AssistedInject constructor( } private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyData.status, - appCurrencyCode = currentAppCurrency().code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = cryptoCurrencyData.status, + appCurrencyCode = currentAppCurrency().code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index f0a6cb16c5..fcab39bc50 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -42,8 +42,8 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) implementation(projects.domain.demo) - implementation(projects.domain.legacy) implementation(projects.domain.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index 1c618de087..fedeec4e55 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -7,10 +7,12 @@ import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -19,9 +21,8 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.redux.ReduxStateHolder -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.impl.R @@ -43,7 +44,8 @@ internal class OnrampOperationModel @Inject constructor( private val router: AppRouter, private val analyticsEventHandler: AnalyticsEventHandler, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val isDemoCardUseCase: IsDemoCardUseCase, private val messageSender: UiMessageSender, private val rampStateManager: RampStateManager, @@ -119,9 +121,13 @@ internal class OnrampOperationModel @Inject constructor( val appCurrencyCode = getSelectedAppCurrencyUseCase.invokeSync() .getOrElse { AppCurrency.Default }.code - reduxStateHolder.dispatch( - action = TradeCryptoAction.Sell(status, appCurrencyCode), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = status, + appCurrencyCode = appCurrencyCode, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } } diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 10cb869ad1..642d83b272 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -69,10 +69,10 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) implementation(projects.domain.demo) - implementation(projects.domain.legacy) implementation(projects.domain.markets.models) implementation(projects.domain.models) implementation(projects.domain.notifications.models) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.onramp.models) implementation(projects.domain.promo) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index f06bee0ec8..1e88fcd05f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -18,11 +18,13 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.AnalyticsExceptionHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.ExceptionAnalyticsEvent +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference @@ -47,15 +49,14 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.ShouldShowPromoTokenUseCase import com.tangem.domain.promo.models.PromoId -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.GetStakingAvailabilityUseCase import com.tangem.domain.staking.GetStakingEntryInfoUseCase import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent @@ -130,7 +131,8 @@ internal class TokenDetailsModel @Inject constructor( private val retryIncompleteTransactionUseCase: RetryIncompleteTransactionUseCase, private val openTrustlineUseCase: OpenTrustlineUseCase, private val dismissIncompleteTransactionUseCase: DismissIncompleteTransactionUseCase, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val analyticsEventsHandler: AnalyticsEventHandler, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, @@ -709,12 +711,13 @@ internal class TokenDetailsModel @Inject constructor( showErrorIfDemoModeOrElse { val status = cryptoCurrencyStatus ?: return@showErrorIfDemoModeOrElse - reduxStateHolder.dispatch( - TradeCryptoAction.Sell( - cryptoCurrencyStatus = status, - appCurrencyCode = selectedAppCurrencyFlow.value.code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = status, + appCurrencyCode = selectedAppCurrencyFlow.value.code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventsHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 5d647616a7..1105aae82f 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { implementation(projects.domain.nft) implementation(projects.domain.nft.models) implementation(projects.domain.hotWallet) + implementation(projects.domain.offramp) implementation(projects.domain.onramp) implementation(projects.domain.onramp.models) implementation(projects.domain.promo) 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 e6baa06b02..e2422a22be 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 @@ -11,8 +11,10 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent +import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped 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.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference @@ -36,13 +38,12 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.offramp.GetOfframpUrlUseCase import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.staking.model.StakingOption import com.tangem.domain.tokens.* -import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.analytics.TokenReceiveCopyActionSource import com.tangem.domain.tokens.model.analytics.TokenReceiveNewAnalyticsEvent @@ -136,7 +137,8 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val getStoryContentUseCase: GetStoryContentUseCase, private val analyticsEventHandler: AnalyticsEventHandler, private val dispatchers: CoroutineDispatcherProvider, - private val reduxStateHolder: ReduxStateHolder, + private val getOfframpUrlUseCase: GetOfframpUrlUseCase, + private val urlOpener: UrlOpener, private val vibratorHapticManager: VibratorHapticManager, private val clipboardManager: ClipboardManager, private val appRouter: AppRouter, @@ -335,12 +337,13 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( showErrorIfDemoModeOrElse { modelScope.launch(dispatchers.main) { - reduxStateHolder.dispatch( - action = TradeCryptoAction.Sell( - cryptoCurrencyStatus = cryptoCurrencyStatus, - appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, - ), - ) + getOfframpUrlUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + appCurrencyCode = getSelectedAppCurrencyUseCase.unwrap().code, + ).onRight { url -> + urlOpener.openUrl(url) + analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) + } } } } From d33f3405402cc41c1d9384123c08215cd545b8e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 16 Feb 2026 14:38:15 +0400 Subject: [PATCH 19/97] Updated on 2026-08-14 --- .../configs/feature_toggles_config.json | 9 +------ .../data/staking/DefaultStakeKitRepository.kt | 8 +++--- .../data/staking/DefaultStakingRepository.kt | 26 +++---------------- .../data/staking/di/StakingDataModule.kt | 2 -- .../toggles/DefaultStakingFeatureToggles.kt | 6 ----- .../staking/toggles/StakingFeatureToggles.kt | 2 -- 6 files changed, 7 insertions(+), 46 deletions(-) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index cdbd47714a..ef756aefce 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -7,14 +7,7 @@ "name": "VISA_ONBOARDING_ENABLED", "version": "undefined" }, - { - "name": "STAKING_TON_ENABLED", - "version": "5.28.0" - }, - { - "name": "STAKING_CARDANO_ENABLED", - "version": "5.31.1" - }, + { "name": "STAKING_ETH_ENABLED", "version": "undefined" diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt index 5eedc18e16..171c5c720a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakeKitRepository.kt @@ -49,7 +49,9 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext import timber.log.Timber @@ -161,10 +163,6 @@ internal class DefaultStakeKitRepository( private fun getAvailableStakeKitIntegrationsIds(): List { return StakingIntegrationID.StakeKit.entries - // load all integrations for now and filter in use cases if needed - // .filterNot { - // it.blockchain == Blockchain.Cardano && !stakingFeatureToggles.isCardanoStakingEnabled - // } } private fun NetworkTypeDTO.extractJsonName(): String { diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 626219cb64..60747a0d0c 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -7,7 +7,6 @@ import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability @@ -16,7 +15,6 @@ import com.tangem.domain.staking.repositories.P2PEthPoolRepository import com.tangem.domain.staking.repositories.StakeKitRepository import com.tangem.domain.staking.repositories.StakingRepository import com.tangem.domain.staking.toggles.StakingFeatureToggles -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.lib.crypto.BlockchainUtils.isCardano import com.tangem.lib.crypto.BlockchainUtils.isSolana @@ -34,7 +32,6 @@ internal class DefaultStakingRepository( private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, - private val walletManagersFacade: WalletManagersFacade, ) : StakingRepository { override fun getStakingAvailability( @@ -42,7 +39,7 @@ internal class DefaultStakingRepository( cryptoCurrency: CryptoCurrency, ): Flow { return channelFlow { - if (!checkFeatureToggleEnabled(userWalletId, cryptoCurrency)) { + if (!checkFeatureToggleEnabled(cryptoCurrency)) { send(StakingAvailability.Unavailable) return@channelFlow } @@ -78,7 +75,7 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): StakingAvailability { - if (!checkFeatureToggleEnabled(userWalletId, cryptoCurrency)) { + if (!checkFeatureToggleEnabled(cryptoCurrency)) { return StakingAvailability.Unavailable } @@ -118,31 +115,14 @@ internal class DefaultStakingRepository( } } - private suspend fun checkFeatureToggleEnabled(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + private fun checkFeatureToggleEnabled(cryptoCurrency: CryptoCurrency): Boolean { return when (cryptoCurrency.network.id.toBlockchain()) { - Blockchain.TON -> stakingFeatureToggles.isTonStakingEnabled Blockchain.Ethereum -> { when (cryptoCurrency) { is CryptoCurrency.Coin -> stakingFeatureToggles.isEthStakingEnabled is CryptoCurrency.Token -> true } } - Blockchain.Cardano -> { - val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() - val balance = stakingBalanceStoreV2.getSyncOrNull( - userWalletId = userWalletId, - stakingId = StakingID( - integrationId = StakingIntegrationID.create(currencyId = cryptoCurrency.id)?.value - ?: return false, - address = address, - ), - ) - if ((balance as? StakingBalance.Data.StakeKit)?.balance?.items?.isNotEmpty() == true) { - return true - } else { - stakingFeatureToggles.isCardanoStakingEnabled - } - } else -> true } } diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt index f1c17a770a..0aa8abf1b5 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingDataModule.kt @@ -61,7 +61,6 @@ internal object StakingDataModule { dispatchers: CoroutineDispatcherProvider, getUserWalletUseCase: GetUserWalletUseCase, stakingFeatureToggles: StakingFeatureToggles, - walletManagersFacade: WalletManagersFacade, ): StakingRepository { return DefaultStakingRepository( stakeKitRepository = stakeKitRepository, @@ -69,7 +68,6 @@ internal object StakingDataModule { stakingBalanceStoreV2 = stakeKitBalancesStore, dispatchers = dispatchers, getUserWalletUseCase = getUserWalletUseCase, - walletManagersFacade = walletManagersFacade, stakingFeatureToggles = stakingFeatureToggles, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt index e5ee6c623a..fe7e688a8f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/toggles/DefaultStakingFeatureToggles.kt @@ -7,12 +7,6 @@ internal class DefaultStakingFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : StakingFeatureToggles { - override val isTonStakingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "STAKING_TON_ENABLED") - - override val isCardanoStakingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("STAKING_CARDANO_ENABLED") - override val isEthStakingEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("STAKING_ETH_ENABLED") } \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt index fa08534edf..3553692065 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/toggles/StakingFeatureToggles.kt @@ -1,7 +1,5 @@ package com.tangem.domain.staking.toggles interface StakingFeatureToggles { - val isTonStakingEnabled: Boolean - val isCardanoStakingEnabled: Boolean val isEthStakingEnabled: Boolean } \ No newline at end of file From 0221d36f60fd46db98e767ebb542c6812c8c992f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Feb 2026 12:34:37 +0400 Subject: [PATCH 20/97] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 1 - .../configs/feature_toggles_config.json | 8 ------- .../tokendetails/TokenDetailsPreviewData.kt | 2 -- .../tokendetails/model/TokenDetailsModel.kt | 13 +++--------- .../tokendetails/state/TokenDetailsState.kt | 1 - .../TokenDetailsLoadedBalanceConverter.kt | 7 +------ .../TokenDetailsSkeletonStateConverter.kt | 3 --- .../state/factory/TokenDetailsStateFactory.kt | 6 +----- .../tokendetails/ui/TokenDetailsScreen.kt | 6 ++---- .../wallet/child/wallet/model/WalletModel.kt | 8 ++----- .../WalletCurrencyActionsClickIntents.kt | 9 ++------ .../supply/api/YieldSupplyFeatureToggles.kt | 7 ------- .../impl/DefaultYieldSupplyFeatureToggles.kt | 14 ------------- .../impl/di/YieldSupplyFeatureModule.kt | 21 ------------------- .../WalletManagerFactoryCreator.kt | 8 ++----- .../di/BlockchainSDKFactoryModule.kt | 3 --- 16 files changed, 13 insertions(+), 104 deletions(-) delete mode 100644 features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt delete mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt delete mode 100644 features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index a7aa7666e3..6606e530c4 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -148,7 +148,6 @@ abstract class BaseTestCase : TestCase( "SWAP_REDESIGN_ENABLED" to false, "NEW_ONRAMP_MAIN_ENABLED" to true, "HOT_WALLET_ENABLED" to true, - "YIELD_SUPPLY_FEATURE_ENABLED" to true, "ACCOUNTS_FEATURE_ENABLED" to true, "FEED_ENABLED" to true, "GASLESS_TRANSACTIONS_ENABLED" to true, diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index ef756aefce..995a84061d 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -28,14 +28,6 @@ "name": "TANGEM_PAY_ENABLED", "version": "5.31.0" }, - { - "name": "YIELD_SUPPLY_FEATURE_ENABLED", - "version": "5.30.0" - }, - { - "name": "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED", - "version": "5.33.0" - }, { "name": "NEW_ONRAMP_MAIN_ENABLED", "version": "5.31.0" diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index a221f254ec..fa2e9d6908 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -176,7 +176,6 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = false, - isYieldSupplyFeatureEnabled = false, ) val tokenDetailsState_2 = TokenDetailsState( @@ -202,7 +201,6 @@ internal object TokenDetailsPreviewData { bottomSheetConfig = null, isBalanceHidden = false, isMarketPriceAvailable = true, - isYieldSupplyFeatureEnabled = true, ) val tokenDetailsState_3 = tokenDetailsState_2.copy(stakingBlocksState = stakingBalanceBlock) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 1e88fcd05f..a0b3afb86c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -92,7 +92,6 @@ import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter import com.tangem.features.yield.supply.api.YieldSupplyDepositedWarningComponent -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.features.yield.supply.api.analytics.YieldSupplyAnalytics import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -146,7 +145,6 @@ internal class TokenDetailsModel @Inject constructor( private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val receiveAddressesFactory: ReceiveAddressesFactory, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, @@ -195,7 +193,6 @@ internal class TokenDetailsModel @Inject constructor( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) @@ -425,9 +422,7 @@ internal class TokenDetailsModel @Inject constructor( } private fun subscribeOnYieldSupplyBalanceIfActive(status: CryptoCurrencyStatus) { - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - status.value.yieldSupplyStatus?.isActive == true - ) { + if (status.value.yieldSupplyStatus?.isActive == true) { if (yieldSupplyBalanceJobHolder.isActive && status.value.sources.networkSource != StatusSource.ACTUAL) { return } @@ -1238,13 +1233,11 @@ internal class TokenDetailsModel @Inject constructor( } private suspend fun needShowYieldSupplyWarning(): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) + return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) } private fun isActiveYieldSupply(): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true + return cryptoCurrencyStatus?.value?.yieldSupplyStatus?.isActive == true } override fun onYieldSupplyWarningAcknowledged(tokenAction: TokenAction) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index ed8cf22b9f..8fc3962fe6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -23,5 +23,4 @@ internal data class TokenDetailsState( val bottomSheetConfig: TangemBottomSheetConfig?, val isBalanceHidden: Boolean, val isMarketPriceAvailable: Boolean, - val isYieldSupplyFeatureEnabled: Boolean, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 0b10013609..16a861d7c9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -19,7 +19,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsYieldSupplyState import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter @@ -31,7 +30,6 @@ internal class TokenDetailsLoadedBalanceConverter( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, private val clickIntents: TokenDetailsClickIntents, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) : Converter, TokenDetailsState> { override fun convert(value: Either): TokenDetailsState { @@ -113,10 +111,7 @@ internal class TokenDetailsLoadedBalanceConverter( selectedBalanceType = currentState.selectedBalanceType, isBalanceSelectorEnabled = isBalanceSelectorEnabled, isBalanceFlickering = status.value.isFlickering(), - yieldSupplyState = - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - status.value.yieldSupplyStatus?.isActive == true - ) { + yieldSupplyState = if (status.value.yieldSupplyStatus?.isActive == true) { TokenDetailsYieldSupplyState.Active(clickIntents::onYieldInfoClick) } else { TokenDetailsYieldSupplyState.Empty diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index 4bf97d2e4d..5580bcc639 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -19,7 +19,6 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.state.* import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton import com.tangem.features.tokendetails.impl.R -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.lib.crypto.BlockchainUtils.isBitcoin import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.ImmutableList @@ -31,7 +30,6 @@ internal class TokenDetailsSkeletonStateConverter( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) : Converter { private val iconStateConverter by lazy { TokenDetailsIconStateConverter() } @@ -72,7 +70,6 @@ internal class TokenDetailsSkeletonStateConverter( bottomSheetConfig = null, isBalanceHidden = true, isMarketPriceAvailable = value.id.rawCurrencyId != null, - isYieldSupplyFeatureEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 323b323e72..aa14a1aa20 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -4,7 +4,6 @@ import arrow.core.Either import com.tangem.common.ui.bottomsheet.chooseaddress.ChooseAddressBottomSheetConfig import com.tangem.common.ui.tokendetails.TokenDetailsDialogConfig import com.tangem.common.ui.tokens.getUnavailabilityReasonText -import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.extensions.TextReference @@ -33,8 +32,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBala import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.clore.CloreMigrationBottomSheetConfig import com.tangem.features.tokendetails.impl.R -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import kotlinx.collections.immutable.toImmutableList @@ -48,7 +47,6 @@ internal class TokenDetailsStateFactory( private val networkHasDerivationUseCase: NetworkHasDerivationUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val userWalletId: UserWalletId, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, ) { private val skeletonStateConverter by lazy { @@ -57,7 +55,6 @@ internal class TokenDetailsStateFactory( networkHasDerivationUseCase = networkHasDerivationUseCase, getUserWalletUseCase = getUserWalletUseCase, userWalletId = userWalletId, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } @@ -74,7 +71,6 @@ internal class TokenDetailsStateFactory( currentStateProvider = currentStateProvider, appCurrencyProvider = appCurrencyProvider, clickIntents = tokenDetailsClickIntents, - yieldSupplyFeatureToggles = yieldSupplyFeatureToggles, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index abf30bd8f0..a3280e26c4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -148,10 +148,8 @@ internal fun TokenDetailsScreen( ) } - if (state.isYieldSupplyFeatureEnabled) { - item { - yieldSupplyComponent.Content(modifier = itemModifier) - } + item { + yieldSupplyComponent.Content(modifier = itemModifier) } expressTransactionsItems( 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 256d99fb51..7a4e44a02a 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 @@ -47,7 +47,6 @@ import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import kotlinx.coroutines.* @@ -93,7 +92,6 @@ internal class WalletModel @Inject constructor( private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, private val tangemPayOnboardingRepository: OnboardingRepository, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val getAppThemeModeUseCase: GetAppThemeModeUseCase, @@ -166,10 +164,8 @@ internal class WalletModel @Inject constructor( } private fun updateYieldSupplyApy() { - if (yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled) { - modelScope.launch(dispatchers.default) { - yieldSupplyApyUpdateUseCase() - } + modelScope.launch(dispatchers.default) { + yieldSupplyApyUpdateUseCase() } } 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 e2422a22be..f42ebbb24e 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 @@ -64,7 +64,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.features.yield.supply.api.YieldSupplyFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -145,7 +144,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val rampStateManager: RampStateManager, private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, private val receiveAddressesFactory: ReceiveAddressesFactory, - private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, @@ -475,9 +473,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onMultiWalletSwapClick(userWalletId: UserWalletId) { val selectedWallet = stateHolder.getSelectedWallet() as? WalletState.MultiCurrency.Content ?: return - val tokenListState = selectedWallet.tokensListState - - when (tokenListState) { + when (val tokenListState = selectedWallet.tokensListState) { is WalletTokensListState.ContentState.Content -> checkSwapCryptoAvailability( tokenCount = tokenListState.items.count { it is TokensListItemUM.Token }, ) @@ -666,8 +662,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } private suspend fun needShowYieldSupplyWarning(cryptoCurrencyStatus: CryptoCurrencyStatus): Boolean { - return yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled && - needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) + return needShowYieldSupplyDepositedWarningUseCase(cryptoCurrencyStatus) } private fun navigateToSend(cryptoCurrencyStatus: CryptoCurrencyStatus, userWalletId: UserWalletId) { diff --git a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt b/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt deleted file mode 100644 index 37c2e71379..0000000000 --- a/features/yield-supply/api/src/main/java/com/tangem/features/yield/supply/api/YieldSupplyFeatureToggles.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.features.yield.supply.api - -interface YieldSupplyFeatureToggles { - - val isYieldSupplyFeatureEnabled: Boolean - val isYieldSupplyPendingTransactionsEnabled: Boolean -} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt deleted file mode 100644 index 4f0a442329..0000000000 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/DefaultYieldSupplyFeatureToggles.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.yield.supply.impl - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles - -internal class DefaultYieldSupplyFeatureToggles( - private val featureToggles: FeatureTogglesManager, -) : YieldSupplyFeatureToggles { - override val isYieldSupplyFeatureEnabled: Boolean - get() = featureToggles.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED") - - override val isYieldSupplyPendingTransactionsEnabled: Boolean - get() = featureToggles.isFeatureEnabled("YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED") -} \ No newline at end of file diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt deleted file mode 100644 index 45b8806bd6..0000000000 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/di/YieldSupplyFeatureModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.yield.supply.impl.di - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.yield.supply.impl.DefaultYieldSupplyFeatureToggles -import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@InstallIn(SingletonComponent::class) -@Module -internal object YieldSupplyFeatureModule { - - @Singleton - @Provides - fun provideYieldFeatureToggles(featureTogglesManager: FeatureTogglesManager): YieldSupplyFeatureToggles { - return DefaultYieldSupplyFeatureToggles(featureTogglesManager) - } -} \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt index a4f5188846..6c0fb03aca 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/WalletManagerFactoryCreator.kt @@ -7,7 +7,6 @@ import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.datastorage.BlockchainDataStorage import com.tangem.blockchain.common.logging.BlockchainSDKLogger import com.tangem.blockchainsdk.providers.BlockchainProviderTypes -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import timber.log.Timber import javax.inject.Inject @@ -24,7 +23,6 @@ internal class WalletManagerFactoryCreator @Inject constructor( private val accountCreator: AccountCreator, private val blockchainDataStorage: BlockchainDataStorage, private val blockchainSDKLogger: BlockchainSDKLogger, - private val featureTogglesManager: FeatureTogglesManager, ) { fun create(config: BlockchainSdkConfig, blockchainProviderTypes: BlockchainProviderTypes): WalletManagerFactory { @@ -35,10 +33,8 @@ internal class WalletManagerFactoryCreator @Inject constructor( blockchainProviderTypes = blockchainProviderTypes, accountCreator = accountCreator, featureToggles = BlockchainFeatureToggles( - isYieldSupplyEnabled = featureTogglesManager.isFeatureEnabled("YIELD_SUPPLY_FEATURE_ENABLED"), - isPendingTransactionsEnabled = featureTogglesManager.isFeatureEnabled( - "YIELD_SUPPLY_PENDING_TRANSACTIONS_ENABLED", - ), + isYieldSupplyEnabled = true, + isPendingTransactionsEnabled = true, ), blockchainDataStorage = blockchainDataStorage, loggers = listOf(blockchainSDKLogger), diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index 9abd481655..cce9a8c942 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -17,7 +17,6 @@ import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.DevBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.dev.BlockchainProvidersResponseSerializer -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage @@ -91,13 +90,11 @@ internal object BlockchainSDKFactoryModule { tangemTechApi: TangemTechApi, appPreferencesStore: AppPreferencesStore, blockchainSDKLogger: BlockchainSDKLogger, - featureTogglesManager: FeatureTogglesManager, ): WalletManagerFactoryCreator { return WalletManagerFactoryCreator( accountCreator = DefaultAccountCreator(tangemTechApi), blockchainDataStorage = DefaultBlockchainDataStorage(appPreferencesStore), blockchainSDKLogger = blockchainSDKLogger, - featureTogglesManager = featureTogglesManager, ) } } \ No newline at end of file From 3f7a1e0d205e84b931116b1871dbcc7dbfa6e562 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Feb 2026 14:59:08 +0200 Subject: [PATCH 21/97] Updated on 2026-08-14 --- build.gradle.kts | 73 ------------------- .../configurations/ProjectConfigurations.kt | 1 + .../configurations/TestConfigurations.kt | 48 ++++++++++++ 3 files changed, 49 insertions(+), 73 deletions(-) create mode 100644 plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt diff --git a/build.gradle.kts b/build.gradle.kts index c8864ff496..b61f203435 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,3 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import java.util.concurrent.ConcurrentHashMap - plugins { alias(deps.plugins.kotlin.android) apply false alias(deps.plugins.kotlin.jvm) apply false @@ -33,83 +30,13 @@ interface Injected { val fs: FileSystemOperations } -data class TestStats( - val total: Long = 0, - val passed: Long = 0, - val failed: Long = 0, - val skipped: Long = 0, -) - -val testResultsByModule = ConcurrentHashMap() - // Test task to run unit tests for debug/googleDebug variant (Android) and all JVM modules val unitTest by tasks.registering { group = "verification" description = "Run unit tests for debug/googleDebug variant and all JVM modules" - - doLast { - if (testResultsByModule.isNotEmpty()) { - val totalStats = testResultsByModule.values.fold(TestStats()) { acc, stats -> - TestStats( - total = acc.total + stats.total, - passed = acc.passed + stats.passed, - failed = acc.failed + stats.failed, - skipped = acc.skipped + stats.skipped, - ) - } - - println("\n" + "=".repeat(80)) - println("TEST SUMMARY") - println("=".repeat(80)) - - testResultsByModule.toSortedMap().forEach { (module, stats) -> - println(" $module: ${stats.total} tests (${stats.passed} passed, ${stats.failed} failed, ${stats.skipped} skipped)") - } - - println("-".repeat(80)) - println("TOTAL: ${totalStats.total} tests in ${testResultsByModule.size} modules") - println(" Passed: ${totalStats.passed}") - println(" Failed: ${totalStats.failed}") - println(" Skipped: ${totalStats.skipped}") - println("=".repeat(80)) - } - } } -// Test Logging and testCI dependencies subprojects { - tasks.withType().configureEach { - println("Test task scheduled: $path") - - testLogging { - exceptionFormat = TestExceptionFormat.FULL - showStandardStreams = true - - afterSuite(KotlinClosure2({ desc, result -> - if (desc.parent == null) { // will match the outermost suite - testResultsByModule[path] = TestStats( - total = result.testCount, - passed = result.successfulTestCount, - failed = result.failedTestCount, - skipped = result.skippedTestCount, - ) - - val output = - "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" - val startItem = "| " - val endItem = " |" - val repeatLength = startItem.length + output.length + endItem.length - println( - "\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat( - repeatLength - ) - ) - } - })) - } - } - - // Register testCI dependencies // App module plugins.withId("com.android.application") { afterEvaluate { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt index 644b1eaead..20b67b7b6d 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/ProjectConfigurations.kt @@ -5,4 +5,5 @@ import org.gradle.api.Project internal fun Project.configure() { configureKotlinCompilerOptions() configureDetektRules() + configureTestLogging() } \ No newline at end of file diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt new file mode 100644 index 0000000000..5f5c3d5d9c --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/TestConfigurations.kt @@ -0,0 +1,48 @@ +package com.tangem.plugin.configuration.configurations + +import org.gradle.api.Project +import org.gradle.api.tasks.testing.Test +import org.gradle.api.tasks.testing.TestDescriptor +import org.gradle.api.tasks.testing.TestListener +import org.gradle.api.tasks.testing.TestResult +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent +import java.io.Serializable + +internal fun Project.configureTestLogging() { + tasks.withType(Test::class.java).configureEach { + println("Test task scheduled: $path") + testLogging { + exceptionFormat = TestExceptionFormat.FULL + showStandardStreams = true + events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + } + addTestListener(TestSuiteLogger(path)) + } +} + +private class TestSuiteLogger(private val taskPath: String) : TestListener, Serializable { + override fun beforeSuite(suite: TestDescriptor) {} + + override fun afterSuite(suite: TestDescriptor, result: TestResult) { + if (suite.parent == null) { + val output = + "$taskPath - Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" + val startItem = "| " + val endItem = " |" + val repeatLength = startItem.length + output.length + endItem.length + println( + "\n" + "-".repeat(repeatLength) + "\n" + startItem + output + endItem + "\n" + "-".repeat( + repeatLength, + ), + ) + } + } + + override fun beforeTest(testDescriptor: TestDescriptor) {} + override fun afterTest(testDescriptor: TestDescriptor, result: TestResult) {} + + companion object { + private const val serialVersionUID = 1L + } +} \ No newline at end of file From 52c39047d699e673c3b95e82c08314b5e2998f7c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 18 Feb 2026 18:56:52 +0400 Subject: [PATCH 22/97] Updated on 2026-08-14 --- .../main/java/com/tangem/tap/MainActivity.kt | 4 ---- .../configs/feature_toggles_config.json | 4 ---- data/visa/build.gradle.kts | 2 -- domain/visa/build.gradle.kts | 2 -- .../tangem/features/kyc/MockKycComponent.kt | 1 - .../tangempay/TangemPayFeatureToggles.kt | 5 ----- .../DefaultTangemPayFeatureToggles.kt | 10 --------- .../tangempay/di/TangemPayDetailsModule.kt | 21 ------------------- .../DefaultOnboardVisaDeepLinkHandler.kt | 14 ++++--------- .../wallet/child/wallet/model/WalletModel.kt | 3 --- .../model/intents/TangemPayClickIntents.kt | 6 +----- .../implementors/MultiWalletContentLoader.kt | 6 +----- .../MultiWalletContentLoaderFactory.kt | 3 --- .../MultiWalletContentLoaderV2.kt | 11 ++-------- 14 files changed, 8 insertions(+), 84 deletions(-) delete mode 100644 features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt delete mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt delete mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index cf540d0ec0..ea612e1a2d 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -44,7 +44,6 @@ import com.tangem.domain.settings.ShouldInitiallyAskPermissionUseCase import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.staking.SendUnsubmittedHashesUseCase import com.tangem.domain.wallets.usecase.ClearAllHotWalletContextualUnlockUseCase -import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.tester.api.TesterMenuLauncher import com.tangem.google.GoogleServicesHelper import com.tangem.operations.backup.BackupService @@ -161,9 +160,6 @@ class MainActivity : AppCompatActivity(), ActivityResultCallbackHolder { @Inject internal lateinit var clearAllHotWalletContextualUnlockUseCase: ClearAllHotWalletContextualUnlockUseCase - @Inject - internal lateinit var tangemPayFeatureToggles: TangemPayFeatureToggles - private val viewModel: MainViewModel by viewModels() private lateinit var appThemeModeFlow: SharedFlow diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 995a84061d..46ff675017 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -24,10 +24,6 @@ "name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED", "version": "5.32.0" }, - { - "name": "TANGEM_PAY_ENABLED", - "version": "5.31.0" - }, { "name": "NEW_ONRAMP_MAIN_ENABLED", "version": "5.31.0" diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 3c9ef9ff07..22542554f7 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -40,8 +40,6 @@ dependencies { /** Feature API - remove after removing [HotWalletFeatureToggles] */ implementation(projects.features.hotWallet.api) - /** Feature API - remove after removing [TangemPayFeatureToggles] */ - implementation(projects.features.tangempay.details.api) /** Project - Utils */ implementation(projects.core.utils) diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index 3dfad027a7..cab4b8254c 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -25,8 +25,6 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) - /** Feature API - remove after removing [TangemPayFeatureToggles] */ - implementation(projects.features.tangempay.details.api) /** Security */ implementation(deps.spongecastle.core) diff --git a/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt b/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt index b6965fc720..a841936911 100644 --- a/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt +++ b/features/kyc/mock/src/main/kotlin/com/tangem/features/kyc/MockKycComponent.kt @@ -9,7 +9,6 @@ import dagger.assisted.AssistedInject /** * Mocking it for release/external builds to exclude SumSub dependency - * This will never be called if the FT [isTangemPayEnabled] is off */ @Suppress("UnusedPrivateProperty") internal class MockKycComponent @AssistedInject constructor( diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt deleted file mode 100644 index 393e589bce..0000000000 --- a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.tangempay - -interface TangemPayFeatureToggles { - val isTangemPayEnabled: Boolean -} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt deleted file mode 100644 index a51c11a3bc..0000000000 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.tangempay - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -internal class DefaultTangemPayFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : TangemPayFeatureToggles { - override val isTangemPayEnabled - get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ENABLED") -} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt deleted file mode 100644 index a6ea142d28..0000000000 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.tangempay.di - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles -import com.tangem.features.tangempay.TangemPayFeatureToggles -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object TangemPayDetailsModule { - - @Provides - @Singleton - fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles { - return DefaultTangemPayFeatureToggles(featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt index 8c9400fe8e..08c9604ba6 100644 --- a/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt +++ b/features/tangempay/onboarding/impl/src/main/kotlin/com/tangem/features/tangempay/deeplink/DefaultOnboardVisaDeepLinkHandler.kt @@ -3,7 +3,6 @@ package com.tangem.features.tangempay.deeplink import android.net.Uri import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.features.tangempay.TangemPayFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -11,18 +10,13 @@ import dagger.assisted.AssistedInject internal class DefaultOnboardVisaDeepLinkHandler @AssistedInject constructor( @Assisted uri: Uri, appRouter: AppRouter, - tangemPayFeatureToggles: TangemPayFeatureToggles, ) : OnboardVisaDeepLinkHandler { init { - if (tangemPayFeatureToggles.isTangemPayEnabled) { - val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink( - deeplink = uri.toString(), - ) - appRouter.push(AppRoute.TangemPayOnboarding(mode)) - } else { - appRouter.push(AppRoute.Home()) - } + val mode = AppRoute.TangemPayOnboarding.Mode.Deeplink( + deeplink = uri.toString(), + ) + appRouter.push(AppRoute.TangemPayOnboarding(mode)) } @AssistedFactory 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 7a4e44a02a..7a6c21dbc3 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 @@ -45,7 +45,6 @@ import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvid import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks -import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.utils.Provider import com.tangem.utils.coroutines.* @@ -89,7 +88,6 @@ internal class WalletModel @Inject constructor( private val setNotificationsEnabledUseCase: SetNotificationsEnabledUseCase, private val getIsHuaweiDeviceWithoutGoogleServicesUseCase: GetIsHuaweiDeviceWithoutGoogleServicesUseCase, private val userWalletsListRepository: UserWalletsListRepository, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, private val tangemPayOnboardingRepository: OnboardingRepository, private val accountsFeatureToggles: AccountsFeatureToggles, @@ -390,7 +388,6 @@ internal class WalletModel @Inject constructor( * Update state each time a user opens/returns to wallet screen * and every minute while user stays on the main screen */ - if (!tangemPayFeatureToggles.isTangemPayEnabled) return combine( flow = screenLifecycleProvider.isBackgroundState, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt index 1638e3ed00..5a779f8e1d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/TangemPayClickIntents.kt @@ -27,7 +27,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogCon import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayHideOnboardingStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshNeededStateTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.TangemPayRefreshShowProgressTransformer -import com.tangem.features.tangempay.TangemPayFeatureToggles import kotlinx.coroutines.launch import javax.inject.Inject @@ -64,7 +63,6 @@ internal interface TangemPayIntents { @ModelScoped internal class TangemPayClickIntentsImplementor @Inject constructor( private val stateHolder: WalletStateController, - private val featureToggles: TangemPayFeatureToggles, private val onboardingRepository: OnboardingRepository, private val produceInitialDataTangemPay: ProduceTangemPayInitialDataUseCase, private val getWalletMetainfoUseCase: GetWalletMetaInfoUseCase, @@ -77,9 +75,7 @@ internal class TangemPayClickIntentsImplementor @Inject constructor( override suspend fun onPullToRefresh() { val userWalletId = stateHolder.getSelectedWalletId() - if (!featureToggles.isTangemPayEnabled || - !onboardingRepository.isTangemPayInitialDataProduced(userWalletId) - ) { + if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { return } tangemPayMainScreenCustomerInfoUseCase.fetch(userWalletId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index c71fb95879..498fdd3e0d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -20,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.tangempay.TangemPayFeatureToggles @Suppress("LongParameterList") @Deprecated("Use MultiWalletContentLoaderV2 instead") @@ -44,7 +43,6 @@ internal class MultiWalletContentLoader( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { @@ -88,9 +86,7 @@ internal class MultiWalletContentLoader( getStoryContentUseCase = getStoryContentUseCase, ).let(::add) - if (tangemPayFeatureToggles.isTangemPayEnabled) { - add(tangemPayMainSubscriberFactory.create(userWallet)) - } + add(tangemPayMainSubscriberFactory.create(userWallet)) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt index 525899d92e..1d26373427 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt @@ -20,7 +20,6 @@ import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenList import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.TangemPayMainSubscriber -import com.tangem.features.tangempay.TangemPayFeatureToggles import javax.inject.Inject @Suppress("LongParameterList") @@ -43,7 +42,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) { @@ -66,7 +64,6 @@ internal class MultiWalletContentLoaderFactory @Inject constructor( currenciesRepository = currenciesRepository, yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - tangemPayFeatureToggles = tangemPayFeatureToggles, tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt index fea4a02329..e6d8b5a984 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt @@ -8,7 +8,6 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarni import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.features.tangempay.TangemPayFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -25,11 +24,10 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor( private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val getStoryContentUseCase: GetStoryContentUseCase, private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, - private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List = listOfNotNull( + override fun create(): List = listOf( accountListSubscriberFactory.create(userWallet = userWallet), walletNFTListSubscriberV2Factory.create(userWallet = userWallet), checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet), @@ -46,12 +44,7 @@ internal class MultiWalletContentLoaderV2 @AssistedInject constructor( stateHolder = stateController, getStoryContentUseCase = getStoryContentUseCase, ), - - if (tangemPayFeatureToggles.isTangemPayEnabled) { - tangemPayMainSubscriberFactory.create(userWallet) - } else { - null - }, + tangemPayMainSubscriberFactory.create(userWallet), ) @AssistedFactory From 2be3a915eabafe0a1f1b5c90953345ece6588e9e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 14:25:13 +0400 Subject: [PATCH 23/97] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 1 - .../configs/feature_toggles_config.json | 4 - .../alloffers/entity/AllOffersIntents.kt | 2 +- .../alloffers/entity/AllOffersStateFactory.kt | 6 +- .../alloffers/entity/AllOffersStateUM.kt | 2 +- .../onramp/alloffers/model/AllOffersModel.kt | 2 +- .../alloffers/ui/AllOffersContentSheet.kt | 8 +- .../alloffers/ui/PaymentMethodsContent.kt | 8 +- .../onramp/main/DefaultOnrampMainComponent.kt | 28 +- .../onramp/main/OnrampMainComponent.kt | 1 - .../main/di/OnrampMainComponentModelModule.kt | 20 - .../main/di/OnrampMainComponentModule.kt | 9 + .../onramp/main/entity/AmountBlockState.kt | 25 - .../entity/OnrampAmountBlockUM.kt} | 14 +- .../onramp/main/entity/OnrampIntents.kt | 11 +- .../onramp/main/entity/OnrampLastUpdate.kt | 10 - .../entity/OnrampMainBottomSheetConfig.kt | 8 +- .../main/entity/OnrampMainComponentUM.kt | 44 +- .../onramp/main/entity/OnrampMainTopBarUM.kt | 10 - .../entity/OnrampOfferBlockUM.kt | 2 +- .../main/entity/OnrampProviderBlockUM.kt | 18 - .../onramp/main/entity/OnrampProvidersUM.kt | 15 + .../OnrampAmountFieldChangeConverter.kt} | 22 +- .../OnrampAmountButtonUMStateFactory.kt | 12 +- .../factory/OnrampAmountStateFactory.kt} | 34 +- .../factory/OnrampOffersStateFactory.kt | 17 +- .../main/entity/factory/OnrampStateFactory.kt | 87 ++- .../OnrampAmountFieldChangeConverter.kt | 75 --- .../amount/OnrampAmountStateFactory.kt | 259 -------- .../main/model/OnrampMainComponentModel.kt | 575 ++++++++---------- .../onramp/main/ui/OnrampAmountContent.kt | 116 ++-- .../onramp/main/ui/OnrampButtonComponent.kt | 104 ---- .../ui/OnrampFooterContent.kt | 20 +- .../main/ui/OnrampMainComponentContent.kt | 100 ++- .../ui/OnrampOffersContent.kt | 4 +- .../onramp/main/ui/OnrampProviderContent.kt | 128 ---- .../mainv2/DefaultOnrampV2MainComponent.kt | 100 --- .../DefaultOnrampV2MainFeatureToggle.kt | 10 - .../onramp/mainv2/OnrampV2MainComponent.kt | 21 - .../mainv2/OnrampV2MainFeatureToggle.kt | 5 - .../di/OnrampMainV2ComponentModelModule.kt | 20 - .../mainv2/di/OnrampNewV2ComponentModule.kt | 33 - .../onramp/mainv2/entity/OnrampV2Intents.kt | 16 - .../entity/OnrampV2MainBottomSheetConfig.kt | 16 - .../mainv2/entity/OnrampV2MainComponentUM.kt | 32 - .../mainv2/entity/OnrampV2ProvidersUM.kt | 15 - .../entity/factory/OnrampV2StateFactory.kt | 177 ------ .../model/OnrampV2MainComponentModel.kt | 417 ------------- .../ui/OnrampNewMainComponentContent.kt | 139 ----- .../onramp/mainv2/ui/OnrampV2AmountContent.kt | 184 ------ .../onramp/root/DefaultOnrampComponent.kt | 59 +- 51 files changed, 597 insertions(+), 2448 deletions(-) delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{mainv2/entity/OnrampV2AmountBlockUM.kt => main/entity/OnrampAmountBlockUM.kt} (71%) delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{mainv2 => main}/entity/OnrampOfferBlockUM.kt (97%) delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt => main/entity/converter/OnrampAmountFieldChangeConverter.kt} (77%) rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{mainv2 => main}/entity/factory/OnrampAmountButtonUMStateFactory.kt (71%) rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{mainv2/entity/factory/OnrampV2AmountStateFactory.kt => main/entity/factory/OnrampAmountStateFactory.kt} (83%) rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{mainv2 => main}/entity/factory/OnrampOffersStateFactory.kt (89%) delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{mainv2 => main}/ui/OnrampFooterContent.kt (83%) rename features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/{mainv2 => main}/ui/OnrampOffersContent.kt (99%) delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 6606e530c4..ca62baa8b2 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -146,7 +146,6 @@ abstract class BaseTestCase : TestCase( return ApplicationInjectionExecutionRule( toggleStates = mapOf( "SWAP_REDESIGN_ENABLED" to false, - "NEW_ONRAMP_MAIN_ENABLED" to true, "HOT_WALLET_ENABLED" to true, "ACCOUNTS_FEATURE_ENABLED" to true, "FEED_ENABLED" to true, diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 46ff675017..1a804c5896 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -24,10 +24,6 @@ "name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED", "version": "5.32.0" }, - { - "name": "NEW_ONRAMP_MAIN_ENABLED", - "version": "5.31.0" - }, { "name": "ACCOUNTS_FEATURE_ENABLED", "version": "5.33.0" diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt index 2f3739e450..039af4534d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersIntents.kt @@ -1,7 +1,7 @@ package com.tangem.features.onramp.alloffers.entity import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM internal interface AllOffersIntents { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt index 3dcc789481..ee7af4a393 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateFactory.kt @@ -10,9 +10,9 @@ import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.* import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.MINUS import kotlinx.collections.immutable.toImmutableList diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt index 8e1847afc5..4807f6e61a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/entity/AllOffersStateUM.kt @@ -4,7 +4,7 @@ import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.domain.onramp.model.PaymentMethodStatus -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM import kotlinx.collections.immutable.ImmutableList internal sealed interface AllOffersStateUM { diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt index b29663f8cc..5b0c5748d3 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/model/AllOffersModel.kt @@ -12,7 +12,7 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersIntents import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.AllOffersStateFactory import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.Job diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt index e59a517b99..beaf222881 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/AllOffersContentSheet.kt @@ -32,10 +32,10 @@ import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.AllOffersStateUM import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM -import com.tangem.features.onramp.mainv2.ui.Offer +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM +import com.tangem.features.onramp.main.ui.Offer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt index 46ca3d4017..5e40a22e41 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/alloffers/ui/PaymentMethodsContent.kt @@ -33,10 +33,10 @@ import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.features.onramp.alloffers.entity.AllOffersPaymentMethodUM import com.tangem.features.onramp.alloffers.entity.OnrampPaymentMethodConfig import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampOfferAdvantagesUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferCategoryUM -import com.tangem.features.onramp.mainv2.entity.OnrampOfferUM -import com.tangem.features.onramp.mainv2.ui.TimingBlock +import com.tangem.features.onramp.main.entity.OnrampOfferAdvantagesUM +import com.tangem.features.onramp.main.entity.OnrampOfferCategoryUM +import com.tangem.features.onramp.main.entity.OnrampOfferUM +import com.tangem.features.onramp.main.ui.TimingBlock import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt index 42967bcc47..bfcf43a112 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/DefaultOnrampMainComponent.kt @@ -8,15 +8,16 @@ import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss +import com.arkivanov.essenty.lifecycle.subscribe import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.features.onramp.alloffers.AllOffersComponent import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent import com.tangem.features.onramp.main.entity.OnrampMainBottomSheetConfig import com.tangem.features.onramp.main.model.OnrampMainComponentModel -import com.tangem.features.onramp.main.ui.OnrampMainComponentContent -import com.tangem.features.onramp.providers.SelectProviderComponent +import com.tangem.features.onramp.main.ui.OnrampMainScreen import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -27,10 +28,15 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( @Assisted private val params: OnrampMainComponent.Params, private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, - private val selectProviderComponentFactory: SelectProviderComponent.Factory, + private val allOffersComponentFactory: AllOffersComponent.Factory, ) : OnrampMainComponent, AppComponentContext by appComponentContext { private val model: OnrampMainComponentModel = getOrCreateModel(params) + + init { + lifecycle.subscribe(onStop = model::onStop) + } + private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, serializer = null, @@ -43,7 +49,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( val state by model.state.collectAsState() val bottomSheet by bottomSheetSlot.subscribeAsState() - OnrampMainComponentContent(modifier = modifier, state = state) + OnrampMainScreen(modifier = modifier, state = state) bottomSheet.child?.instance?.BottomSheet() } @@ -57,7 +63,7 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( userWalletId = params.userWalletId, cryptoCurrency = params.cryptoCurrency, country = config.country, - isLaunchSepa = params.isLaunchSepa, + isLaunchSepa = false, onDismiss = { model.bottomSheetNavigation.dismiss() model.handleOnrampAvailable() @@ -72,14 +78,14 @@ internal class DefaultOnrampMainComponent @AssistedInject constructor( onDismiss = model.bottomSheetNavigation::dismiss, ), ) - is OnrampMainBottomSheetConfig.ProvidersList -> selectProviderComponentFactory.create( + is OnrampMainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( context = childByContext(componentContext), - params = SelectProviderComponent.Params( - onProviderClick = model::onProviderSelected, - onDismiss = model.bottomSheetNavigation::dismiss, - selectedProviderId = config.selectedProviderId, - selectedPaymentMethod = config.selectedPaymentMethod, + params = AllOffersComponent.Params( + userWallet = model.userWallet, cryptoCurrency = params.cryptoCurrency, + onDismiss = model.bottomSheetNavigation::dismiss, + openRedirectPage = params.openRedirectPage, + amountCurrencyCode = config.amountCurrencyCode, ), ) } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt index d4858314cf..98df5c2a8e 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/OnrampMainComponent.kt @@ -15,7 +15,6 @@ internal interface OnrampMainComponent : ComposableContentComponent { val source: OnrampSource, val openSettings: () -> Unit, val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, - val isLaunchSepa: Boolean, ) interface Factory : ComponentFactory diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt deleted file mode 100644 index f3bbdba606..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.onramp.main.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.onramp.main.model.OnrampMainComponentModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface OnrampMainComponentModelModule { - - @Binds - @IntoMap - @ClassKey(OnrampMainComponentModel::class) - fun bindOnrampSelectCountryModel(model: OnrampMainComponentModel): Model -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt index 78140491e5..440b6f0c7f 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/di/OnrampMainComponentModule.kt @@ -1,11 +1,15 @@ package com.tangem.features.onramp.main.di +import com.tangem.core.decompose.model.Model import com.tangem.features.onramp.main.DefaultOnrampMainComponent import com.tangem.features.onramp.main.OnrampMainComponent +import com.tangem.features.onramp.main.model.OnrampMainComponentModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap import javax.inject.Singleton @Module @@ -15,4 +19,9 @@ internal interface OnrampMainComponentModule { @Binds @Singleton fun bindOnrampMainComponentFactory(factory: DefaultOnrampMainComponent.Factory): OnrampMainComponent.Factory + + @Binds + @IntoMap + @ClassKey(OnrampMainComponentModel::class) + fun bindOnrampMainComponentModel(model: OnrampMainComponentModel): Model } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt deleted file mode 100644 index c56ecedb67..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/AmountBlockState.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.extensions.TextReference - -internal data class OnrampAmountBlockUM( - val currencyUM: OnrampCurrencyUM, - val amountFieldModel: AmountFieldModel, - val secondaryFieldModel: OnrampAmountSecondaryFieldUM, -) - -internal data class OnrampCurrencyUM( - val code: String, - val iconUrl: String?, - val precision: Int, - val onClick: () -> Unit, -) - -@Immutable -internal sealed interface OnrampAmountSecondaryFieldUM { - data object Loading : OnrampAmountSecondaryFieldUM - data class Content(val amount: TextReference) : OnrampAmountSecondaryFieldUM - data class Error(val error: TextReference) : OnrampAmountSecondaryFieldUM -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt similarity index 71% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt index 9ae8528504..729d275e25 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2AmountBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampAmountBlockUM.kt @@ -1,17 +1,17 @@ -package com.tangem.features.onramp.mainv2.entity +package com.tangem.features.onramp.main.entity import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList -internal data class OnrampNewAmountBlockUM( - val currencyUM: OnrampNewCurrencyUM, +internal data class OnrampAmountBlockUM( + val currencyUM: OnrampCurrencyUM, val amountFieldModel: AmountFieldModel, val secondaryFieldModel: OnrampSecondaryFieldErrorUM, ) -internal data class OnrampNewCurrencyUM( +internal data class OnrampCurrencyUM( val unit: String, val code: String, val iconUrl: String?, @@ -25,9 +25,9 @@ internal sealed interface OnrampSecondaryFieldErrorUM { data class Error(val error: TextReference) : OnrampSecondaryFieldErrorUM } -internal sealed interface OnrampV2AmountButtonUMState { - data class Loaded(val amountButtons: ImmutableList) : OnrampV2AmountButtonUMState - data object None : OnrampV2AmountButtonUMState +internal sealed interface OnrampAmountButtonUMState { + data class Loaded(val amountButtons: ImmutableList) : OnrampAmountButtonUMState + data object None : OnrampAmountButtonUMState } internal data class OnrampAmountButtonUM( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt index 0115a77152..574c819d6a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampIntents.kt @@ -2,12 +2,15 @@ package com.tangem.features.onramp.main.entity import com.tangem.domain.onramp.model.OnrampProviderWithQuote -interface OnrampIntents { - fun onAmountValueChanged(value: String, isValuePasted: Boolean) +internal interface OnrampIntents { + fun onAmountValueChanged(value: String) fun openSettings() fun openCurrenciesList() - fun onBuyClick(quote: OnrampProviderWithQuote.Data) + fun onBuyClick( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) fun openProviders() fun onRefresh() - fun onLinkClick(link: String) } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt deleted file mode 100644 index 9bcc516c6b..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampLastUpdate.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.domain.onramp.model.OnrampAmount -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -data class OnrampLastUpdate( - val fromAmount: OnrampAmount, - val countryCode: String, - val paymentMethod: OnrampPaymentMethod, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt index 2b0cff2f34..a317b2a287 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainBottomSheetConfig.kt @@ -1,11 +1,10 @@ package com.tangem.features.onramp.main.entity import com.tangem.domain.onramp.model.OnrampCountry -import com.tangem.domain.onramp.model.OnrampPaymentMethod import kotlinx.serialization.Serializable @Serializable -internal sealed interface OnrampMainBottomSheetConfig { +sealed interface OnrampMainBottomSheetConfig { @Serializable data class ConfirmResidency(val country: OnrampCountry) : OnrampMainBottomSheetConfig @@ -13,8 +12,5 @@ internal sealed interface OnrampMainBottomSheetConfig { data object CurrenciesList : OnrampMainBottomSheetConfig @Serializable - data class ProvidersList( - val selectedProviderId: String, - val selectedPaymentMethod: OnrampPaymentMethod, - ) : OnrampMainBottomSheetConfig + data class AllOffers(val amountCurrencyCode: String) : OnrampMainBottomSheetConfig } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt index 72fa50f884..b9e86bee09 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainComponentUM.kt @@ -4,55 +4,29 @@ import androidx.compose.runtime.Immutable import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.onramp.impl.R @Immutable internal sealed interface OnrampMainComponentUM { val topBarConfig: OnrampMainTopBarUM - val buyButtonConfig: BuyButtonConfig val errorNotification: NotificationUM? data class InitialLoading( - val currency: String, - val onClose: () -> Unit, - val openSettings: () -> Unit, - override val errorNotification: NotificationUM? = null, - ) : OnrampMainComponentUM { - override val topBarConfig: OnrampMainTopBarUM = OnrampMainTopBarUM( - title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), - startButtonUM = TopAppBarButtonUM.Back( - onBackClicked = onClose, - enabled = true, - ), - endButtonUM = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_more_vertical_24, - onClicked = openSettings, - isEnabled = false, - ), - ) - - override val buyButtonConfig: BuyButtonConfig = BuyButtonConfig( - text = resourceReference(R.string.common_buy), - onClick = {}, - isEnabled = false, - ) - } + override val topBarConfig: OnrampMainTopBarUM, + override val errorNotification: NotificationUM?, + ) : OnrampMainComponentUM data class Content( override val topBarConfig: OnrampMainTopBarUM, - override val buyButtonConfig: BuyButtonConfig, override val errorNotification: NotificationUM?, val amountBlockState: OnrampAmountBlockUM, - val providerBlockState: OnrampProviderBlockUM, + val offersBlockState: OnrampOffersBlockUM, + val onrampAmountButtonUMState: OnrampAmountButtonUMState, ) : OnrampMainComponentUM } -internal data class BuyButtonConfig( - val text: TextReference, - val onClick: () -> Unit, - val isEnabled: Boolean, +internal data class OnrampMainTopBarUM( + val title: TextReference, + val startButtonUM: TopAppBarButtonUM, + val endButtonUM: TopAppBarButtonUM, ) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt deleted file mode 100644 index 5cd08fe2fa..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampMainTopBarUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference - -internal data class OnrampMainTopBarUM( - val title: TextReference, - val startButtonUM: TopAppBarButtonUM, - val endButtonUM: TopAppBarButtonUM, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt similarity index 97% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt index aecbc43ee9..978aed9131 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampOfferBlockUM.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampOfferBlockUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.entity +package com.tangem.features.onramp.main.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt deleted file mode 100644 index 5434116a79..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProviderBlockUM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.onramp.main.entity - -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -sealed class OnrampProviderBlockUM { - data object Empty : OnrampProviderBlockUM() - data object Loading : OnrampProviderBlockUM() - data class Content( - val providerId: String, - val paymentMethod: OnrampPaymentMethod, - val providerName: String, - val termsOfUseLink: String?, - val privacyPolicyLink: String?, - val isBestRate: Boolean, - val onLinkClick: (String) -> Unit, - val onClick: () -> Unit, - ) : OnrampProviderBlockUM() -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt new file mode 100644 index 0000000000..dcbc368282 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/OnrampProvidersUM.kt @@ -0,0 +1,15 @@ +package com.tangem.features.onramp.main.entity + +import com.tangem.domain.onramp.model.OnrampPaymentMethod + +sealed interface OnrampProvidersUM { + + data object Empty : OnrampProvidersUM + + data object Loading : OnrampProvidersUM + + data class Content( + val providerId: String, + val paymentMethod: OnrampPaymentMethod, + ) : OnrampProvidersUM +} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt similarity index 77% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt index 34902a7683..ce0cba166b 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/converter/OnrampV2AmountFieldChangeConverter.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/converter/OnrampAmountFieldChangeConverter.kt @@ -1,24 +1,24 @@ -package com.tangem.features.onramp.mainv2.entity.converter +package com.tangem.features.onramp.main.entity.converter import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import com.tangem.core.ui.utils.parseBigDecimalOrNull -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory +import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory import com.tangem.utils.Provider import com.tangem.utils.converter.Converter import java.math.BigDecimal -internal class OnrampV2AmountFieldChangeConverter( - private val currentStateProvider: Provider, +internal class OnrampAmountFieldChangeConverter( + private val currentStateProvider: Provider, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, - private val onrampIntents: OnrampV2Intents, -) : Converter { + private val onrampIntents: OnrampIntents, +) : Converter { - override fun convert(value: String): OnrampV2MainComponentUM { + override fun convert(value: String): OnrampMainComponentUM { val state = currentStateProvider() - if (state !is OnrampV2MainComponentUM.Content) return state + if (state !is OnrampMainComponentUM.Content) return state if (value.isEmpty()) return state.emptyState() @@ -36,13 +36,13 @@ internal class OnrampV2AmountFieldChangeConverter( amountFieldModel = amountFieldModel, secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, offersBlockState = OnrampOffersBlockUM.Loading, errorNotification = null, ) } - private fun OnrampV2MainComponentUM.Content.emptyState(): OnrampV2MainComponentUM.Content { + private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content { val amountFieldModel = amountBlockState.amountFieldModel.copy( value = "", fiatValue = "", diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt similarity index 71% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt index 4bf1a7285a..27b5cdabd5 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampAmountButtonUMStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountButtonUMStateFactory.kt @@ -1,7 +1,7 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory -import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState import kotlinx.collections.immutable.toPersistentList internal class OnrampAmountButtonUMStateFactory { @@ -12,7 +12,7 @@ internal class OnrampAmountButtonUMStateFactory { currencyCode: String, currencySymbol: String, onAmountValueChanged: (String) -> Unit, - ): OnrampV2AmountButtonUMState { + ): OnrampAmountButtonUMState { return when (currencyCode) { USD_CODE, EUR_CODE -> { val buttons = defaultPreselectedAmount.map { value -> @@ -22,9 +22,9 @@ internal class OnrampAmountButtonUMStateFactory { onClick = { onAmountValueChanged(value.toString()) }, ) }.toPersistentList() - OnrampV2AmountButtonUMState.Loaded(buttons) + OnrampAmountButtonUMState.Loaded(buttons) } - else -> OnrampV2AmountButtonUMState.None + else -> OnrampAmountButtonUMState.None } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt similarity index 83% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt index a91f463964..462c27438a 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2AmountStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampAmountStateFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.extensions.resourceReference @@ -11,34 +11,34 @@ import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError import com.tangem.domain.tokens.model.AmountType import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.converter.OnrampV2AmountFieldChangeConverter +import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.converter.OnrampAmountFieldChangeConverter import com.tangem.utils.Provider -internal class OnrampV2AmountStateFactory( - private val currentStateProvider: Provider, +internal class OnrampAmountStateFactory( + private val currentStateProvider: Provider, private val analyticsEventHandler: AnalyticsEventHandler, - private val onrampIntents: OnrampV2Intents, + private val onrampIntents: OnrampIntents, private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, ) { - private val onrampAmountFieldChangeConverter: OnrampV2AmountFieldChangeConverter by lazy( + private val onrampAmountFieldChangeConverter: OnrampAmountFieldChangeConverter by lazy( mode = LazyThreadSafetyMode.NONE, ) { - OnrampV2AmountFieldChangeConverter( + OnrampAmountFieldChangeConverter( currentStateProvider = currentStateProvider, onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, onrampIntents = onrampIntents, ) } - fun getOnAmountValueChange(value: String): OnrampV2MainComponentUM { + fun getOnAmountValueChange(value: String): OnrampMainComponentUM { return onrampAmountFieldChangeConverter.convert(value) } - fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampV2MainComponentUM { + fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState @@ -72,9 +72,9 @@ internal class OnrampV2AmountStateFactory( ) } - fun getSecondaryFieldAmountErrorState(quotes: List): OnrampV2MainComponentUM { + fun getSecondaryFieldAmountErrorState(quotes: List): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState @@ -91,23 +91,23 @@ internal class OnrampV2AmountStateFactory( ) } - fun getAmountSecondaryFieldResetState(): OnrampV2MainComponentUM { + fun getAmountSecondaryFieldResetState(): OnrampMainComponentUM { val currentState = currentStateProvider() - if (currentState !is OnrampV2MainComponentUM.Content) return currentState + if (currentState !is OnrampMainComponentUM.Content) return currentState val amountState = currentState.amountBlockState if (amountState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Empty) return currentState return currentState.copy( amountBlockState = amountState.copy(secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, errorNotification = null, offersBlockState = currentState.offersBlockState, ) } private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( - amountState: OnrampNewAmountBlockUM, + amountState: OnrampAmountBlockUM, ): OnrampSecondaryFieldErrorUM.Error { val amount = error.requiredAmount.format { fiat( diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt similarity index 89% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt index c16f800f59..889f62feca 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampOffersStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampOffersStateFactory.kt @@ -1,24 +1,23 @@ -package com.tangem.features.onramp.mainv2.entity.factory +package com.tangem.features.onramp.main.entity.factory import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.onramp.model.* import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.main.entity.* import com.tangem.utils.Provider import kotlinx.collections.immutable.toPersistentList internal class OnrampOffersStateFactory( - private val currentStateProvider: Provider, - private val onrampIntents: OnrampV2Intents, + private val currentStateProvider: Provider, + private val onrampIntents: OnrampIntents, ) { - fun getOffersState(offers: List): OnrampV2MainComponentUM { - val currentState = currentStateProvider.invoke() - return when (currentState) { - is OnrampV2MainComponentUM.InitialLoading -> currentState - is OnrampV2MainComponentUM.Content -> { + fun getOffersState(offers: List): OnrampMainComponentUM { + return when (val currentState = currentStateProvider.invoke()) { + is OnrampMainComponentUM.InitialLoading -> currentState + is OnrampMainComponentUM.Content -> { if (currentState.offersBlockState is OnrampOffersBlockUM.Loading) { return currentState } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt index 8faf435657..1d19b99737 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/OnrampStateFactory.kt @@ -8,7 +8,9 @@ import com.tangem.common.ui.amountScreen.models.AmountFieldModel import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.error.OnrampError @@ -22,16 +24,30 @@ import java.math.BigDecimal internal class OnrampStateFactory( private val currentStateProvider: Provider, + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, private val cryptoCurrency: CryptoCurrency, private val onrampIntents: OnrampIntents, ) { - fun getInitialState(currency: String, onClose: () -> Unit): OnrampMainComponentUM.InitialLoading { + fun getInitialState( + currency: String, + onClose: () -> Unit, + openSettings: () -> Unit, + ): OnrampMainComponentUM.InitialLoading { return OnrampMainComponentUM.InitialLoading( - currency = currency, - onClose = onClose, - openSettings = onrampIntents::openSettings, errorNotification = null, + topBarConfig = OnrampMainTopBarUM( + title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), + startButtonUM = TopAppBarButtonUM.Close( + onCloseClick = onClose, + enabled = true, + ), + endButtonUM = TopAppBarButtonUM.Icon( + iconRes = R.drawable.ic_more_vertical_24, + onClicked = openSettings, + isEnabled = false, + ), + ), ) } @@ -42,12 +58,19 @@ internal class OnrampStateFactory( is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) } + + val initialAmountBlockState = getInitialAmountBlockState(currency) + return OnrampMainComponentUM.Content( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - buyButtonConfig = state.buyButtonConfig, - amountBlockState = getInitialAmountBlockState(currency), - providerBlockState = OnrampProviderBlockUM.Empty, + amountBlockState = initialAmountBlockState, + offersBlockState = OnrampOffersBlockUM.Empty, errorNotification = null, + onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( + currencyCode = currency.code, + currencySymbol = currency.unit, + onAmountValueChanged = onrampIntents::onAmountValueChanged, + ), ) } @@ -68,21 +91,6 @@ internal class OnrampStateFactory( } } - private fun getNoPairsErrorState(): OnrampMainComponentUM { - val state = currentStateProvider() - val contentState = state as? OnrampMainComponentUM.Content ?: return state - - return contentState.copy( - buyButtonConfig = contentState.buyButtonConfig.copy(isEnabled = false), - amountBlockState = contentState.amountBlockState.copy( - amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Error( - error = resourceReference(R.string.onramp_no_available_providers), - ), - ), - ) - } - fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampMainComponentUM { val state = currentStateProvider() val endButton = when (val button = state.topBarConfig.endButtonUM) { @@ -93,15 +101,15 @@ internal class OnrampStateFactory( return when (state) { is OnrampMainComponentUM.Content -> state.copy( topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - buyButtonConfig = state.buyButtonConfig.copy(isEnabled = false), - amountBlockState = state.amountBlockState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), - ), - providerBlockState = OnrampProviderBlockUM.Empty, + offersBlockState = OnrampOffersBlockUM.Empty, errorNotification = NotificationUM.Warning.OnrampErrorNotification( errorCode = errorCode, onRefresh = onRefresh, ), + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, + amountBlockState = state.amountBlockState.copy( + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, + ), ) is OnrampMainComponentUM.InitialLoading -> state.copy( errorNotification = NotificationUM.Warning.OnrampErrorNotification( @@ -112,6 +120,22 @@ internal class OnrampStateFactory( } } + private fun getNoPairsErrorState(): OnrampMainComponentUM { + val state = currentStateProvider() + val contentState = state as? OnrampMainComponentUM.Content ?: return state + + return contentState.copy( + amountBlockState = contentState.amountBlockState.copy( + amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error( + error = resourceReference(R.string.onramp_no_available_providers), + ), + ), + onrampAmountButtonUMState = OnrampAmountButtonUMState.None, + offersBlockState = OnrampOffersBlockUM.Empty, + ) + } + private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampAmountBlockUM { return OnrampAmountBlockUM( currencyUM = OnrampCurrencyUM( @@ -119,11 +143,12 @@ internal class OnrampStateFactory( iconUrl = currency.image, precision = currency.precision, onClick = onrampIntents::openCurrenciesList, + unit = currency.unit, ), amountFieldModel = AmountFieldModel( value = "", fiatValue = "", - onValueChange = { onrampIntents.onAmountValueChanged(value = it, isValuePasted = false) }, + onValueChange = onrampIntents::onAmountValueChanged, keyboardOptions = KeyboardOptions( imeAction = ImeAction.None, keyboardType = KeyboardType.Number, @@ -139,7 +164,7 @@ internal class OnrampStateFactory( isValuePasted = false, onValuePastedTriggerDismiss = {}, ), - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ) } @@ -149,8 +174,4 @@ internal class OnrampStateFactory( decimals = currency.precision, type = AmountType.FiatType(currency.code), ) - - companion object { - const val PREDEFINED_SEPA_AMOUNT = "100" - } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt deleted file mode 100644 index ef9cff01ac..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountFieldChangeConverter.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.tangem.features.onramp.main.entity.factory.amount - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.utils.parseBigDecimalOrNull -import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM -import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.isNullOrZero -import java.math.BigDecimal - -internal class OnrampAmountFieldChangeConverter( - private val currentStateProvider: Provider, -) : Converter { - - override fun convert(input: Input): OnrampMainComponentUM { - val value = input.value - val isValuePasted = input.isValuePasted - - val state = currentStateProvider() - if (state !is OnrampMainComponentUM.Content) return state - - if (value.isEmpty()) return state.emptyState() - - val amountState = state.amountBlockState - val amountTextField = amountState.amountFieldModel - val fiatDecimal = value.parseBigDecimalOrNull() ?: BigDecimal.ZERO - val isDoneActionEnabled = !fiatDecimal.isNullOrZero() - val amountFieldModel = amountState.amountFieldModel.copy( - fiatValue = value, - fiatAmount = amountTextField.fiatAmount.copy(value = fiatDecimal), - keyboardOptions = KeyboardOptions( - imeAction = if (isDoneActionEnabled) ImeAction.Done else ImeAction.None, - keyboardType = KeyboardType.Number, - ), - isValuePasted = isValuePasted, - ) - - return state.copy( - amountBlockState = amountState.copy( - amountFieldModel = amountFieldModel, - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, - ), - providerBlockState = OnrampProviderBlockUM.Loading, - ) - } - - private fun OnrampMainComponentUM.Content.emptyState(): OnrampMainComponentUM.Content { - val amountFieldModel = amountBlockState.amountFieldModel.copy( - value = "", - fiatValue = "", - cryptoAmount = amountBlockState.amountFieldModel.cryptoAmount.copy(value = BigDecimal.ZERO), - fiatAmount = amountBlockState.amountFieldModel.fiatAmount.copy(value = BigDecimal.ZERO), - isError = false, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.None, - keyboardType = KeyboardType.Number, - ), - ) - return copy( - amountBlockState = amountBlockState.copy( - amountFieldModel = amountFieldModel, - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content(TextReference.EMPTY), - ), - buyButtonConfig = buyButtonConfig.copy(isEnabled = false), - providerBlockState = OnrampProviderBlockUM.Empty, - ) - } - - data class Input(val value: String, val isValuePasted: Boolean) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt deleted file mode 100644 index 24010b69e9..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/entity/factory/amount/OnrampAmountStateFactory.kt +++ /dev/null @@ -1,259 +0,0 @@ -package com.tangem.features.onramp.main.entity.factory.amount - -import com.tangem.core.analytics.api.AnalyticsEventHandler -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.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent -import com.tangem.domain.onramp.model.OnrampCurrency -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampQuote -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.tokens.model.AmountType -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.* -import com.tangem.features.onramp.providers.entity.SelectProviderResult -import com.tangem.utils.Provider -import com.tangem.utils.extensions.isSingleItem - -internal class OnrampAmountStateFactory( - private val currentStateProvider: Provider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val onrampIntents: OnrampIntents, - private val cryptoCurrency: CryptoCurrency, - private val needApplyFCARestrictions: Provider, -) { - - private val onrampAmountFieldChangeConverter = OnrampAmountFieldChangeConverter( - currentStateProvider = currentStateProvider, - ) - - fun getOnAmountValueChange(value: String, isValuePasted: Boolean) = - onrampAmountFieldChangeConverter.convert(OnrampAmountFieldChangeConverter.Input(value, isValuePasted)) - - fun getUpdatedCurrencyState(currency: OnrampCurrency): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - return currentState.copy( - amountBlockState = amountState.copy( - currencyUM = amountState.currencyUM.copy( - code = currency.code, - iconUrl = currency.image, - precision = currency.precision, - ), - amountFieldModel = amountState.amountFieldModel.copy( - isError = false, - fiatAmount = amountState.amountFieldModel.fiatAmount.copy( - currencySymbol = currency.unit, - decimals = currency.precision, - type = AmountType.FiatType(currency.code), - ), - ), - ), - ) - } - - fun getAmountSecondaryLoadingState(): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - - return currentState.copy( - amountBlockState = amountState.copy(secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading), - providerBlockState = OnrampProviderBlockUM.Loading, - buyButtonConfig = currentState.buyButtonConfig.copy(isEnabled = false), - errorNotification = null, - ) - } - - fun getAmountSecondaryUpdatedState(quote: OnrampQuote): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - if (amountState.amountFieldModel.fiatValue.isEmpty()) return currentState - - return currentState.copy( - amountBlockState = amountState.copy( - amountFieldModel = amountState.amountFieldModel.copy(isError = false), - secondaryFieldModel = quote.toSecondaryFieldUiModel(amountState) ?: amountState.secondaryFieldModel, - ), - buyButtonConfig = currentState.buyButtonConfig.copy( - isEnabled = quote is OnrampQuote.Data, - onClick = { - if (quote is OnrampQuote.Data) { - onrampIntents.onBuyClick( - OnrampProviderWithQuote.Data( - provider = quote.provider, - paymentMethod = quote.paymentMethod, - toAmount = quote.toAmount, - fromAmount = quote.fromAmount, - ), - ) - } - }, - ), - errorNotification = null, - ) - } - - fun getUpdatedProviderState(selectedQuote: OnrampQuote, quotes: List): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - analyticsEventHandler.send( - OnrampAnalyticsEvent.ProviderCalculated( - providerName = selectedQuote.provider.info.name, - tokenSymbol = cryptoCurrency.symbol, - paymentMethod = selectedQuote.paymentMethod.name, - ), - ) - - val bestProvider = quotes.firstOrNull() - val isMultipleQuotes = !quotes.isSingleItem() - val isOtherQuotesHasData = quotes - .filter { it.paymentMethod == selectedQuote.paymentMethod } - .filterNot { it == bestProvider } - .any { it is OnrampQuote.Data } - - val isBestProvider = selectedQuote == bestProvider && - isMultipleQuotes && - isOtherQuotesHasData && - !needApplyFCARestrictions() - - return currentState.copy( - providerBlockState = selectedQuote.toProviderBlockState(isBestProvider), - ) - } - - fun getAmountSecondaryUpdatedState( - providerResult: SelectProviderResult, - isBestRate: Boolean, - ): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - val secondaryField = when (providerResult) { - is SelectProviderResult.ProviderWithError -> { - providerResult.quoteError.toSecondaryFieldUiModel(amountState) - } - is SelectProviderResult.ProviderWithQuote -> { - val amount = providerResult.toAmount.value.format { - crypto(symbol = providerResult.toAmount.symbol, decimals = providerResult.toAmount.decimals) - } - OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) - } - } - return currentState.copy( - amountBlockState = amountState.copy(secondaryFieldModel = secondaryField), - providerBlockState = OnrampProviderBlockUM.Content( - paymentMethod = providerResult.paymentMethod, - providerId = providerResult.provider.id, - providerName = providerResult.provider.info.name, - isBestRate = isBestRate && !needApplyFCARestrictions(), - onClick = onrampIntents::openProviders, - termsOfUseLink = providerResult.provider.info.termsOfUseLink, - privacyPolicyLink = providerResult.provider.info.privacyPolicyLink, - onLinkClick = onrampIntents::onLinkClick, - ), - buyButtonConfig = currentState.buyButtonConfig.copy( - isEnabled = providerResult is SelectProviderResult.ProviderWithQuote, - onClick = { - if (providerResult is SelectProviderResult.ProviderWithQuote) { - onrampIntents.onBuyClick( - OnrampProviderWithQuote.Data( - provider = providerResult.provider, - paymentMethod = providerResult.paymentMethod, - toAmount = providerResult.toAmount, - fromAmount = providerResult.fromAmount, - ), - ) - } - }, - ), - errorNotification = null, - ) - } - - fun getAmountSecondaryResetState(): OnrampMainComponentUM { - val currentState = currentStateProvider() - if (currentState !is OnrampMainComponentUM.Content) return currentState - - val amountState = currentState.amountBlockState - - if (amountState.secondaryFieldModel is OnrampAmountSecondaryFieldUM.Content) return currentState - - return currentState.copy( - amountBlockState = amountState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Content( - amount = TextReference.EMPTY, - ), - ), - errorNotification = null, - ) - } - - private fun OnrampQuote.toProviderBlockState(isBestRate: Boolean): OnrampProviderBlockUM { - return OnrampProviderBlockUM.Content( - paymentMethod = paymentMethod, - providerId = provider.id, - providerName = provider.info.name, - isBestRate = isBestRate, - onClick = onrampIntents::openProviders, - termsOfUseLink = provider.info.termsOfUseLink, - privacyPolicyLink = provider.info.privacyPolicyLink, - onLinkClick = onrampIntents::onLinkClick, - ) - } - - private fun OnrampQuote.toSecondaryFieldUiModel(amountState: OnrampAmountBlockUM): OnrampAmountSecondaryFieldUM? { - return when (this) { - is OnrampQuote.Error -> null - is OnrampQuote.Data -> { - val amount = toAmount.value.format { - crypto(symbol = toAmount.symbol, decimals = toAmount.decimals) - } - OnrampAmountSecondaryFieldUM.Content(stringReference(amount)) - } - is OnrampQuote.AmountError -> this.toSecondaryFieldUiModel(amountState) - } - } - - private fun OnrampQuote.AmountError.toSecondaryFieldUiModel( - amountState: OnrampAmountBlockUM, - ): OnrampAmountSecondaryFieldUM.Error { - val amount = error.requiredAmount.format { - fiat( - fiatCurrencyCode = amountState.amountFieldModel.fiatAmount.currencySymbol, - fiatCurrencySymbol = amountState.amountFieldModel.fiatAmount.currencySymbol, - ) - } - - val errorTextRes = when (error) { - is OnrampError.AmountError.TooBigError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MaxAmountError()) - R.string.onramp_max_amount_restriction - } - is OnrampError.AmountError.TooSmallError -> { - analyticsEventHandler.send(OnrampAnalyticsEvent.MinAmountError()) - R.string.onramp_min_amount_restriction - } - } - - return OnrampAmountSecondaryFieldUM.Error( - resourceReference( - errorTextRes, - wrappedList(amount), - ), - ) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt index 7c501be535..b72f54919d 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/model/OnrampMainComponentModel.kt @@ -1,38 +1,25 @@ package com.tangem.features.onramp.main.model -import androidx.compose.runtime.mutableStateOf import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate -import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.core.analytics.api.AnalyticsEventHandler 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.components.fields.InputManager -import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.EventMessageAction -import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.* import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent import com.tangem.domain.onramp.model.OnrampAvailability -import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.OnrampProviderWithQuote import com.tangem.domain.onramp.model.OnrampQuote import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.settings.usercountry.models.UserCountry -import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.onramp.main.OnrampMainComponent import com.tangem.features.onramp.main.entity.* +import com.tangem.features.onramp.main.entity.factory.OnrampAmountButtonUMStateFactory +import com.tangem.features.onramp.main.entity.factory.OnrampAmountStateFactory +import com.tangem.features.onramp.main.entity.factory.OnrampOffersStateFactory import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory -import com.tangem.features.onramp.main.entity.factory.OnrampStateFactory.Companion.PREDEFINED_SEPA_AMOUNT -import com.tangem.features.onramp.main.entity.factory.amount.OnrampAmountStateFactory -import com.tangem.features.onramp.providers.entity.SelectProviderResult -import com.tangem.features.onramp.utils.model.EUR_CURRENCY import com.tangem.features.onramp.utils.sendOnrampErrorEvent import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -43,7 +30,6 @@ import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber -import java.util.Locale import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @@ -51,85 +37,141 @@ internal class OnrampMainComponentModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, private val router: Router, - private val isDemoCardUseCase: IsDemoCardUseCase, private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, private val getOnrampCountryUseCase: GetOnrampCountryUseCase, private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase, private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, private val fetchPairsUseCase: OnrampFetchPairsUseCase, - private val onrampSaveDefaultCurrencyUseCase: OnrampSaveDefaultCurrencyUseCase, - private val onrampGetDefaultCurrencyUseCase: OnrampGetDefaultCurrencyUseCase, private val amountInputManager: InputManager, - private val messageSender: UiMessageSender, - private val urlOpener: UrlOpener, - getWalletsUseCase: GetWalletsUseCase, - getUserCountryUseCase: GetUserCountryUseCase, + private val getOnrampOffersUseCase: GetOnrampOffersUseCase, paramsContainer: ParamsContainer, + getWalletsUseCase: GetWalletsUseCase, ) : Model(), OnrampIntents { - private val params: OnrampMainComponent.Params = paramsContainer.require() + val params = paramsContainer.require() - private var shouldForceChooseSepa = params.isLaunchSepa - private var currencyToRestore: OnrampCurrency? = null - - val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - - private val lastUpdateState = mutableStateOf(null) - private var userCountry: UserCountry? = null + private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampAmountButtonUMStateFactory() + } @Suppress("PropertyUsedBeforeDeclaration") - private val stateFactory = OnrampStateFactory( - currentStateProvider = Provider { state.value }, - cryptoCurrency = params.cryptoCurrency, - onrampIntents = this, - ) + private val stateFactory: OnrampStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampStateFactory( + currentStateProvider = Provider { state.value }, + cryptoCurrency = params.cryptoCurrency, + onrampIntents = this, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + ) + } val state: StateFlow field = MutableStateFlow( value = stateFactory.getInitialState( currency = params.cryptoCurrency.name, onClose = ::onCloseClick, + openSettings = ::openSettings, ), ) - private val amountStateFactory = OnrampAmountStateFactory( - currentStateProvider = Provider { state.value }, - analyticsEventHandler = analyticsEventHandler, - onrampIntents = this, - cryptoCurrency = params.cryptoCurrency, - needApplyFCARestrictions = Provider { userCountry.needApplyFCARestrictions() }, - ) + private val amountStateFactory: OnrampAmountStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampAmountStateFactory( + currentStateProvider = Provider { state.value }, + analyticsEventHandler = analyticsEventHandler, + onrampIntents = this, + onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, + ) + } + + private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { + OnrampOffersStateFactory( + currentStateProvider = Provider { state.value }, + onrampIntents = this, + ) + } private val quotesTaskScheduler = SingleTaskScheduler() - init { - userCountry = getUserCountryUseCase.invokeSync().getOrNull() - ?: UserCountry.Other(Locale.getDefault().country) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } + init { modelScope.launch { clearOnrampCacheUseCase() - - if (params.isLaunchSepa) { - currencyToRestore = onrampGetDefaultCurrencyUseCase.invoke().getOrNull() - onrampSaveDefaultCurrencyUseCase.invoke(EUR_CURRENCY) - } } - + startLoadingQuotes() sendScreenOpenAnalytics() checkResidenceCountry() subscribeToAmountChanges() + subscribeToCountryAndCurrencyUpdates() + subscribeToQuotesUpdate() + subscribeOnOffers() } - private fun sendScreenOpenAnalytics() { + override fun onDestroy() { + modelScope.launch { clearOnrampCacheUseCase.invoke() } + quotesTaskScheduler.cancelTask() + super.onDestroy() + } + + override fun onAmountValueChanged(value: String) { + state.update { amountStateFactory.getOnAmountValueChange(value) } + modelScope.launch { amountInputManager.update(value) } + } + + override fun openSettings() { + params.openSettings.invoke() + } + + override fun openCurrenciesList() { + analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) + bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) + } + + override fun onBuyClick( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) { + val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return analyticsEventHandler.send( - OnrampAnalyticsEvent.ScreenOpened( - source = params.source, + OnrampAnalyticsEvent.OnBuyClick( + providerName = quote.provider.info.name, + currency = currentContentState.amountBlockState.currencyUM.code, tokenSymbol = params.cryptoCurrency.symbol, ), ) + sendOfferClickEvent( + quote = quote, + onrampOfferAdvantagesUM = onrampOfferAdvantagesUM, + categoryUM = categoryUM, + ) + params.openRedirectPage(quote) + } + + override fun openProviders() { + val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return + val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code + bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.AllOffers(amountCurrentCode)) + } + + override fun onRefresh() { + state.update { + stateFactory.getInitialState( + currency = params.cryptoCurrency.name, + onClose = router::pop, + openSettings = ::openSettings, + ) + } + modelScope.launch { + clearOnrampCacheUseCase.invoke() + checkResidenceCountry() + handleOnrampAvailable() + } + } + + fun onStop() { + quotesTaskScheduler.cancelTask() } fun handleOnrampAvailable() { @@ -137,92 +179,6 @@ internal class OnrampMainComponentModel @Inject constructor( subscribeToQuotesUpdate() } - fun onProviderSelected(result: SelectProviderResult, isBestRate: Boolean) { - state.update { amountStateFactory.getAmountSecondaryUpdatedState(result, isBestRate) } - - if (result.paymentMethod.id != SEPA_METHOD_ID) { - shouldForceChooseSepa = false - } - } - - private fun checkResidenceCountry() { - modelScope.launch { - checkOnrampAvailabilityUseCase(userWallet) - .onRight(::handleOnrampAvailability) - .onLeft(::handleOnrampError) - } - } - - private fun handleOnrampAvailability(availability: OnrampAvailability) { - when (availability) { - is OnrampAvailability.Available -> handleOnrampAvailable() - is OnrampAvailability.ConfirmResidency, - is OnrampAvailability.NotSupported, - -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) - } - } - - private fun subscribeToCountryAndCurrencyUpdates() { - getOnrampCountryUseCase.invoke() - .onEach { maybeCountry -> - maybeCountry.fold( - ifLeft = ::handleOnrampError, - ifRight = { country -> - if (country == null) return@onEach - - val wasInitialLoading = state.value is OnrampMainComponentUM.InitialLoading - state.update { prevState -> - if (prevState is OnrampMainComponentUM.InitialLoading) { - stateFactory.getReadyState(country.defaultCurrency) - } else { - amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) - } - } - - updatePairsAndQuotes() - - if (wasInitialLoading && params.isLaunchSepa) { - onAmountValueChanged(value = PREDEFINED_SEPA_AMOUNT, isValuePasted = true) - } - }, - ) - } - .launchIn(modelScope) - } - - private fun subscribeToAmountChanges() = modelScope.launch { - amountInputManager.query - .filter(String::isNotEmpty) - .collectLatest { _ -> - state.update { amountStateFactory.getAmountSecondaryLoadingState() } - startLoadingQuotes() - } - } - - private suspend fun updatePairsAndQuotes() { - state.update { prevState -> - val contentState = state.value as? OnrampMainComponentUM.Content ?: return@update prevState - - if (contentState.amountBlockState.amountFieldModel.fiatValue.isNotEmpty()) { - amountStateFactory.getAmountSecondaryLoadingState() - } else { - prevState - } - } - - fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( - ifLeft = ::handleOnrampError, - ifRight = { state.update { amountStateFactory.getAmountSecondaryResetState() } }, - ) - startLoadingQuotes() - } - - private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) - sendOnrampErrorAnalytic(onrampError) - state.update { stateFactory.getOnrampErrorState(onrampError) } - } - private fun startLoadingQuotes() { quotesTaskScheduler.cancelTask() quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask()) @@ -233,11 +189,12 @@ internal class OnrampMainComponentModel @Inject constructor( delay = UPDATE_DELAY, task = { runSuspendCatching { - val content = state.value as? OnrampMainComponentUM.Content ?: return@runSuspendCatching - val amountBlockState = content.amountBlockState - if (amountBlockState.amountFieldModel.fiatAmount.value.isNullOrZero()) { - return@runSuspendCatching - } + val amountBlockState = (state.value as? OnrampMainComponentUM.Content)?.amountBlockState + ?: return@runSuspendCatching + + val fiatAmount = amountBlockState.amountFieldModel.fiatAmount + if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching + fetchQuotesUseCase.invoke( userWallet = userWallet, amount = amountBlockState.amountFieldModel.fiatAmount, @@ -250,6 +207,84 @@ internal class OnrampMainComponentModel @Inject constructor( ) } + private fun checkResidenceCountry() { + modelScope.launch { + checkOnrampAvailabilityUseCase(userWallet) + .onRight(::handleOnrampAvailability) + .onLeft(::handleOnrampError) + } + } + + private fun handleOnrampAvailability(availability: OnrampAvailability) { + when (availability) { + is OnrampAvailability.Available -> Unit + is OnrampAvailability.ConfirmResidency, + is OnrampAvailability.NotSupported, + -> bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.ConfirmResidency(availability.country)) + } + } + + private fun onCloseClick() { + analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) + router.pop() + } + + private fun subscribeOnOffers() = modelScope.launch { + getOnrampOffersUseCase + .invoke() + .collectLatest { maybeOffers -> + maybeOffers.fold( + ifLeft = ::handleOnrampError, + ifRight = { offers -> + val currentState = state.value + if (currentState is OnrampMainComponentUM.Content) { + if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) { + state.update { + currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty) + } + return@fold + } + state.update { + onrampOffersStateFactory.getOffersState(offers) + } + } + }, + ) + } + } + + private fun subscribeToAmountChanges() = modelScope.launch { + amountInputManager.query + .filter(String::isNotEmpty) + .collectLatest { _ -> + startLoadingQuotes() + } + } + + private fun subscribeToCountryAndCurrencyUpdates() { + getOnrampCountryUseCase.invoke() + .onEach { maybeCountry -> + maybeCountry.fold( + ifLeft = ::handleOnrampError, + ifRight = { country -> + if (country == null) return@onEach + state.update { prevState -> + when (prevState) { + is OnrampMainComponentUM.Content -> { + amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) + } + is OnrampMainComponentUM.InitialLoading -> { + stateFactory.getReadyState(country.defaultCurrency) + } + } + } + updatePairsAndQuotes() + }, + ) + } + .launchIn(modelScope) + } + private fun subscribeToQuotesUpdate() { getOnrampQuotesUseCase.invoke() .conflate() @@ -262,152 +297,31 @@ internal class OnrampMainComponentModel @Inject constructor( .launchIn(modelScope) } - override fun onAmountValueChanged(value: String, isValuePasted: Boolean) { - state.update { amountStateFactory.getOnAmountValueChange(value, isValuePasted) } - modelScope.launch { amountInputManager.update(value) } - } - - override fun openSettings() { - params.openSettings() - } - - override fun onBuyClick(quote: OnrampProviderWithQuote.Data) { - if (userWallet is UserWallet.Cold && isDemoCardUseCase.invoke(userWallet.cardId)) { - showDemoWarning() - } else { - val currentContentState = state.value as? OnrampMainComponentUM.Content ?: return - analyticsEventHandler.send( - OnrampAnalyticsEvent.OnBuyClick( - providerName = quote.provider.info.name, - currency = currentContentState.amountBlockState.currencyUM.code, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - params.openRedirectPage(quote) - } - } - - override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) - bottomSheetNavigation.activate(OnrampMainBottomSheetConfig.CurrenciesList) - } - - override fun openProviders() { - val providerState = (state.value as? OnrampMainComponentUM.Content)?.providerBlockState ?: return - val providerContentState = providerState as? OnrampProviderBlockUM.Content ?: return - bottomSheetNavigation.activate( - OnrampMainBottomSheetConfig.ProvidersList( - selectedPaymentMethod = providerContentState.paymentMethod, - selectedProviderId = providerContentState.providerId, - ), - ) - } - - override fun onRefresh() { - state.update { - stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = router::pop, - ) - } - quotesTaskScheduler.cancelTask() - modelScope.launch { - clearOnrampCacheUseCase.invoke() - checkResidenceCountry() - } - } - - override fun onLinkClick(link: String) = urlOpener.openUrl(link) - - override fun onDestroy() { - modelScope.launch { clearOnrampCacheUseCase.invoke() } - quotesTaskScheduler.cancelTask() - - modelScope.launch { - if (params.isLaunchSepa) { - currencyToRestore?.let { onrampSaveDefaultCurrencyUseCase.invoke(it) } - } - } - - super.onDestroy() - } - - private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) - router.pop() - } - private fun handleQuoteResult(quotes: List) { sendOnrampQuotesErrorAnalytic(quotes) - - val quote = selectOrUpdateQuote(quotes) - - if (quote == null) { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - lastUpdateState.value = null - return - } - state.update { amountStateFactory.getAmountSecondaryUpdatedState(quote = quote) } - } - - /** - * !!! Important quote selection logic !!! - * Selects or updated quote based on input data (amount, country, currency). - * If input data has changed select new best quote, otherwise last selected quote. - * If last selected quote on same input data is in an error state, select next best quote - * If new best quote or next best quote does not exist (i.e. Error state) select nothing. - */ - private fun selectOrUpdateQuote(quotes: List): OnrampQuote? { - val quoteToCheck = quotes.firstOrNull { it !is OnrampQuote.Error } - - val bestSepaQuote = if (params.isLaunchSepa && shouldForceChooseSepa) { - quotes.filterIsInstance() - .filter { it.paymentMethod.id == SEPA_METHOD_ID } - .maxByOrNull { it.toAmount.value } - } else { - null - } - - // Check if amount, country or currency has changed - val newQuote = bestSepaQuote ?: if (isAmountOrCountryChanged(quoteToCheck)) { - quoteToCheck - } else { - val state = state.value as? OnrampMainComponentUM.Content - val providerState = state?.providerBlockState as? OnrampProviderBlockUM.Content - - // Get current selected quote to update - val lastSelectedQuote = quotes.firstOrNull { quote -> - quote.provider.id == providerState?.providerId && - quote.paymentMethod.id == providerState.paymentMethod.id + when { + quotes.isEmpty() -> { + state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } } - - // Check if selected updated quote is not error - if (lastSelectedQuote is OnrampQuote.Error) { - quoteToCheck - } else { - lastSelectedQuote + quotes.all { it is OnrampQuote.AmountError } -> { + state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) } + } + quotes.none { it is OnrampQuote.Data } -> { + state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } + } + else -> { + state.update { prevState -> + val resetState = amountStateFactory.getAmountSecondaryFieldResetState() + if (prevState is OnrampMainComponentUM.Content && + resetState is OnrampMainComponentUM.Content && + prevState.offersBlockState is OnrampOffersBlockUM.Loading + ) { + resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty) + } else { + resetState + } + } } - } - if (newQuote != null) { - updateProvider(newQuote, quotes) - } - - return newQuote - } - - private fun updateProvider(quote: OnrampQuote, quotes: List) { - lastUpdateState.value = OnrampLastUpdate( - quote.fromAmount, - quote.countryCode, - quote.paymentMethod, - ) - - if (quote.paymentMethod.id != SEPA_METHOD_ID) { - shouldForceChooseSepa = false - } - - state.update { - amountStateFactory.getUpdatedProviderState(selectedQuote = quote, quotes = quotes) } } @@ -415,41 +329,30 @@ internal class OnrampMainComponentModel @Inject constructor( state.update { prevState -> (prevState as? OnrampMainComponentUM.Content)?.copy( errorNotification = null, - providerBlockState = OnrampProviderBlockUM.Loading, + offersBlockState = OnrampOffersBlockUM.Loading, amountBlockState = prevState.amountBlockState.copy( - secondaryFieldModel = OnrampAmountSecondaryFieldUM.Loading, + secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, ), ) ?: prevState } startLoadingQuotes() } - private fun showDemoWarning() { - val alertUM = AlertDemoModeUM(onConfirmClick = {}) - val message = DialogMessage( - title = alertUM.title, - message = alertUM.message, - firstActionBuilder = { - EventMessageAction( - title = alertUM.confirmButtonText, - onClick = alertUM.onConfirmClick, - ) + private suspend fun updatePairsAndQuotes() { + fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( + ifLeft = ::handleOnrampError, + ifRight = { + state.update { + amountStateFactory.getAmountSecondaryFieldResetState() + } + startLoadingQuotes() }, - secondActionBuilder = { cancelAction() }, ) - - messageSender.send(message) } - private fun sendOnrampErrorAnalytic(error: OnrampError) { - val content = state.value as? OnrampMainComponentUM.Content - val providerContent = content?.providerBlockState as? OnrampProviderBlockUM.Content - analyticsEventHandler.sendOnrampErrorEvent( - error = error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = providerContent?.providerName, - paymentMethod = providerContent?.paymentMethod?.name, - ) + private fun handleOnrampError(onrampError: OnrampError) { + Timber.e(onrampError.toString()) + state.update { stateFactory.getOnrampErrorState(onrampError) } } private fun sendOnrampQuotesErrorAnalytic(quotes: List) { @@ -467,20 +370,48 @@ internal class OnrampMainComponentModel @Inject constructor( providerName = errorState.provider.info.name, paymentMethod = errorState.paymentMethod.name, ) - else -> { /* no-op */ - } + else -> Unit } } } - private fun isAmountOrCountryChanged(quote: OnrampQuote?): Boolean { - return lastUpdateState.value?.fromAmount != quote?.fromAmount || - lastUpdateState.value?.countryCode != quote?.countryCode + private fun sendScreenOpenAnalytics() { + analyticsEventHandler.send( + OnrampAnalyticsEvent.ScreenOpened( + source = params.source, + tokenSymbol = params.cryptoCurrency.symbol, + ), + ) + } + + private fun sendOfferClickEvent( + quote: OnrampProviderWithQuote.Data, + onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, + categoryUM: OnrampOfferCategoryUM, + ) { + val event = when (categoryUM) { + OnrampOfferCategoryUM.RecentlyUsed -> { + OnrampAnalyticsEvent.RecentlyBuyClicked( + tokenSymbol = params.cryptoCurrency.symbol, + providerName = quote.provider.info.name, + paymentMethod = quote.paymentMethod.name, + ) + } + OnrampOfferCategoryUM.Recommended -> { + onrampOfferAdvantagesUM.toAnalyticsEvent( + cryptoCurrencySymbol = params.cryptoCurrency.symbol, + providerName = quote.provider.info.name, + paymentMethodName = quote.paymentMethod.name, + ) + } + } + + if (event != null) { + analyticsEventHandler.send(event) + } } private companion object { const val UPDATE_DELAY = 10_000L - - const val SEPA_METHOD_ID = "sepa" } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt index 0bdfa02446..c2293646bd 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampAmountContent.kt @@ -1,5 +1,7 @@ package com.tangem.features.onramp.main.ui +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -17,61 +19,89 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDirection import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.fields.AmountTextField import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation 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.BuyTokenDetailsScreenTestTags import com.tangem.core.ui.utils.rememberDecimalFormat import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampAmountBlockUM -import com.tangem.features.onramp.main.entity.OnrampAmountSecondaryFieldUM import com.tangem.features.onramp.main.entity.OnrampCurrencyUM +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampSecondaryFieldErrorUM @Composable -internal fun OnrampAmountContent(state: OnrampAmountBlockUM, modifier: Modifier = Modifier) { +internal fun OnrampAmountContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { Column( modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(vertical = TangemTheme.dimens.spacing28), + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), + ) + .padding(vertical = 24.dp, horizontal = 16.dp) + .animateContentSize(), horizontalAlignment = Alignment.CenterHorizontally, ) { - OnrampCurrencyIcon(currencyUM = state.currencyUM) - OnrampAmountField(amountField = state.amountFieldModel) - OnrampAmountSecondary(state = state.secondaryFieldModel) + OnrampHeaderTitle() + + OnrampAmountField( + amountField = state.amountBlockState.amountFieldModel, + currencyCode = state.amountBlockState.currencyUM.code, + ) + + AnimatedVisibility( + visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty, + ) { + if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) { + OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) + } + } + + SpacerH(20.dp) + + OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM) } } @Composable -private fun OnrampAmountField(amountField: AmountFieldModel) { +private fun OnrampHeaderTitle() { + Text( + text = stringResourceSafe(R.string.onramp_you_will_pay_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) +} + +@Composable +private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) { val decimalFormat = rememberDecimalFormat() val requester = remember { FocusRequester() } - val symbolColor = if (amountField.fiatValue.isBlank()) { - TangemTheme.colors.text.disabled - } else { - TangemTheme.colors.text.primary1 - } AmountTextField( value = amountField.fiatValue, decimals = amountField.fiatAmount.decimals, visualTransformation = AmountVisualTransformation( decimals = amountField.fiatAmount.decimals, - symbol = amountField.fiatAmount.currencySymbol, - currencyCode = amountField.fiatAmount.currencySymbol, + symbol = currencyCode, + currencyCode = currencyCode, decimalFormat = decimalFormat, - symbolColor = symbolColor, + symbolColor = if (amountField.fiatValue.isBlank()) { + TangemTheme.colors.text.disabled + } else { + TangemTheme.colors.text.primary1 + }, ), onValueChange = amountField.onValueChange, keyboardOptions = amountField.keyboardOptions, keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.h2.copy( + textStyle = TangemTheme.typography.head.copy( color = TangemTheme.colors.text.primary1, textAlign = TextAlign.Center, ), @@ -82,7 +112,8 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { modifier = Modifier .focusRequester(requester) .padding( - top = TangemTheme.dimens.spacing24, + top = TangemTheme.dimens.spacing8, + bottom = TangemTheme.dimens.spacing4, start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ) @@ -96,7 +127,7 @@ private fun OnrampAmountField(amountField: AmountFieldModel) { } @Composable -private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { +private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) { Box( modifier = Modifier .fillMaxWidth() @@ -107,24 +138,12 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { ), contentAlignment = Alignment.Center, ) { - when (state) { - is OnrampAmountSecondaryFieldUM.Content -> Text( - text = state.amount.resolveReference(), - style = TangemTheme.typography.caption2.copy(textDirection = TextDirection.ContentOrLtr), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ) - is OnrampAmountSecondaryFieldUM.Error -> Text( - text = state.error.resolveReference(), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - ) - is OnrampAmountSecondaryFieldUM.Loading -> TextShimmer( - style = TangemTheme.typography.caption2, - modifier = Modifier.width(TangemTheme.dimens.size62), - ) - } + Text( + text = state.error.resolveReference(), + color = TangemTheme.colors.text.warning, + style = TangemTheme.typography.caption2, + textAlign = TextAlign.Center, + ) } } @@ -132,20 +151,27 @@ private fun OnrampAmountSecondary(state: OnrampAmountSecondaryFieldUM) { private fun OnrampCurrencyIcon(currencyUM: OnrampCurrencyUM, modifier: Modifier = Modifier) { Row( modifier = modifier - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(14.dp)) + .background(TangemTheme.colors.button.secondary) .clickable(onClick = currencyUM.onClick) - .padding(start = TangemTheme.dimens.spacing24), + .padding(horizontal = 6.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), + horizontalArrangement = Arrangement.spacedBy(4.dp), ) { AsyncImage( modifier = Modifier - .size(TangemTheme.dimens.size40) + .size(20.dp) .clip(CircleShape) .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), model = currencyUM.iconUrl, contentDescription = null, ) + Text( + text = currencyUM.code, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold), + textAlign = TextAlign.Center, + ) Icon( modifier = Modifier .size(TangemTheme.dimens.size16) diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt deleted file mode 100644 index 11e356cfef..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampButtonComponent.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.tangem.features.onramp.main.ui - -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.ClickableText -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.extensions.appendColored -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampMainComponentUM -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM - -private const val TERMS_OF_USE_KEY = "termsOfUse" -private const val PRIVACY_POLICY_KEY = "privacyPolicy" - -@Composable -internal fun OnrampButtonComponent(state: OnrampMainComponentUM) { - val content = state as? OnrampMainComponentUM.Content - val providerState = content?.providerBlockState as? OnrampProviderBlockUM.Content - Column( - modifier = Modifier - .navigationBarsPadding() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - OnrampTosText(providerState) - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(id = R.string.common_buy), - onClick = state.buyButtonConfig.onClick, - enabled = state.buyButtonConfig.isEnabled, - ) - } -} - -@Composable -private fun OnrampTosText(provider: OnrampProviderBlockUM.Content?) { - val termsOfUse = stringResourceSafe(R.string.common_terms_of_use) - val privacyPolicy = stringResourceSafe(R.string.common_privacy_policy) - val tosText = stringResourceSafe(R.string.onramp_legal, termsOfUse, privacyPolicy) - - val clickableAnnotation = buildAnnotatedString { - append(tosText.substringBefore(termsOfUse)) - - pushStringAnnotation(TERMS_OF_USE_KEY, "") - appendColored(termsOfUse, TangemTheme.colors.text.accent) - pop() - - append(tosText.substringAfter(termsOfUse).substringBefore(privacyPolicy)) - - pushStringAnnotation(PRIVACY_POLICY_KEY, "") - appendColored(privacyPolicy, TangemTheme.colors.text.accent) - pop() - } - - AnimatedContent( - targetState = provider, - transitionSpec = { fadeIn().togetherWith(fadeOut()) }, - label = "Onramp Legal Info Animation", - ) { state -> - val termsOfUseLink = provider?.termsOfUseLink - val privacyPolicyLink = provider?.privacyPolicyLink - - if (state != null && termsOfUseLink != null && privacyPolicyLink != null) { - ClickableText( - text = clickableAnnotation, - style = TangemTheme.typography.caption2.copy( - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - ), - onClick = { offset -> - val tosAnnotations = clickableAnnotation.getStringAnnotations( - tag = TERMS_OF_USE_KEY, - start = offset, - end = offset, - ) - - if (tosAnnotations.any()) { - state.onLinkClick(termsOfUseLink) - } - - val privacyPolicyAnnotations = clickableAnnotation.getStringAnnotations( - tag = PRIVACY_POLICY_KEY, - start = offset, - end = offset, - ) - - if (privacyPolicyAnnotations.any()) { - state.onLinkClick(privacyPolicyLink) - } - }, - ) - } - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt similarity index 83% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt index f2ecc446d7..6b79790e18 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampFooterContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampFooterContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.ui +package com.tangem.features.onramp.main.ui import androidx.compose.animation.* import androidx.compose.animation.core.tween @@ -20,13 +20,13 @@ import com.tangem.core.ui.components.Keyboard import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.mainv2.entity.OnrampAmountButtonUM -import com.tangem.features.onramp.mainv2.entity.OnrampOffersBlockUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2AmountButtonUMState -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUM +import com.tangem.features.onramp.main.entity.OnrampAmountButtonUMState +import com.tangem.features.onramp.main.entity.OnrampMainComponentUM +import com.tangem.features.onramp.main.entity.OnrampOffersBlockUM @Composable -internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { +internal fun BoxScope.OnrampFooterContent(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { AnimatedVisibility( modifier = modifier .imePadding() @@ -53,16 +53,16 @@ internal fun BoxScope.OnrampFooterContent(state: OnrampV2MainComponentUM.Content } @Composable -private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) { +private fun OnrampAmountButtons(state: OnrampAmountButtonUMState) { val keyboard by keyboardAsState() AnimatedVisibility( - visible = state is OnrampV2AmountButtonUMState.Loaded, + visible = state is OnrampAmountButtonUMState.Loaded, enter = fadeIn(), exit = fadeOut(), ) { when (state) { - is OnrampV2AmountButtonUMState.Loaded -> { + is OnrampAmountButtonUMState.Loaded -> { if (keyboard is Keyboard.Opened) { LazyRow( modifier = Modifier.background(color = TangemTheme.colors.button.secondary), @@ -82,7 +82,7 @@ private fun OnrampAmountButtons(state: OnrampV2AmountButtonUMState) { } } } - OnrampV2AmountButtonUMState.None -> Unit + OnrampAmountButtonUMState.None -> Unit } } } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt index fa0797f476..2b24ad2fb9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampMainComponentContent.kt @@ -5,13 +5,12 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.FabPosition import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import com.tangem.core.ui.components.CircleShimmer +import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.appbar.TangemTopAppBar import com.tangem.core.ui.components.notifications.Notification @@ -21,41 +20,58 @@ import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.features.onramp.main.entity.OnrampMainComponentUM @Composable -internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { +internal fun OnrampMainScreen(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { Scaffold( - modifier = modifier.imePadding(), - contentWindowInsets = WindowInsetsZero, - containerColor = TangemTheme.colors.background.secondary, + modifier = modifier.systemBarsPadding(), topBar = { TangemTopAppBar( - modifier = Modifier.statusBarsPadding(), startButton = state.topBarConfig.startButtonUM, endButton = state.topBarConfig.endButtonUM, title = state.topBarConfig.title.resolveReference(), ) }, - content = { innerPadding -> - val contentModifier = Modifier - .padding(innerPadding) - .padding(horizontal = TangemTheme.dimens.spacing16) + contentWindowInsets = WindowInsetsZero, + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + OnrampMainComponentContent( + state = state, + modifier = Modifier.padding(scaffoldPaddings), + ) + } +} + +@Composable +internal fun OnrampMainComponentContent(state: OnrampMainComponentUM, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary), + ) { + Column( + modifier = Modifier .fillMaxWidth() - .wrapContentHeight() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { when (state) { - is OnrampMainComponentUM.InitialLoading -> InitialLoading(modifier = contentModifier, state = state) - is OnrampMainComponentUM.Content -> Content(modifier = contentModifier, state = state) + is OnrampMainComponentUM.InitialLoading -> InitialLoading(state = state) + is OnrampMainComponentUM.Content -> Content(state = state) } - }, - floatingActionButton = { - OnrampButtonComponent(state) - }, - floatingActionButtonPosition = FabPosition.Center, - ) + } + + if (state is OnrampMainComponentUM.Content) { + OnrampFooterContent(state = state) + } + } } @Composable private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier: Modifier = Modifier) { Column( - modifier = modifier, + modifier = modifier + .fillMaxWidth() + .wrapContentHeight() + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { OnrampAmountContentLoading() @@ -64,27 +80,38 @@ private fun InitialLoading(state: OnrampMainComponentUM.InitialLoading, modifier } @Composable -private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) { +private fun OnrampAmountContentLoading() { Column( - modifier = modifier + modifier = Modifier .fillMaxWidth() .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) .background(TangemTheme.colors.background.action) .padding(vertical = TangemTheme.dimens.spacing28), horizontalAlignment = Alignment.CenterHorizontally, ) { - CircleShimmer(modifier = Modifier.size(TangemTheme.dimens.size40)) RectangleShimmer( modifier = Modifier .padding(top = TangemTheme.dimens.spacing16) - .size(width = TangemTheme.dimens.size96, height = TangemTheme.dimens.size24), - radius = TangemTheme.dimens.radius3, + .size(width = 76.dp, height = 20.dp), + radius = TangemTheme.dimens.radius4, ) RectangleShimmer( modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16) - .size(width = TangemTheme.dimens.size72, height = TangemTheme.dimens.size12), - radius = TangemTheme.dimens.radius3, + .padding(top = TangemTheme.dimens.spacing12) + .size(width = 136.dp, height = 44.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing8) + .size(width = 52.dp, height = 16.dp), + radius = TangemTheme.dimens.radius4, + ) + RectangleShimmer( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing20) + .size(width = 84.dp, height = 28.dp), + radius = TangemTheme.dimens.radius14, ) } } @@ -93,13 +120,20 @@ private fun OnrampAmountContentLoading(modifier: Modifier = Modifier) { private fun Content(state: OnrampMainComponentUM.Content, modifier: Modifier = Modifier) { Column( modifier = modifier - .verticalScroll(rememberScrollState()) + .fillMaxWidth() + .wrapContentHeight() .navigationBarsPadding() - .padding(bottom = TangemTheme.dimens.spacing76), + .padding( + bottom = 76.dp, + start = 16.dp, + end = 16.dp, + ), verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), ) { - OnrampAmountContent(state = state.amountBlockState) - OnrampProviderContent(state = state.providerBlockState, modifier = Modifier.fillMaxWidth()) + OnrampAmountContent(state = state) + + OnrampOffersContent(state = state.offersBlockState) + if (state.errorNotification != null) Notification(config = state.errorNotification.config) } } \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt similarity index 99% rename from features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt rename to features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt index dba9eac25a..4c161835e9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampOffersContent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampOffersContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.onramp.mainv2.ui +package com.tangem.features.onramp.main.ui import android.content.res.Configuration import androidx.compose.animation.AnimatedVisibility @@ -33,7 +33,7 @@ import com.tangem.core.ui.test.OnrampOffersBlockTestTags import com.tangem.domain.onramp.model.OnrampPaymentMethod import com.tangem.domain.onramp.model.PaymentMethodType import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* +import com.tangem.features.onramp.main.entity.* import kotlinx.collections.immutable.persistentListOf @Composable diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt deleted file mode 100644 index e13fd96310..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/main/ui/OnrampProviderContent.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.features.onramp.main.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.CircularProgressIndicator -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.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle -import com.tangem.core.ui.extensions.appendSpace -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.main.entity.OnrampProviderBlockUM -import com.tangem.features.onramp.paymentmethod.ui.PaymentMethodIcon - -@Composable -internal fun OnrampProviderContent(state: OnrampProviderBlockUM, modifier: Modifier = Modifier) { - when (state) { - is OnrampProviderBlockUM.Empty -> Unit - is OnrampProviderBlockUM.Loading -> OnrampProviderLoading(modifier) - is OnrampProviderBlockUM.Content -> OnrampProviderBlock(modifier = modifier, state = state) - } -} - -@Composable -private fun OnrampProviderBlock(state: OnrampProviderBlockUM.Content, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .clickable(onClick = state.onClick) - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - PaymentMethodIcon(imageUrl = state.paymentMethod.imageUrl) - Column(modifier = Modifier.weight(1F)) { - Text( - text = buildAnnotatedString { - append(stringResourceSafe(id = R.string.onramp_pay_with)) - appendSpace() - withStyle( - style = SpanStyle( - fontWeight = TangemTheme.typography.subtitle2.fontWeight, - color = TangemTheme.colors.text.primary1, - ), - ) { - append(state.paymentMethod.name) - } - }, - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.body2, - ) - Text( - text = buildAnnotatedString { - append(stringResourceSafe(id = R.string.onramp_via)) - appendSpace() - append(state.providerName) - }, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - AnimatedVisibility( - visible = state.isBestRate, - enter = fadeIn(), - exit = fadeOut(), - label = "Best Rate visibility animation", - ) { - Text( - modifier = Modifier - .background( - color = TangemTheme.colors.icon.accent, - shape = RoundedCornerShape(TangemTheme.dimens.radius4), - ) - .padding( - horizontal = TangemTheme.dimens.spacing6, - vertical = TangemTheme.dimens.spacing1, - ), - text = stringResourceSafe(id = R.string.express_provider_best_rate), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary2, - ) - } - } -} - -@Composable -private fun OnrampProviderLoading(modifier: Modifier = Modifier) { - Column( - modifier = modifier - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Text( - text = stringResourceSafe(id = R.string.express_provider), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - CircularProgressIndicator( - color = TangemTheme.colors.icon.informative, - strokeWidth = TangemTheme.dimens.size2, - modifier = Modifier.size(TangemTheme.dimens.size16), - ) - Text( - text = stringResourceSafe(id = R.string.express_fetch_best_rates), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt deleted file mode 100644 index 4d417ad448..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainComponent.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.arkivanov.essenty.lifecycle.subscribe -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.features.onramp.alloffers.AllOffersComponent -import com.tangem.features.onramp.confirmresidency.ConfirmResidencyComponent -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainBottomSheetConfig -import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel -import com.tangem.features.onramp.mainv2.ui.OnrampNewMainScreen -import com.tangem.features.onramp.selectcurrency.SelectCurrencyComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultOnrampV2MainComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted private val params: OnrampV2MainComponent.Params, - private val confirmResidencyComponentFactory: ConfirmResidencyComponent.Factory, - private val selectCurrencyComponentFactory: SelectCurrencyComponent.Factory, - private val allOffersComponentFactory: AllOffersComponent.Factory, -) : OnrampV2MainComponent, AppComponentContext by appComponentContext { - - private val model: OnrampV2MainComponentModel = getOrCreateModel(params) - - init { - lifecycle.subscribe(onStop = model::onStop) - } - - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = null, - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsState() - val bottomSheet by bottomSheetSlot.subscribeAsState() - - OnrampNewMainScreen(modifier = modifier, state = state) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: OnrampV2MainBottomSheetConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (config) { - is OnrampV2MainBottomSheetConfig.ConfirmResidency -> confirmResidencyComponentFactory.create( - context = childByContext(componentContext), - params = ConfirmResidencyComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - country = config.country, - isLaunchSepa = false, - onDismiss = { - model.bottomSheetNavigation.dismiss() - model.handleOnrampAvailable() - }, - ), - ) - is OnrampV2MainBottomSheetConfig.CurrenciesList -> selectCurrencyComponentFactory.create( - context = childByContext(componentContext), - params = SelectCurrencyComponent.Params( - userWallet = model.userWallet, - cryptoCurrency = params.cryptoCurrency, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - is OnrampV2MainBottomSheetConfig.AllOffers -> allOffersComponentFactory.create( - context = childByContext(componentContext), - params = AllOffersComponent.Params( - userWallet = model.userWallet, - cryptoCurrency = params.cryptoCurrency, - onDismiss = model.bottomSheetNavigation::dismiss, - openRedirectPage = params.openRedirectPage, - amountCurrencyCode = config.amountCurrencyCode, - ), - ) - } - - @AssistedFactory - interface Factory : OnrampV2MainComponent.Factory { - override fun create( - context: AppComponentContext, - params: OnrampV2MainComponent.Params, - ): DefaultOnrampV2MainComponent - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt deleted file mode 100644 index 815fa5060b..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/DefaultOnrampV2MainFeatureToggle.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager - -class DefaultOnrampV2MainFeatureToggle( - private val featureTogglesManager: FeatureTogglesManager, -) : OnrampV2MainFeatureToggle { - override val isOnrampNewMainEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("NEW_ONRAMP_MAIN_ENABLED") -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt deleted file mode 100644 index 9767cf4496..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainComponent.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampSource - -internal interface OnrampV2MainComponent : ComposableContentComponent { - - data class Params( - val userWalletId: UserWalletId, - val cryptoCurrency: CryptoCurrency, - val source: OnrampSource, - val openSettings: () -> Unit, - val openRedirectPage: (quote: OnrampProviderWithQuote.Data) -> Unit, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt deleted file mode 100644 index 54595ff8d7..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/OnrampV2MainFeatureToggle.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.features.onramp.mainv2 - -internal interface OnrampV2MainFeatureToggle { - val isOnrampNewMainEnabled: Boolean -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt deleted file mode 100644 index 84fb039ffd..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampMainV2ComponentModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.onramp.mainv2.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.onramp.mainv2.model.OnrampV2MainComponentModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface OnrampMainV2ComponentModelModule { - - @Binds - @IntoMap - @ClassKey(OnrampV2MainComponentModel::class) - fun bindOnrampV2MainComponentModel(model: OnrampV2MainComponentModel): Model -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt deleted file mode 100644 index 08817d31ac..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/di/OnrampNewV2ComponentModule.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.onramp.mainv2.di - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainComponent -import com.tangem.features.onramp.mainv2.DefaultOnrampV2MainFeatureToggle -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle -import dagger.Binds -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface OnrampNewMainComponentModule { - - @Binds - @Singleton - fun bindOnrampV2MainComponentFactory(factory: DefaultOnrampV2MainComponent.Factory): OnrampV2MainComponent.Factory -} - -@Module -@InstallIn(SingletonComponent::class) -internal object FeatureToggleModule { - - @Provides - @Singleton - fun provideOnrampV2MainFeatureToggle(featureTogglesManager: FeatureTogglesManager): OnrampV2MainFeatureToggle { - return DefaultOnrampV2MainFeatureToggle(featureTogglesManager = featureTogglesManager) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt deleted file mode 100644 index 49041e269a..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2Intents.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampProviderWithQuote - -internal interface OnrampV2Intents { - fun onAmountValueChanged(value: String) - fun openSettings() - fun openCurrenciesList() - fun onBuyClick( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) - fun openProviders() - fun onRefresh() -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt deleted file mode 100644 index afc654a422..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainBottomSheetConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampCountry -import kotlinx.serialization.Serializable - -@Serializable -sealed interface OnrampV2MainBottomSheetConfig { - @Serializable - data class ConfirmResidency(val country: OnrampCountry) : OnrampV2MainBottomSheetConfig - - @Serializable - data object CurrenciesList : OnrampV2MainBottomSheetConfig - - @Serializable - data class AllOffers(val amountCurrencyCode: String) : OnrampV2MainBottomSheetConfig -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt deleted file mode 100644 index 256aeadd72..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2MainComponentUM.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference - -@Immutable -internal sealed interface OnrampV2MainComponentUM { - - val topBarConfig: OnrampV2MainTopBarUM - val errorNotification: NotificationUM? - - data class InitialLoading( - override val topBarConfig: OnrampV2MainTopBarUM, - override val errorNotification: NotificationUM?, - ) : OnrampV2MainComponentUM - - data class Content( - override val topBarConfig: OnrampV2MainTopBarUM, - override val errorNotification: NotificationUM?, - val amountBlockState: OnrampNewAmountBlockUM, - val offersBlockState: OnrampOffersBlockUM, - val onrampAmountButtonUMState: OnrampV2AmountButtonUMState, - ) : OnrampV2MainComponentUM -} - -internal data class OnrampV2MainTopBarUM( - val title: TextReference, - val startButtonUM: TopAppBarButtonUM, - val endButtonUM: TopAppBarButtonUM, -) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt deleted file mode 100644 index 750e9ba8f3..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/OnrampV2ProvidersUM.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity - -import com.tangem.domain.onramp.model.OnrampPaymentMethod - -sealed interface OnrampV2ProvidersUM { - - data object Empty : OnrampV2ProvidersUM - - data object Loading : OnrampV2ProvidersUM - - data class Content( - val providerId: String, - val paymentMethod: OnrampPaymentMethod, - ) : OnrampV2ProvidersUM -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt deleted file mode 100644 index 06ec867696..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/entity/factory/OnrampV2StateFactory.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.tangem.features.onramp.mainv2.entity.factory - -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.common.ui.notifications.NotificationUM -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.onramp.model.OnrampCurrency -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.tokens.model.Amount -import com.tangem.domain.tokens.model.AmountType -import com.tangem.domain.tokens.model.convertToAmount -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.utils.Provider -import java.math.BigDecimal - -internal class OnrampV2StateFactory( - private val currentStateProvider: Provider, - private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory, - private val cryptoCurrency: CryptoCurrency, - private val onrampIntents: OnrampV2Intents, -) { - - fun getInitialState( - currency: String, - onClose: () -> Unit, - openSettings: () -> Unit, - ): OnrampV2MainComponentUM.InitialLoading { - return OnrampV2MainComponentUM.InitialLoading( - errorNotification = null, - topBarConfig = OnrampV2MainTopBarUM( - title = combinedReference(resourceReference(R.string.common_buy), stringReference(" $currency")), - startButtonUM = TopAppBarButtonUM.Close( - onCloseClick = onClose, - enabled = true, - ), - endButtonUM = TopAppBarButtonUM.Icon( - iconRes = R.drawable.ic_more_vertical_24, - onClicked = openSettings, - isEnabled = false, - ), - ), - ) - } - - fun getReadyState(currency: OnrampCurrency): OnrampV2MainComponentUM.Content { - val state = currentStateProvider() - - val endButton = when (val button = state.topBarConfig.endButtonUM) { - is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) - is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) - } - - val initialAmountBlockState = getInitialAmountBlockState(currency) - - return OnrampV2MainComponentUM.Content( - topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - amountBlockState = initialAmountBlockState, - offersBlockState = OnrampOffersBlockUM.Empty, - errorNotification = null, - onrampAmountButtonUMState = onrampAmountButtonUMStateFactory.createOnrampAmountActionButton( - currencyCode = currency.code, - currencySymbol = currency.unit, - onAmountValueChanged = onrampIntents::onAmountValueChanged, - ), - ) - } - - fun getOnrampErrorState(onrampError: OnrampError): OnrampV2MainComponentUM { - return when (onrampError) { - OnrampError.PairsNotFound -> getNoPairsErrorState() - is OnrampError.DataError -> getErrorState( - errorCode = onrampError.code, - onRefresh = onrampIntents::onRefresh, - ) - is OnrampError.DomainError -> getErrorState(onRefresh = onrampIntents::onRefresh) - is OnrampError.AmountError.TooBigError, - is OnrampError.AmountError.TooSmallError, - OnrampError.RedirectError.VerificationFailed, - OnrampError.RedirectError.WrongRequestId, - OnrampError.AlreadyHandledTransaction, - -> currentStateProvider() // ignore error state - } - } - - fun getErrorState(errorCode: String? = null, onRefresh: () -> Unit): OnrampV2MainComponentUM { - val state = currentStateProvider() - val endButton = when (val button = state.topBarConfig.endButtonUM) { - is TopAppBarButtonUM.Icon -> button.copy(isEnabled = true) - is TopAppBarButtonUM.Text -> button.copy(isEnabled = true) - } - - return when (state) { - is OnrampV2MainComponentUM.Content -> state.copy( - topBarConfig = state.topBarConfig.copy(endButtonUM = endButton), - offersBlockState = OnrampOffersBlockUM.Empty, - errorNotification = NotificationUM.Warning.OnrampErrorNotification( - errorCode = errorCode, - onRefresh = onRefresh, - ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, - amountBlockState = state.amountBlockState.copy( - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ), - ) - is OnrampV2MainComponentUM.InitialLoading -> state.copy( - errorNotification = NotificationUM.Warning.OnrampErrorNotification( - errorCode = errorCode, - onRefresh = onRefresh, - ), - ) - } - } - - private fun getNoPairsErrorState(): OnrampV2MainComponentUM { - val state = currentStateProvider() - val contentState = state as? OnrampV2MainComponentUM.Content ?: return state - - return contentState.copy( - amountBlockState = contentState.amountBlockState.copy( - amountFieldModel = contentState.amountBlockState.amountFieldModel.copy(isError = true), - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Error( - error = resourceReference(R.string.onramp_no_available_providers), - ), - ), - onrampAmountButtonUMState = OnrampV2AmountButtonUMState.None, - offersBlockState = OnrampOffersBlockUM.Empty, - ) - } - - private fun getInitialAmountBlockState(currency: OnrampCurrency): OnrampNewAmountBlockUM { - return OnrampNewAmountBlockUM( - currencyUM = OnrampNewCurrencyUM( - code = currency.code, - iconUrl = currency.image, - precision = currency.precision, - onClick = onrampIntents::openCurrenciesList, - unit = currency.unit, - ), - amountFieldModel = AmountFieldModel( - value = "", - fiatValue = "", - onValueChange = onrampIntents::onAmountValueChanged, - keyboardOptions = KeyboardOptions( - imeAction = ImeAction.None, - keyboardType = KeyboardType.Number, - ), - keyboardActions = KeyboardActions(), - isFiatValue = true, - cryptoAmount = BigDecimal.ZERO.convertToAmount(cryptoCurrency), - fiatAmount = BigDecimal.ZERO.convertToFiatAmount(currency), - isError = false, - isWarning = false, - error = TextReference.EMPTY, - isFiatUnavailable = false, - isValuePasted = false, - onValuePastedTriggerDismiss = {}, - ), - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ) - } - - private fun BigDecimal.convertToFiatAmount(currency: OnrampCurrency): Amount = Amount( - currencySymbol = currency.unit, - value = this, - decimals = currency.precision, - type = AmountType.FiatType(currency.code), - ) -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt deleted file mode 100644 index 517c304fd6..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/model/OnrampV2MainComponentModel.kt +++ /dev/null @@ -1,417 +0,0 @@ -package com.tangem.features.onramp.mainv2.model - -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.ui.components.fields.InputManager -import com.tangem.domain.onramp.* -import com.tangem.domain.onramp.analytics.OnrampAnalyticsEvent -import com.tangem.domain.onramp.model.OnrampAvailability -import com.tangem.domain.onramp.model.OnrampProviderWithQuote -import com.tangem.domain.onramp.model.OnrampQuote -import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.entity.* -import com.tangem.features.onramp.mainv2.entity.factory.OnrampAmountButtonUMStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampOffersStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2AmountStateFactory -import com.tangem.features.onramp.mainv2.entity.factory.OnrampV2StateFactory -import com.tangem.features.onramp.utils.sendOnrampErrorEvent -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.PeriodicTask -import com.tangem.utils.coroutines.SingleTaskScheduler -import com.tangem.utils.coroutines.runSuspendCatching -import com.tangem.utils.isNullOrZero -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -@Suppress("LongParameterList", "LargeClass") -internal class OnrampV2MainComponentModel @Inject constructor( - override val dispatchers: CoroutineDispatcherProvider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val router: Router, - private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, - private val getOnrampCountryUseCase: GetOnrampCountryUseCase, - private val clearOnrampCacheUseCase: ClearOnrampCacheUseCase, - private val fetchQuotesUseCase: OnrampFetchQuotesUseCase, - private val getOnrampQuotesUseCase: GetOnrampQuotesUseCase, - private val fetchPairsUseCase: OnrampFetchPairsUseCase, - private val amountInputManager: InputManager, - private val getOnrampOffersUseCase: GetOnrampOffersUseCase, - paramsContainer: ParamsContainer, - getWalletsUseCase: GetWalletsUseCase, -) : Model(), OnrampV2Intents { - - val params = paramsContainer.require() - - private val onrampAmountButtonUMStateFactory: OnrampAmountButtonUMStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampAmountButtonUMStateFactory() - } - - @Suppress("PropertyUsedBeforeDeclaration") - private val stateFactory: OnrampV2StateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampV2StateFactory( - currentStateProvider = Provider { state.value }, - cryptoCurrency = params.cryptoCurrency, - onrampIntents = this, - onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, - ) - } - - val state: StateFlow - field = MutableStateFlow( - value = stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = ::onCloseClick, - openSettings = ::openSettings, - ), - ) - - private val amountStateFactory: OnrampV2AmountStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampV2AmountStateFactory( - currentStateProvider = Provider { state.value }, - analyticsEventHandler = analyticsEventHandler, - onrampIntents = this, - onrampAmountButtonUMStateFactory = onrampAmountButtonUMStateFactory, - ) - } - - private val onrampOffersStateFactory: OnrampOffersStateFactory by lazy(LazyThreadSafetyMode.NONE) { - OnrampOffersStateFactory( - currentStateProvider = Provider { state.value }, - onrampIntents = this, - ) - } - - private val quotesTaskScheduler = SingleTaskScheduler() - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val userWallet = getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId } - - init { - modelScope.launch { - clearOnrampCacheUseCase() - } - startLoadingQuotes() - sendScreenOpenAnalytics() - checkResidenceCountry() - subscribeToAmountChanges() - subscribeToCountryAndCurrencyUpdates() - subscribeToQuotesUpdate() - subscribeOnOffers() - } - - override fun onDestroy() { - modelScope.launch { clearOnrampCacheUseCase.invoke() } - quotesTaskScheduler.cancelTask() - super.onDestroy() - } - - override fun onAmountValueChanged(value: String) { - state.update { amountStateFactory.getOnAmountValueChange(value) } - modelScope.launch { amountInputManager.update(value) } - } - - override fun openSettings() { - params.openSettings.invoke() - } - - override fun openCurrenciesList() { - analyticsEventHandler.send(OnrampAnalyticsEvent.SelectCurrencyScreenOpened()) - bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.CurrenciesList) - } - - override fun onBuyClick( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) { - val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return - analyticsEventHandler.send( - OnrampAnalyticsEvent.OnBuyClick( - providerName = quote.provider.info.name, - currency = currentContentState.amountBlockState.currencyUM.code, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - sendOfferClickEvent( - quote = quote, - onrampOfferAdvantagesUM = onrampOfferAdvantagesUM, - categoryUM = categoryUM, - ) - params.openRedirectPage(quote) - } - - override fun openProviders() { - val currentContentState = state.value as? OnrampV2MainComponentUM.Content ?: return - val amountCurrentCode = currentContentState.amountBlockState.currencyUM.code - bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.AllOffers(amountCurrentCode)) - } - - override fun onRefresh() { - state.update { - stateFactory.getInitialState( - currency = params.cryptoCurrency.name, - onClose = router::pop, - openSettings = ::openSettings, - ) - } - modelScope.launch { - clearOnrampCacheUseCase.invoke() - checkResidenceCountry() - handleOnrampAvailable() - } - } - - fun onStop() { - quotesTaskScheduler.cancelTask() - } - - fun handleOnrampAvailable() { - subscribeToCountryAndCurrencyUpdates() - subscribeToQuotesUpdate() - } - - private fun startLoadingQuotes() { - quotesTaskScheduler.cancelTask() - quotesTaskScheduler.scheduleTask(scope = modelScope, task = loadQuotesTask()) - } - - private fun loadQuotesTask(): PeriodicTask { - return PeriodicTask( - delay = UPDATE_DELAY, - task = { - runSuspendCatching { - val amountBlockState = (state.value as? OnrampV2MainComponentUM.Content)?.amountBlockState - ?: return@runSuspendCatching - - val fiatAmount = amountBlockState.amountFieldModel.fiatAmount - if (fiatAmount.value.isNullOrZero()) return@runSuspendCatching - - fetchQuotesUseCase.invoke( - userWallet = userWallet, - amount = amountBlockState.amountFieldModel.fiatAmount, - cryptoCurrency = params.cryptoCurrency, - ).onLeft(::handleOnrampError) - } - }, - onSuccess = {}, - onError = {}, - ) - } - - private fun checkResidenceCountry() { - modelScope.launch { - checkOnrampAvailabilityUseCase(userWallet) - .onRight(::handleOnrampAvailability) - .onLeft(::handleOnrampError) - } - } - - private fun handleOnrampAvailability(availability: OnrampAvailability) { - when (availability) { - is OnrampAvailability.Available -> Unit - is OnrampAvailability.ConfirmResidency, - is OnrampAvailability.NotSupported, - -> bottomSheetNavigation.activate(OnrampV2MainBottomSheetConfig.ConfirmResidency(availability.country)) - } - } - - private fun onCloseClick() { - analyticsEventHandler.send(OnrampAnalyticsEvent.CloseOnramp()) - router.pop() - } - - private fun subscribeOnOffers() = modelScope.launch { - getOnrampOffersUseCase - .invoke() - .collectLatest { maybeOffers -> - maybeOffers.fold( - ifLeft = ::handleOnrampError, - ifRight = { offers -> - val currentState = state.value - if (currentState is OnrampV2MainComponentUM.Content) { - if (currentState.amountBlockState.amountFieldModel.fiatValue.isEmpty()) { - state.update { - currentState.copy(offersBlockState = OnrampOffersBlockUM.Empty) - } - return@fold - } - state.update { - onrampOffersStateFactory.getOffersState(offers) - } - } - }, - ) - } - } - - private fun subscribeToAmountChanges() = modelScope.launch { - amountInputManager.query - .filter(String::isNotEmpty) - .collectLatest { _ -> - startLoadingQuotes() - } - } - - private fun subscribeToCountryAndCurrencyUpdates() { - getOnrampCountryUseCase.invoke() - .onEach { maybeCountry -> - maybeCountry.fold( - ifLeft = ::handleOnrampError, - ifRight = { country -> - if (country == null) return@onEach - state.update { prevState -> - when (prevState) { - is OnrampV2MainComponentUM.Content -> { - amountStateFactory.getUpdatedCurrencyState(country.defaultCurrency) - } - is OnrampV2MainComponentUM.InitialLoading -> { - stateFactory.getReadyState(country.defaultCurrency) - } - } - } - updatePairsAndQuotes() - }, - ) - } - .launchIn(modelScope) - } - - private fun subscribeToQuotesUpdate() { - getOnrampQuotesUseCase.invoke() - .conflate() - .onEach { maybeQuotes -> - maybeQuotes.fold( - ifLeft = ::handleOnrampError, - ifRight = ::handleQuoteResult, - ) - } - .launchIn(modelScope) - } - - private fun handleQuoteResult(quotes: List) { - sendOnrampQuotesErrorAnalytic(quotes) - when { - quotes.isEmpty() -> { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - } - quotes.all { it is OnrampQuote.AmountError } -> { - state.update { amountStateFactory.getSecondaryFieldAmountErrorState(quotes) } - } - quotes.none { it is OnrampQuote.Data } -> { - state.update { stateFactory.getErrorState(onRefresh = ::onRetryQuotes) } - } - else -> { - state.update { prevState -> - val resetState = amountStateFactory.getAmountSecondaryFieldResetState() - if (prevState is OnrampV2MainComponentUM.Content && - resetState is OnrampV2MainComponentUM.Content && - prevState.offersBlockState is OnrampOffersBlockUM.Loading - ) { - resetState.copy(offersBlockState = OnrampOffersBlockUM.Empty) - } else { - resetState - } - } - } - } - } - - private fun onRetryQuotes() { - state.update { prevState -> - (prevState as? OnrampV2MainComponentUM.Content)?.copy( - errorNotification = null, - offersBlockState = OnrampOffersBlockUM.Loading, - amountBlockState = prevState.amountBlockState.copy( - secondaryFieldModel = OnrampSecondaryFieldErrorUM.Empty, - ), - ) ?: prevState - } - startLoadingQuotes() - } - - private suspend fun updatePairsAndQuotes() { - fetchPairsUseCase.invoke(userWallet, params.cryptoCurrency).fold( - ifLeft = ::handleOnrampError, - ifRight = { - state.update { - amountStateFactory.getAmountSecondaryFieldResetState() - } - startLoadingQuotes() - }, - ) - } - - private fun handleOnrampError(onrampError: OnrampError) { - Timber.e(onrampError.toString()) - state.update { stateFactory.getOnrampErrorState(onrampError) } - } - - private fun sendOnrampQuotesErrorAnalytic(quotes: List) { - quotes.forEach { errorState -> - when (errorState) { - is OnrampQuote.Error -> analyticsEventHandler.sendOnrampErrorEvent( - error = errorState.error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = errorState.provider.info.name, - paymentMethod = errorState.paymentMethod.name, - ) - is OnrampQuote.AmountError -> analyticsEventHandler.sendOnrampErrorEvent( - error = errorState.error, - tokenSymbol = params.cryptoCurrency.symbol, - providerName = errorState.provider.info.name, - paymentMethod = errorState.paymentMethod.name, - ) - else -> Unit - } - } - } - - private fun sendScreenOpenAnalytics() { - analyticsEventHandler.send( - OnrampAnalyticsEvent.ScreenOpened( - source = params.source, - tokenSymbol = params.cryptoCurrency.symbol, - ), - ) - } - - private fun sendOfferClickEvent( - quote: OnrampProviderWithQuote.Data, - onrampOfferAdvantagesUM: OnrampOfferAdvantagesUM, - categoryUM: OnrampOfferCategoryUM, - ) { - val event = when (categoryUM) { - OnrampOfferCategoryUM.RecentlyUsed -> { - OnrampAnalyticsEvent.RecentlyBuyClicked( - tokenSymbol = params.cryptoCurrency.symbol, - providerName = quote.provider.info.name, - paymentMethod = quote.paymentMethod.name, - ) - } - OnrampOfferCategoryUM.Recommended -> { - onrampOfferAdvantagesUM.toAnalyticsEvent( - cryptoCurrencySymbol = params.cryptoCurrency.symbol, - providerName = quote.provider.info.name, - paymentMethodName = quote.paymentMethod.name, - ) - } - } - - if (event != null) { - analyticsEventHandler.send(event) - } - } - - private companion object { - const val UPDATE_DELAY = 10_000L - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt deleted file mode 100644 index 7e9499e37d..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampNewMainComponentContent.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.tangem.features.onramp.mainv2.ui - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Scaffold -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.unit.dp -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.utils.WindowInsetsZero -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM - -@Composable -internal fun OnrampNewMainScreen(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) { - Scaffold( - modifier = modifier.systemBarsPadding(), - topBar = { - TangemTopAppBar( - startButton = state.topBarConfig.startButtonUM, - endButton = state.topBarConfig.endButtonUM, - title = state.topBarConfig.title.resolveReference(), - ) - }, - contentWindowInsets = WindowInsetsZero, - containerColor = TangemTheme.colors.background.secondary, - ) { scaffoldPaddings -> - OnrampNewMainComponentContent( - state = state, - modifier = Modifier.padding(scaffoldPaddings), - ) - } -} - -@Composable -internal fun OnrampNewMainComponentContent(state: OnrampV2MainComponentUM, modifier: Modifier = Modifier) { - Box( - modifier = modifier - .fillMaxSize() - .background(TangemTheme.colors.background.secondary), - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - when (state) { - is OnrampV2MainComponentUM.InitialLoading -> InitialLoading(state = state) - is OnrampV2MainComponentUM.Content -> Content(state = state) - } - } - - if (state is OnrampV2MainComponentUM.Content) { - OnrampFooterContent(state = state) - } - } -} - -@Composable -private fun InitialLoading(state: OnrampV2MainComponentUM.InitialLoading, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight() - .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - OnrampAmountContentLoading() - if (state.errorNotification != null) Notification(config = state.errorNotification.config) - } -} - -@Composable -private fun OnrampAmountContentLoading() { - Column( - modifier = Modifier - .fillMaxWidth() - .clip(shape = RoundedCornerShape(size = TangemTheme.dimens.radius16)) - .background(TangemTheme.colors.background.action) - .padding(vertical = TangemTheme.dimens.spacing28), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16) - .size(width = 76.dp, height = 20.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12) - .size(width = 136.dp, height = 44.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing8) - .size(width = 52.dp, height = 16.dp), - radius = TangemTheme.dimens.radius4, - ) - RectangleShimmer( - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing20) - .size(width = 84.dp, height = 28.dp), - radius = TangemTheme.dimens.radius14, - ) - } -} - -@Composable -private fun Content(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .wrapContentHeight() - .navigationBarsPadding() - .padding( - bottom = 76.dp, - start = 16.dp, - end = 16.dp, - ), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - OnrampV2AmountContent(state = state) - - OnrampOffersContent(state = state.offersBlockState) - - if (state.errorNotification != null) Notification(config = state.errorNotification.config) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt deleted file mode 100644 index 8cc5c7aca6..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/mainv2/ui/OnrampV2AmountContent.kt +++ /dev/null @@ -1,184 +0,0 @@ -package com.tangem.features.onramp.mainv2.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -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.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import com.tangem.common.ui.amountScreen.models.AmountFieldModel -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.fields.AmountTextField -import com.tangem.core.ui.components.fields.visualtransformations.AmountVisualTransformation -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.BuyTokenDetailsScreenTestTags -import com.tangem.core.ui.utils.rememberDecimalFormat -import com.tangem.features.onramp.impl.R -import com.tangem.features.onramp.mainv2.entity.OnrampNewCurrencyUM -import com.tangem.features.onramp.mainv2.entity.OnrampSecondaryFieldErrorUM -import com.tangem.features.onramp.mainv2.entity.OnrampV2MainComponentUM - -@Composable -internal fun OnrampV2AmountContent(state: OnrampV2MainComponentUM.Content, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .background( - color = TangemTheme.colors.background.action, - shape = RoundedCornerShape(size = TangemTheme.dimens.radius16), - ) - .padding(vertical = 24.dp, horizontal = 16.dp) - .animateContentSize(), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - OnrampHeaderTitle() - - OnrampAmountField( - amountField = state.amountBlockState.amountFieldModel, - currencyCode = state.amountBlockState.currencyUM.code, - ) - - AnimatedVisibility( - visible = state.amountBlockState.secondaryFieldModel !is OnrampSecondaryFieldErrorUM.Empty, - ) { - if (state.amountBlockState.secondaryFieldModel is OnrampSecondaryFieldErrorUM.Error) { - OnrampAmountSecondary(state = state.amountBlockState.secondaryFieldModel) - } - } - - SpacerH(20.dp) - - OnrampCurrencyIcon(currencyUM = state.amountBlockState.currencyUM) - } -} - -@Composable -private fun OnrampHeaderTitle() { - Text( - text = stringResourceSafe(R.string.onramp_you_will_pay_title), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun OnrampAmountField(amountField: AmountFieldModel, currencyCode: String) { - val decimalFormat = rememberDecimalFormat() - val requester = remember { FocusRequester() } - AmountTextField( - value = amountField.fiatValue, - decimals = amountField.fiatAmount.decimals, - visualTransformation = AmountVisualTransformation( - decimals = amountField.fiatAmount.decimals, - symbol = currencyCode, - currencyCode = currencyCode, - decimalFormat = decimalFormat, - symbolColor = if (amountField.fiatValue.isBlank()) { - TangemTheme.colors.text.disabled - } else { - TangemTheme.colors.text.primary1 - }, - ), - onValueChange = amountField.onValueChange, - keyboardOptions = amountField.keyboardOptions, - keyboardActions = amountField.keyboardActions, - textStyle = TangemTheme.typography.head.copy( - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - ), - isEnabled = !amountField.isError, - isAutoResize = true, - isValuePasted = amountField.isValuePasted, - onValuePastedTriggerDismiss = amountField.onValuePastedTriggerDismiss, - modifier = Modifier - .focusRequester(requester) - .padding( - top = TangemTheme.dimens.spacing8, - bottom = TangemTheme.dimens.spacing4, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ) - .requiredHeightIn(min = TangemTheme.dimens.size32) - .testTag(BuyTokenDetailsScreenTestTags.FIAT_AMOUNT_TEXT_FIELD), - ) - - LaunchedEffect(key1 = Unit) { - requester.requestFocus() - } -} - -@Composable -private fun OnrampAmountSecondary(state: OnrampSecondaryFieldErrorUM.Error) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding( - top = TangemTheme.dimens.spacing8, - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - ), - contentAlignment = Alignment.Center, - ) { - Text( - text = state.error.resolveReference(), - color = TangemTheme.colors.text.warning, - style = TangemTheme.typography.caption2, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -private fun OnrampCurrencyIcon(currencyUM: OnrampNewCurrencyUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(RoundedCornerShape(14.dp)) - .background(TangemTheme.colors.button.secondary) - .clickable(onClick = currencyUM.onClick) - .padding(horizontal = 6.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - ) { - AsyncImage( - modifier = Modifier - .size(20.dp) - .clip(CircleShape) - .testTag(BuyTokenDetailsScreenTestTags.FIAT_CURRENCY_ICON), - model = currencyUM.iconUrl, - contentDescription = null, - ) - Text( - text = currencyUM.code, - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.SemiBold), - textAlign = TextAlign.Center, - ) - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .testTag(BuyTokenDetailsScreenTestTags.EXPAND_FIAT_LIST_BUTTON), - painter = painterResource(id = R.drawable.ic_chevron_24), - tint = TangemTheme.colors.icon.informative, - contentDescription = null, - ) - } -} \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt index 1133a72cfc..daa379e850 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/root/DefaultOnrampComponent.kt @@ -17,8 +17,6 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.onramp.component.OnrampComponent import com.tangem.features.onramp.main.OnrampMainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainComponent -import com.tangem.features.onramp.mainv2.OnrampV2MainFeatureToggle import com.tangem.features.onramp.redirect.OnrampRedirectComponent import com.tangem.features.onramp.root.entity.OnrampChild import com.tangem.features.onramp.settings.OnrampSettingsComponent @@ -32,9 +30,7 @@ internal class DefaultOnrampComponent @AssistedInject constructor( @Assisted private val params: OnrampComponent.Params, private val settingsComponentFactory: OnrampSettingsComponent.Factory, private val onrampMainComponentFactory: OnrampMainComponent.Factory, - private val onrampMainV2ComponentFactory: OnrampV2MainComponent.Factory, private val onrampRedirectComponentFactory: OnrampRedirectComponent.Factory, - private val onrampV2MainFeatureToggle: OnrampV2MainFeatureToggle, ) : OnrampComponent, AppComponentContext by context { private val navigation = StackNavigation() @@ -71,44 +67,23 @@ internal class DefaultOnrampComponent @AssistedInject constructor( onBack = navigation::pop, ), ) - OnrampChild.Main -> if (onrampV2MainFeatureToggle.isOnrampNewMainEnabled) { - onrampMainV2ComponentFactory.create( - context = childByContext(componentContext), - params = OnrampV2MainComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - openSettings = { navigation.push(OnrampChild.Settings) }, - source = params.source, - openRedirectPage = { quote -> - navigation.push( - OnrampChild.RedirectPage( - quote = quote, - cryptoCurrency = params.cryptoCurrency, - ), - ) - }, - ), - ) - } else { - onrampMainComponentFactory.create( - context = childByContext(componentContext), - params = OnrampMainComponent.Params( - userWalletId = params.userWalletId, - cryptoCurrency = params.cryptoCurrency, - openSettings = { navigation.push(OnrampChild.Settings) }, - source = params.source, - openRedirectPage = { onrampProviderWithQuoteData -> - navigation.push( - OnrampChild.RedirectPage( - quote = onrampProviderWithQuoteData, - cryptoCurrency = params.cryptoCurrency, - ), - ) - }, - isLaunchSepa = params.shouldLaunchSepa, - ), - ) - } + OnrampChild.Main -> onrampMainComponentFactory.create( + context = childByContext(componentContext), + params = OnrampMainComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrency = params.cryptoCurrency, + openSettings = { navigation.push(OnrampChild.Settings) }, + source = params.source, + openRedirectPage = { quote -> + navigation.push( + OnrampChild.RedirectPage( + quote = quote, + cryptoCurrency = params.cryptoCurrency, + ), + ) + }, + ), + ) is OnrampChild.RedirectPage -> onrampRedirectComponentFactory.create( context = childByContext(componentContext), params = OnrampRedirectComponent.Params( From 54c160f4f2b736a05c252ce58d38b87bddb348cd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 17 Feb 2026 20:07:31 +0500 Subject: [PATCH 24/97] Updated on 2026-08-14 --- .../com/tangem/common/TangemSiteUrlBuilder.kt | 2 + .../common/ui/notifications/Notifications.kt | 2 +- .../core/ui/ds/message/TangemMessage.kt | 17 +- .../utils/WalletWarningsAnalyticsSender.kt | 73 +++- .../utils/WalletWarningsSingleEventSender.kt | 48 ++- .../GetWalletNotificationsCarouselFactory.kt | 131 +++++++ .../wallet/domain/GetWalletWarningsFactory.kt | 326 +++++++++++++++ .../wallet/state/model/WalletBalanceUM.kt | 13 +- .../state/model/WalletNotificationUM.kt | 371 +++++++++++------- .../wallet/state/model/WalletUM.kt | 4 +- .../transformers/SetWarningsTransformer.kt | 15 +- .../MultiWalletWarningsSubscriber.kt | 15 +- .../MultiWalletWarningsSubscriberV2.kt | 71 ++++ .../SingleWalletNotificationsSubscriber.kt | 3 +- 14 files changed, 914 insertions(+), 177 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt diff --git a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt index 6dc7f0d926..ab45c7159d 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemSiteUrlBuilder.kt @@ -9,6 +9,8 @@ import kotlin.coroutines.suspendCoroutine object TangemSiteUrlBuilder { + const val NOTE_MIGRATION_URL = "https://tangem.com/en/?promocode=Note10" + suspend fun getUtmTags(campaign: String?): String { val langCode = Locale.getDefault().language val utmCampaignPart = campaign?.let { "&utm_campaign=$it-$langCode" }.orEmpty() diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt index 1c3870ab40..39c4d36faa 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -124,7 +124,7 @@ fun LazyListScope.notifications( contentColor = contentColor, modifier = modifier .padding(top = topPadding) - .animateItem(), + .animateItem(null, null, null), ) }, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt index 3591920330..86d955f2d9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessage.kt @@ -22,11 +22,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import com.tangem.core.ui.R -import com.tangem.core.ui.components.flicker import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.ds.button.* import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -58,7 +58,15 @@ fun TangemMessage( if (messageUM.iconUM != null) { TangemIcon( tangemIconUM = messageUM.iconUM, - modifier = Modifier.size(TangemTheme.dimens2.x8), + modifier = Modifier + .align( + if (messageUM.buttonsUM.isEmpty()) { + Alignment.CenterVertically + } else { + Alignment.Top + }, + ) + .size(TangemTheme.dimens2.x7), ) } }, @@ -342,6 +350,7 @@ private class TangemMessagePreviewProvider : PreviewParameterProvider) { + if (screenLifecycleProvider.isBackgroundState.value) return + if (newNotifications.isEmpty()) return + if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return + + val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel + val notificationsDiff = newNotifications.filter { it !in totalNotifications } + + val eventsToSend = getEvents2(notificationsDiff) + + eventsToSend.forEach { event -> + analyticsEventHandler.send(event) + } + } + private fun getEvents(warnings: List): Set { return warnings.mapNotNullTo(mutableSetOf(), ::getEvent) } + private fun getEvents2(notifications: List): Set { + return notifications.mapNotNullTo(mutableSetOf(), ::getEvent2) + } + @Suppress("CyclomaticComplexMethod") private fun getEvent(warning: WalletNotification): AnalyticsEvent? { return when (warning) { @@ -106,4 +124,53 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.UpgradeHotWalletPromo -> null } } + + @Suppress("CyclomaticComplexMethod") + private fun getEvent2(notificationUM: WalletNotificationUM): AnalyticsEvent? { + return when (notificationUM) { + WalletNotificationUM.DevCard -> DevelopmentCard() + WalletNotificationUM.FailedCardValidation -> ProductSampleCard() + is WalletNotificationUM.MissingBackup -> BackupYourWallet() + is WalletNotificationUM.NumberOfSignedHashesIncorrect -> CardSignedTransactions() + WalletNotificationUM.TestnetCard -> TestnetCard() + WalletNotificationUM.DemoCard -> DemoCard() + is WalletNotificationUM.MissingAddresses -> MissingAddresses() + is WalletNotificationUM.RateApp -> HowDoYouLikeTangem() + is WalletNotificationUM.BackupError -> BackupError() + is WalletNotificationUM.NoteMigration -> NotePromo() + is WalletNotificationUM.OnePlusOnePromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.OnePlusOne, + ) + is WalletNotificationUM.YieldPromo -> NoticePromotionBanner( + source = AnalyticsParam.ScreensSources.Main, + program = Program.YieldPromo, + ) + is WalletNotificationUM.FinishWalletActivation -> { + val activationState = if (notificationUM.isBackupExists) { + NoticeFinishActivation.ActivationState.Unfinished + } else { + NoticeFinishActivation.ActivationState.NotStarted + } + val balanceState = when (notificationUM.type) { + WalletNotificationType.Warning -> AnalyticsParam.EmptyFull.Full + else -> AnalyticsParam.EmptyFull.Empty + } + NoticeFinishActivation( + activationState = activationState, + balanceState = balanceState, + ) + } + is WalletNotificationUM.SeedPhraseNotification -> NoticeSeedPhraseSupport() + is WalletNotificationUM.SeedPhraseSecondNotification -> NoticeSeedPhraseSupportSecond() + is WalletNotificationUM.PushNotifications -> PushBanner() + is WalletNotificationUM.UnlockWallets, + is WalletNotificationUM.NoAccount, + is WalletNotificationUM.LowSignatures, + WalletNotificationUM.SomeNetworksUnreachable, + is WalletNotificationUM.UsedOutdatedData, + is WalletNotificationUM.CloreMigration, + -> null + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt index f52290e6d2..fdcdc567b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsSingleEventSender.kt @@ -4,12 +4,8 @@ import com.tangem.common.routing.AppRoute.WalletBackup import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 -import com.tangem.core.ui.components.bottomsheets.message.icon -import com.tangem.core.ui.components.bottomsheets.message.infoBlock -import com.tangem.core.ui.components.bottomsheets.message.onClick -import com.tangem.core.ui.components.bottomsheets.message.primaryButton -import com.tangem.core.ui.components.bottomsheets.message.secondaryButton +import com.tangem.core.ui.components.bottomsheets.message.* +import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.domain.models.wallet.UserWallet @@ -19,7 +15,9 @@ import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject @@ -71,6 +69,44 @@ internal class WalletWarningsSingleEventSender @Inject constructor( } } + suspend fun send( + userWalletId: UserWalletId, + displayedWalletUM: WalletUM?, + newNotifications: List, + ) { + if (screenLifecycleProvider.isBackgroundState.value) return + if (newNotifications.isEmpty()) return + if (displayedWalletUM == null || displayedWalletUM.pullToRefreshConfig.isRefreshing) return + + val totalNotifications = displayedWalletUM.notifications + displayedWalletUM.notificationsCarousel + val events = newNotifications.filter { it !in totalNotifications } + + // We must show activation bs only for the first seen wallet when open the app (if need, see conditions below), + // so we keep this wallet id and use for future checks, ignore other wallets during the app session. + if (isActivationBottomSheetShown.isEmpty()) { + isActivationBottomSheetShown[userWalletId] = false + } + + events.forEach { event -> + when (event) { + is WalletNotificationUM.SeedPhraseNotification -> { + seedPhraseNotificationUseCase.notified(userWalletId = userWalletId) + } + is WalletNotificationUM.FinishWalletActivation -> { + // We check that map contains the first seen wallet (will return null instead false/true otherwise) + // and for this wallet we haven't shown the activation bs yet (check that returns false, not true) + if (isActivationBottomSheetShown[userWalletId] == false) { + if (event.messageEffect == TangemMessageEffect.Warning && event.isBackupExists.not()) { + showFinishActivationBottomSheet(userWalletId) + } + isActivationBottomSheetShown[userWalletId] = true + } + } + else -> Unit + } + } + } + private fun showFinishActivationBottomSheet(userWalletId: UserWalletId) { val userWallet = getUserWalletUseCase(userWalletId).getOrNull() ?: return if (userWallet !is UserWallet.Hot) return diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt new file mode 100644 index 0000000000..8e0d92d545 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt @@ -0,0 +1,131 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.TangemSiteUrlBuilder +import com.tangem.common.ui.notifications.NotificationId +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.notifications.repository.NotificationsRepository +import com.tangem.domain.promo.ShouldShowPromoWalletUseCase +import com.tangem.domain.promo.models.PromoId +import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.wallets.usecase.GetWalletsUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.utils.extensions.addIf +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.conflate +import kotlinx.coroutines.flow.distinctUntilChanged +import javax.inject.Inject + +/** + * Factory for creating a list of notifications that can be shown on the wallet screen. + * These notifications are not critical and can be stacked with each other. + */ +@ModelScoped +internal class GetWalletNotificationsCarouselFactory @Inject constructor( + private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, + private val shouldShowPromoWalletUseCase: ShouldShowPromoWalletUseCase, + private val getWalletsUseCase: GetWalletsUseCase, + private val notificationsRepository: NotificationsRepository, +) { + fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + return combine( + flow = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.YieldPromo) + .distinctUntilChanged(), + flow2 = notificationsRepository.getShouldShowNotification( + NotificationId.EnablePushesReminderNotification.key, + ).distinctUntilChanged(), + flow3 = shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.OnePlusOne) + .distinctUntilChanged(), + flow4 = isReadyToShowRateAppUseCase().distinctUntilChanged(), + flow5 = getWalletsUseCase().conflate(), + ) { showYieldPromo, showPushesNotification, showOnePlusOnePromo, showRateAppPromo, wallets -> + + buildList { + addNoteMigrationNotification(userWallet, wallets, clickIntents) + addRateAppNotification(showRateAppPromo, clickIntents) + addOnePlusOnePromoNotification(clickIntents, showOnePlusOnePromo) + addYieldPromoNotification(clickIntents, showYieldPromo) + + addPushNotification( + shouldShow = showPushesNotification, + isPushesAllowed = notificationsRepository.isUserAllowToSubscribeOnPushNotifications(), + clickIntents = clickIntents, + ) + }.sortedBy { it.type.ordinal }.toImmutableList() + } + } + + private fun MutableList.addRateAppNotification( + isReadyToShowRating: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf(isReadyToShowRating) { + WalletNotificationUM.RateApp( + onLikeClick = clickIntents::onLikeAppClick, + onDislikeClick = clickIntents::onDislikeAppClick, + onCloseClick = clickIntents::onCloseRateAppWarningClick, + ) + } + } + + private fun MutableList.addYieldPromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf(shouldShowPromo) { + WalletNotificationUM.YieldPromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.YieldPromo) }, + onTermsAndConditionsClick = { clickIntents.onYieldPromoTermsAndConditionsClick() }, + ) + } + } + + private fun MutableList.addOnePlusOnePromoNotification( + clickIntents: WalletClickIntents, + shouldShowPromo: Boolean, + ) { + addIf(shouldShowPromo) { + WalletNotificationUM.OnePlusOnePromo( + onCloseClick = { clickIntents.onClosePromoClick(promoId = PromoId.OnePlusOne) }, + onClick = { clickIntents.onPromoClick(promoId = PromoId.OnePlusOne) }, + ) + } + } + + private fun MutableList.addNoteMigrationNotification( + userWallet: UserWallet, + userWallets: List, + clickIntents: WalletClickIntents, + ) { + val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver + + val isUserHasWalletOrWallet2 = userWallets.filterIsInstance().any { wallet -> + val typesResolver = wallet.scanResponse.cardTypesResolver + typesResolver.isTangemWallet() || typesResolver.isWallet2() + } + + addIf(cardTypesResolver != null && cardTypesResolver.isTangemNote() && !isUserHasWalletOrWallet2) { + WalletNotificationUM.NoteMigration( + onClick = { clickIntents.onNoteMigrationButtonClick(TangemSiteUrlBuilder.NOTE_MIGRATION_URL) }, + ) + } + } + + private fun MutableList.addPushNotification( + shouldShow: Boolean, + isPushesAllowed: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf(shouldShow && !isPushesAllowed) { + WalletNotificationUM.PushNotifications( + onCloseClick = clickIntents::onDenyPermissions, + onEnabledClick = clickIntents::onAllowPermissions, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt new file mode 100644 index 0000000000..c900381523 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt @@ -0,0 +1,326 @@ +package com.tangem.feature.wallet.presentation.wallet.domain + +import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.ds.message.TangemMessageEffect +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.demo.IsDemoCardUseCase +import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +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.wallet.UserWallet +import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus +import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase +import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.account.AccountDependencies +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.hot.sdk.model.HotWalletId +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.extensions.addIf +import com.tangem.utils.extensions.isPositive +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.* +import javax.inject.Inject + +/** + * Factory for creating a list of notifications that can be shown on the wallet screen. + * These notifications are critical and should be shown separately from each other. + */ +@Suppress("LongParameterList") +@ModelScoped +internal class GetWalletWarningsFactory @Inject constructor( + private val isDemoCardUseCase: IsDemoCardUseCase, + private val isNeedToBackupUseCase: IsNeedToBackupUseCase, + private val backupValidator: BackupValidator, + private val seedPhraseNotificationUseCase: SeedPhraseNotificationUseCase, + private val accountDependencies: AccountDependencies, + private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, + private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, +) { + fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { + val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver + + val params = SingleAccountStatusListProducer.Params(userWallet.walletId) + val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) + + return combine( + flow = accountStatusListFlow, + flow2 = isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), + flow3 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), + flow4 = getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), + ) { accountList, isNeedToBackup, seedPhraseIssueStatus, shouldAccessCodeSkipped -> + val totalFiatBalance = accountList.totalFiatBalance + val flattenCurrencies = accountList.flattenCurrencies() + + buildList { + addUsedOutdatedDataNotification(totalFiatBalance) + + addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) + + addFinishWalletActivationNotification( + userWallet = userWallet, + totalFiatBalance = totalFiatBalance, + clickIntents = clickIntents, + shouldAccessCodeSkipped = shouldAccessCodeSkipped, + ) + + addInformationalNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + ) + + addWarningNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + isNeedToBackup = isNeedToBackup, + clickIntents = clickIntents, + ) + }.sortedBy { it.type.ordinal }.toImmutableList() + } + } + + private fun MutableList.addUsedOutdatedDataNotification(totalFiatBalance: TotalFiatBalance) { + addIf( + element = WalletNotificationUM.UsedOutdatedData, + condition = (totalFiatBalance as? TotalFiatBalance.Loaded)?.source == StatusSource.ONLY_CACHE, + ) + } + + private fun MutableList.addCriticalNotifications( + userWallet: UserWallet, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + if (userWallet !is UserWallet.Cold) { + return + } + + addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents) + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + addIf( + element = WalletNotificationUM.BackupError { clickIntents.onSupportClick() }, + condition = !backupValidator.isValidBackupStatus(userWallet.scanResponse.card) || userWallet.hasBackupError, + ) + + addIf( + element = WalletNotificationUM.DevCard, + condition = !cardTypesResolver.isReleaseFirmwareType(), + ) + + addIf( + element = WalletNotificationUM.FailedCardValidation, + condition = cardTypesResolver.isReleaseFirmwareType() && cardTypesResolver.isAttestationFailed(), + ) + + cardTypesResolver.getRemainingSignatures()?.let { remainingSignatures -> + addIf( + element = WalletNotificationUM.LowSignatures(count = remainingSignatures), + condition = remainingSignatures <= MAX_REMAINING_SIGNATURES_COUNT, + ) + } + } + + private fun MutableList.addInformationalNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver?, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.DemoCard, + condition = cardTypesResolver != null && isDemoCardUseCase(cardId = cardTypesResolver.getCardId()), + ) + + addMissingAddressesNotification(userWallet, flattenCurrencies, clickIntents) + } + + private fun MutableList.addMissingAddressesNotification( + userWallet: UserWallet, + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + val currencies = flattenCurrencies.getMissingAddressCurrencies().ifEmpty { return } + + addIf( + element = WalletNotificationUM.MissingAddresses( + tangemIcon = walletInterationIcon(userWallet), + missingAddressesCount = currencies.count(), + onGenerateClick = { + clickIntents.onGenerateMissedAddressesClick(missedAddressCurrencies = currencies) + }, + ), + condition = currencies.isNotEmpty(), + ) + } + + private fun List.getMissingAddressCurrencies(): List { + return this + .filter { it.value is CryptoCurrencyStatus.MissedDerivation } + .map(CryptoCurrencyStatus::currency) + } + + private suspend fun MutableList.addWarningNotifications( + userWallet: UserWallet, + cardTypesResolver: CardTypesResolver?, + flattenCurrencies: List, + isNeedToBackup: Boolean, + clickIntents: WalletClickIntents, + ) { + addIf( + element = WalletNotificationUM.MissingBackup( + onClick = clickIntents::onAddBackupCardClick, + ), + condition = isNeedToBackup, + ) + + addIf( + element = WalletNotificationUM.TestnetCard, + condition = cardTypesResolver?.isTestCard() == true, + ) + + addIf( + element = WalletNotificationUM.SomeNetworksUnreachable, + condition = flattenCurrencies.hasUnreachableNetworks(), + ) + + addCloreMigrationNotification(flattenCurrencies, clickIntents) + + addNoAccountWarning(cryptoCurrencyStatus = flattenCurrencies.firstOrNull()) + + addIf( + element = WalletNotificationUM.NumberOfSignedHashesIncorrect( + onCloseClick = clickIntents::onCloseAlreadySignedHashesWarningClick, + ), + condition = hasSignedHashes(userWallet, flattenCurrencies.firstOrNull()), + ) + } + + private fun MutableList.addNoAccountWarning(cryptoCurrencyStatus: CryptoCurrencyStatus?) { + val noAccountStatus = cryptoCurrencyStatus?.value as? CryptoCurrencyStatus.NoAccount + if (noAccountStatus != null) { + add( + element = WalletNotificationUM.NoAccount( + network = cryptoCurrencyStatus.currency.name, + amount = noAccountStatus.amountToCreateAccount.toString(), + symbol = cryptoCurrencyStatus.currency.symbol, + ), + ) + } + } + + private fun MutableList.addCloreMigrationNotification( + flattenCurrencies: List, + clickIntents: WalletClickIntents, + ) { + val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return + + add( + WalletNotificationUM.CloreMigration( + onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) }, + ), + ) + } + + private fun List.findCloreCurrency(): CryptoCurrencyStatus? { + return find { currencyStatus -> + BlockchainUtils.isClore(currencyStatus.currency.network.rawId) + } + } + + private fun List.hasUnreachableNetworks(): Boolean { + return any { it.value is CryptoCurrencyStatus.Unreachable } + } + + private fun MutableList.addFinishWalletActivationNotification( + userWallet: UserWallet, + totalFiatBalance: TotalFiatBalance, + clickIntents: WalletClickIntents, + shouldAccessCodeSkipped: Boolean, + ) { + if (userWallet !is UserWallet.Hot) return + + val isBackupExists = userWallet.backedUp + val isAccessCodeRequired = userWallet.hotWalletId.authType == HotWalletId.AuthType.NoPassword && + !shouldAccessCodeSkipped + val shouldShowFinishActivation = !isBackupExists || isAccessCodeRequired + + val messageEffect = when (totalFiatBalance) { + TotalFiatBalance.Failed, + TotalFiatBalance.Loading, + -> TangemMessageEffect.None + is TotalFiatBalance.Loaded -> if (totalFiatBalance.amount.orZero().isPositive()) { + TangemMessageEffect.Warning + } else { + TangemMessageEffect.None + } + } + + addIf( + element = WalletNotificationUM.FinishWalletActivation( + messageEffect = messageEffect, + onClick = { clickIntents.onFinishWalletActivationClick(isBackupExists) }, + isBackupExists = isBackupExists, + ), + condition = shouldShowFinishActivation, + ) + } + + private fun MutableList.addSeedNotificationIfNeeded( + userWallet: UserWallet.Cold, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + val isNotificationAvailable = with(userWallet) { + val isDemo = isDemoCardUseCase(cardId = userWallet.cardId) + val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported + + !isDemo && isWalletWithSeedPhrase + } + + when (seedPhraseIssueStatus) { + SeedPhraseNotificationsStatus.SHOW_FIRST -> addIf( + element = WalletNotificationUM.SeedPhraseNotification( + onDeclineClick = clickIntents::onSeedPhraseNotificationDecline, + onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.SHOW_SECOND -> addIf( + element = WalletNotificationUM.SeedPhraseSecondNotification( + onDeclineClick = clickIntents::onSeedPhraseSecondNotificationReject, + onConfirmClick = clickIntents::onSeedPhraseSecondNotificationAccept, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.NOT_NEEDED -> Unit + } + } + + private suspend fun hasSignedHashes( + selectedWallet: UserWallet, + cryptoCurrencyStatus: CryptoCurrencyStatus?, + ): Boolean { + if (selectedWallet !is UserWallet.Cold || !selectedWallet.isMultiCurrency) return false + val network = cryptoCurrencyStatus?.currency?.network ?: return false + + return hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = network) + .conflate() + .distinctUntilChanged() + .firstOrNull() == true + } + + private companion object { + const val MAX_REMAINING_SIGNATURES_COUNT = 10 + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt index f55919de64..cdbe103b75 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt @@ -4,7 +4,17 @@ import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId -/** Wallet card state */ +/** + * Represents the state of the wallet balance in the UI. + * + * The sealed interface has three implementations: + * - [Content]: Represents the state when the wallet balance is successfully loaded. + * - [Error]: Represents the state when there was an error loading the wallet balance. + * - [Loading]: Represents the state when the wallet balance is currently being loaded. + * + * @property id The unique identifier of the wallet. + * @property name The name of the wallet. + */ @Immutable internal sealed interface WalletBalanceUM { @@ -27,7 +37,6 @@ internal sealed interface WalletBalanceUM { val balance: TextReference, val isBalanceFlickering: Boolean, val isZeroBalance: Boolean?, - ) : WalletBalanceUM /** diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt index eba6f7dbbe..9d24a0c3da 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -34,14 +34,33 @@ internal enum class WalletNotificationType { */ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val type: WalletNotificationType) { - data object DevCard : WalletNotificationUM( + // region Status + data object SomeNetworksUnreachable : WalletNotificationUM( messageUM = TangemMessageUM( - id = "DevCardNotification", - title = resourceReference(id = R.string.warning_developer_card_title), - subtitle = resourceReference(id = R.string.warning_developer_card_message), + id = "SomeNetworksUnreachableNotification", + title = resourceReference(id = R.string.warning_some_networks_unreachable_title), + subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), messageEffect = TangemMessageEffect.None, ), - type = WalletNotificationType.Warning, + type = WalletNotificationType.Status, + ) + + data object UsedOutdatedData : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "UsedOutdatedDataNotification", + title = stringReference("Missing some token balances"), // todo redesign main lokalise + subtitle = stringReference("Will be updated as soon as possible"), // todo redesign main lokalise + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_error_sync_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + messageEffect = TangemMessageEffect.None, + ), + type = WalletNotificationType.Status, ) data object FailedCardValidation : WalletNotificationUM( @@ -49,17 +68,69 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t id = "FailedCardValidationNotification", title = resourceReference(id = R.string.warning_failed_to_verify_card_title), subtitle = resourceReference(id = R.string.warning_failed_to_verify_card_message), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), messageEffect = TangemMessageEffect.Warning, ), type = WalletNotificationType.Status, ) + data object DevCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DevCardNotification", + title = resourceReference(id = R.string.warning_developer_card_title), + subtitle = resourceReference(id = R.string.warning_developer_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + + data object TestnetCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "TestnetCardNotification", + title = resourceReference(id = R.string.warning_testnet_card_title), + subtitle = resourceReference(id = R.string.warning_testnet_card_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + + data object DemoCard : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "DemoCardNotification", + title = resourceReference(id = R.string.warning_demo_mode_title), + subtitle = resourceReference(id = R.string.warning_demo_mode_message), + messageEffect = TangemMessageEffect.None, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + ), + type = WalletNotificationType.Status, + ) + // endregion + + // region Critical data class BackupError(val onClick: () -> Unit) : WalletNotificationUM( messageUM = TangemMessageUM( id = "BackupErrorNotification", title = resourceReference(id = R.string.warning_backup_errors_title), subtitle = resourceReference(id = R.string.warning_backup_errors_message), messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(id = R.string.common_contact_support), @@ -68,7 +139,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), ), ), - type = WalletNotificationType.Warning, + type = WalletNotificationType.Critical, ) data class SeedPhraseNotification( @@ -92,6 +163,10 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t onClick = onConfirmClick, ), ), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), ), type = WalletNotificationType.Critical, ) @@ -105,6 +180,10 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t title = resourceReference(id = R.string.warning_seedphrase_action_required_title), subtitle = resourceReference(id = R.string.warning_seedphrase_contacted_support), messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(id = R.string.seed_warning_no), @@ -117,6 +196,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t onClick = onConfirmClick, ), ), + ), type = WalletNotificationType.Critical, ) @@ -127,6 +207,10 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t title = resourceReference(id = R.string.warning_no_backup_title), subtitle = resourceReference(id = R.string.warning_no_backup_message), messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(id = R.string.button_start_backup_process), @@ -138,37 +222,6 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t type = WalletNotificationType.Critical, ) - data object SomeNetworksUnreachable : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "SomeNetworksUnreachableNotification", - title = resourceReference(id = R.string.warning_some_networks_unreachable_title), - subtitle = resourceReference(id = R.string.warning_some_networks_unreachable_message), - messageEffect = TangemMessageEffect.None, - ), - type = WalletNotificationType.Status, - ) - - data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "NumberOfSignedHashesIncorrectNotification", - title = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_title), - subtitle = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_message), - messageEffect = TangemMessageEffect.Warning, - onCloseClick = onCloseClick, - ), - type = WalletNotificationType.Warning, - ) - - data object TestnetCard : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "TestnetCardNotification", - title = resourceReference(id = R.string.warning_testnet_card_title), - subtitle = resourceReference(id = R.string.warning_testnet_card_message), - messageEffect = TangemMessageEffect.None, - ), - type = WalletNotificationType.Warning, - ) - data class LowSignatures(val count: Int) : WalletNotificationUM( messageUM = TangemMessageUM( id = "LowSignaturesNotification", @@ -177,11 +230,69 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t id = R.string.warning_low_signatures_message, formatArgs = wrappedList(count.toString()), ), + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), messageEffect = TangemMessageEffect.None, ), type = WalletNotificationType.Critical, ) + data class FinishWalletActivation( + val messageEffect: TangemMessageEffect, + val isBackupExists: Boolean, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "FinishWalletActivationNotification", + title = resourceReference(R.string.hw_activation_need_title), + subtitle = if (isBackupExists) { + resourceReference(R.string.hw_activation_need_warning_description) + } else { + resourceReference(R.string.hw_activation_need_description) + }, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { + when (messageEffect) { + TangemMessageEffect.Warning -> TangemTheme.colors2.graphic.neutral.primary + else -> TangemTheme.colors2.graphic.status.attention + } + }, + ), + messageEffect = messageEffect, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.hw_activation_need_finish), + type = TangemButtonType.PrimaryInverse, + onClick = onClick, + ), + ), + ), + type = when (messageEffect) { + TangemMessageEffect.Warning -> WalletNotificationType.Critical + else -> WalletNotificationType.Warning + }, + ) + + data class NumberOfSignedHashesIncorrect(val onCloseClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NumberOfSignedHashesIncorrectNotification", + title = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_title), + subtitle = resourceReference(id = R.string.warning_number_of_signed_hashes_incorrect_message), + messageEffect = TangemMessageEffect.Warning, + iconUM = TangemIconUM.Icon( + iconRes = R.drawable.img_knight_shield_32, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + onCloseClick = onCloseClick, + ), + type = WalletNotificationType.Critical, + ) + // endregion + + // region Warning data class MissingAddresses( @DrawableRes val tangemIcon: Int?, val missingAddressesCount: Int, @@ -196,12 +307,11 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t formatArgs = wrappedList(missingAddressesCount), ), isCentered = true, - iconUM = tangemIcon?.let { TangemIconUM.Icon(it) }, - messageEffect = TangemMessageEffect.Magic, + messageEffect = TangemMessageEffect.Card, buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(id = R.string.common_generate_addresses), - type = TangemButtonType.PrimaryInverse, + type = TangemButtonType.Primary, iconRes = tangemIcon, onClick = onGenerateClick, ), @@ -223,33 +333,6 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t type = WalletNotificationType.Warning, ) - data object DemoCard : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "DemoCardNotification", - title = resourceReference(id = R.string.warning_demo_mode_title), - subtitle = resourceReference(id = R.string.warning_demo_mode_message), - messageEffect = TangemMessageEffect.None, - ), - type = WalletNotificationType.Warning, - ) - - data class NoteMigration(val onClick: () -> Unit) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "NoteMigrationNotification", - title = resourceReference(R.string.wallet_promo_banner_title), - subtitle = resourceReference(R.string.wallet_promo_banner_description), - messageEffect = TangemMessageEffect.None, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(R.string.wallet_promo_banner_button_title), - onClick = onClick, - type = TangemButtonType.PrimaryInverse, - ), - ), - ), - type = WalletNotificationType.Promo, - ) - data class UnlockWallets(val onClick: () -> Unit) : WalletNotificationUM( messageUM = TangemMessageUM( id = "UnlockWalletsNotification", @@ -261,12 +344,76 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), ), onClick = onClick, - messageEffect = TangemMessageEffect.Magic, + messageEffect = TangemMessageEffect.Card, isCentered = true, ), type = WalletNotificationType.Warning, ) + // endregion + // region Promo + data class NoteMigration(val onClick: () -> Unit) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "NoteMigrationNotification", + title = resourceReference(R.string.wallet_promo_banner_title), + subtitle = resourceReference(R.string.wallet_promo_banner_description), + messageEffect = TangemMessageEffect.Magic, + isCentered = true, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.wallet_promo_banner_button_title), + onClick = onClick, + type = TangemButtonType.Primary, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class OnePlusOnePromo( + val onCloseClick: () -> Unit, + val onClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "OnePlusOnePromoNotification", + title = resourceReference(R.string.notification_one_plus_one_title), + subtitle = resourceReference(R.string.notification_one_plus_one_text), + messageEffect = TangemMessageEffect.Magic, + onCloseClick = onCloseClick, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_one_plus_one_button), + type = TangemButtonType.Primary, + onClick = onClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + + data class YieldPromo( + val onCloseClick: () -> Unit, + val onTermsAndConditionsClick: () -> Unit, + ) : WalletNotificationUM( + messageUM = TangemMessageUM( + id = "YieldPromoNotification", + title = resourceReference(R.string.notification_yield_promo_title), + subtitle = resourceReference(R.string.notification_yield_promo_text), + onCloseClick = onCloseClick, + messageEffect = TangemMessageEffect.Magic, + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = resourceReference(R.string.notification_yield_promo_button), + type = TangemButtonType.Primary, + onClick = onTermsAndConditionsClick, + ), + ), + ), + type = WalletNotificationType.Promo, + ) + // endregion + + // region Survey data class RateApp( val onLikeClick: () -> Unit, val onDislikeClick: () -> Unit, @@ -294,49 +441,9 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), type = WalletNotificationType.Survey, ) + // endregion - data object UsedOutdatedData : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "UsedOutdatedDataNotification", - title = stringReference("Missing some token balances"), // todo redesign main lokalise - subtitle = stringReference("Will be updated as soon as possible"), - messageEffect = TangemMessageEffect.None, - ), - type = WalletNotificationType.Status, - ) - - data class FinishWalletActivation( - val messageEffect: TangemMessageEffect, - val isBackupExists: Boolean, - val onClick: () -> Unit, - ) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "FinishWalletActivationNotification", - title = resourceReference(R.string.hw_activation_need_title), - subtitle = if (isBackupExists) { - resourceReference(R.string.hw_activation_need_warning_description) - } else { - resourceReference(R.string.hw_activation_need_description) - }, - iconUM = TangemIconUM.Icon( - iconRes = R.drawable.img_knight_shield_32, - tintReference = { TangemTheme.colors2.graphic.status.attention }, - ), - messageEffect = messageEffect, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(R.string.hw_activation_need_finish), - type = TangemButtonType.PrimaryInverse, - onClick = onClick, - ), - ), - ), - type = when (messageEffect) { - TangemMessageEffect.Card -> WalletNotificationType.Critical - else -> WalletNotificationType.Warning - }, - ) - + // region Informational data class PushNotifications( val onCloseClick: () -> Unit, val onEnabledClick: () -> Unit, @@ -346,7 +453,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t title = resourceReference(R.string.user_push_notification_banner_title), subtitle = resourceReference(R.string.user_push_notification_banner_subtitle), onCloseClick = onCloseClick, - messageEffect = TangemMessageEffect.Card, + messageEffect = TangemMessageEffect.Magic, buttonsUM = persistentListOf( TangemMessageButtonUM( text = resourceReference(R.string.common_later), @@ -385,45 +492,5 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t ), type = WalletNotificationType.Informational, ) - - data class OnePlusOnePromo( - val onCloseClick: () -> Unit, - val onClick: () -> Unit, - ) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "OnePlusOnePromoNotification", - title = resourceReference(R.string.notification_one_plus_one_title), - subtitle = resourceReference(R.string.notification_one_plus_one_text), - messageEffect = TangemMessageEffect.Magic, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(R.string.notification_one_plus_one_button), - type = TangemButtonType.PrimaryInverse, - onClick = onCloseClick, - ), - ), - ), - type = WalletNotificationType.Promo, - ) - - data class YieldPromo( - val onCloseClick: () -> Unit, - val onTermsAndConditionsClick: () -> Unit, - ) : WalletNotificationUM( - messageUM = TangemMessageUM( - id = "YieldPromoNotification", - title = resourceReference(R.string.notification_yield_promo_title), - subtitle = resourceReference(R.string.notification_yield_promo_text), - onCloseClick = onCloseClick, - messageEffect = TangemMessageEffect.Magic, - buttonsUM = persistentListOf( - TangemMessageButtonUM( - text = resourceReference(R.string.notification_yield_promo_button), - type = TangemButtonType.Primary, - onClick = onTermsAndConditionsClick, - ), - ), - ), - type = WalletNotificationType.Promo, - ) + // endregion } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt index 896c34b9aa..d7b3a5af2d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletUM.kt @@ -15,6 +15,7 @@ internal sealed interface WalletUM { val buttons: PersistentList val notifications: ImmutableList + val notificationsCarousel: ImmutableList val tokensListUM: WalletTokensListUM @@ -29,11 +30,11 @@ internal sealed interface WalletUM { override val walletsBalanceUM: WalletBalanceUM, override val buttons: PersistentList, override val notifications: ImmutableList, + override val notificationsCarousel: ImmutableList, override val tokensListUM: WalletTokensListUM, override val nftState: WalletNFTItemUM, override val type: WalletType, override val tangemPayState: TangemPayState, - val stackableNotifications: ImmutableList, ) : WalletUM data class Locked( @@ -42,6 +43,7 @@ internal sealed interface WalletUM { override val type: WalletType, override val notifications: ImmutableList = persistentListOf(), ) : WalletUM { + override val notificationsCarousel: ImmutableList = persistentListOf() override val pullToRefreshConfig = PullToRefreshConfig(false, {}) override val tokensListUM: WalletTokensListUM = WalletTokensListUM.Empty // todo redesign main locked state override val nftState: WalletNFTItemUM = WalletNFTItemUM.Hidden diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt index 3a20a52812..fde5efd497 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetWarningsTransformer.kt @@ -2,14 +2,18 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import timber.log.Timber internal class SetWarningsTransformer( userWalletId: UserWalletId, private val warnings: ImmutableList, + private val notifications: ImmutableList = persistentListOf(), + private val notificationsCarousel: ImmutableList = persistentListOf(), ) : WalletStateTransformer(userWalletId) { override fun transform(prevState: WalletState): WalletState { @@ -26,6 +30,15 @@ internal class SetWarningsTransformer( } override fun transform(walletUM: WalletUM): WalletUM { - return walletUM // todo redesign main + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + notifications = notifications, + notificationsCarousel = notificationsCarousel, + ) + is WalletUM.Locked -> { + Timber.w("Impossible to update notifications for locked wallet") + walletUM + } + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index a8c3a91d43..577da2f275 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -9,12 +9,9 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.conflate -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* internal class MultiWalletWarningsSubscriber( private val userWallet: UserWallet, @@ -37,7 +34,13 @@ internal class MultiWalletWarningsSubscriber( it.wallets.any { walletState -> walletState.walletCardState.id == userWallet.walletId } } - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateHolder.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = warnings, + notifications = persistentListOf(), + ), + ) walletWarningsAnalyticsSender.send(displayedState, warnings) walletWarningsSingleEventSender.send( userWalletId = userWallet.walletId, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt new file mode 100644 index 0000000000..f7e10a9ecf --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt @@ -0,0 +1,71 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender +import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender +import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletNotificationsCarouselFactory +import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.* + +@Suppress("LongParameterList") +internal class MultiWalletWarningsSubscriberV2( + private val userWallet: UserWallet, + private val stateHolder: WalletStateController, + private val clickIntents: WalletClickIntents, + private val getWalletWarningsFactory: GetWalletWarningsFactory, + private val getWalletNotificationsCarouselFactory: GetWalletNotificationsCarouselFactory, + private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, + private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, +) : WalletSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow> { + return combine( + flow = getWalletWarningsFactory.create(userWallet, clickIntents).conflate().distinctUntilChanged(), + flow2 = getWalletNotificationsCarouselFactory.create(userWallet, clickIntents).conflate() + .distinctUntilChanged(), + ) { notifications, notificationsCarousel -> + val displayedWalletUM = stateHolder.getWalletUM(userWallet.walletId) + + // Wait until the wallet appears in the list + stateHolder.uiState.first { + it.wallets2.any { walletUM -> walletUM.walletsBalanceUM.id == userWallet.walletId } + } + + // If there are notifications, we need to filter out the RateApp notification from stackable notifications, + // because it should not be shown together with other notifications. + val alteredNotificationsCarousel = if (notifications.isNotEmpty()) { + notificationsCarousel.filterNot { it is WalletNotificationUM.RateApp } + } else { + notificationsCarousel + }.toPersistentList() + + stateHolder.update( + SetWarningsTransformer( + userWalletId = userWallet.walletId, + warnings = persistentListOf(), + notifications = notifications, + notificationsCarousel = alteredNotificationsCarousel, + ), + ) + + val totalNotifications = (notifications + alteredNotificationsCarousel).toPersistentList() + + walletWarningsAnalyticsSender.send(displayedWalletUM, totalNotifications) + walletWarningsSingleEventSender.send( + userWalletId = userWallet.walletId, + displayedWalletUM = displayedWalletUM, + newNotifications = totalNotifications, + ) + + totalNotifications + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt index d815a9c21c..349dce0765 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -8,6 +8,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.conflate @@ -32,7 +33,7 @@ internal class SingleWalletNotificationsSubscriber( .onEach { warnings -> val displayedState = stateHolder.getWalletState(userWallet.walletId) - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings)) + stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings, persistentListOf())) walletWarningsAnalyticsSender.send(displayedState, warnings) } } From 2348d3456f53f1b98d9baa2c3e4d7d8ff144eeda Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 17:05:56 +0500 Subject: [PATCH 25/97] Updated on 2026-08-14 --- .../TangemPullToRefreshContainer.kt | 3 +- features/wallet/impl/build.gradle.kts | 16 + .../wallet/child/wallet/WalletComponent.kt | 44 +- .../presentation/common/WalletPreviewData.kt | 12 - .../preview/WalletBalancePreview.kt | 38 ++ .../state/model/WalletBottomSheetConfig.kt | 51 -- .../TypedWalletStateTransformer.kt | 22 - .../presentation/wallet/ui/WalletScreen.kt | 1 - .../presentation/wallet/ui/WalletScreen2.kt | 440 ++++++++++++++++++ .../ui/components/common/WalletBottomSheet.kt | 141 ------ 10 files changed, 528 insertions(+), 240 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt index 386f268756..19e1491cf4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/pullToRefresh/TangemPullToRefreshContainer.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.res.TangemThemePreview fun TangemPullToRefreshContainer( config: PullToRefreshConfig, modifier: Modifier = Modifier, + indicatorModifier: Modifier = Modifier, content: @Composable () -> Unit, ) { val state = rememberPullToRefreshState() @@ -32,7 +33,7 @@ fun TangemPullToRefreshContainer( modifier = modifier, indicator = { Indicator( - modifier = Modifier.align(Alignment.TopCenter), + modifier = indicatorModifier.align(Alignment.TopCenter), isRefreshing = config.isRefreshing, state = state, containerColor = TangemTheme.colors.background.tertiary, diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 1105aae82f..b7e61208c1 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -9,6 +9,12 @@ plugins { android { namespace = "com.tangem.feature.wallet.impl" + packaging { + resources { + // To build and run composable preview + merges += "paymentrequest.proto" + } + } } dependencies { @@ -44,6 +50,16 @@ dependencies { exclude(group = "com.google.firebase", module = "protolite-well-known-types") exclude(group = "com.google.protobuf", module = "protobuf-javalite") } + implementation(deps.haze) { + exclude(module = "activity-compose") + exclude(module = "activity") + exclude(module = "activity-ktx") + } + implementation(deps.haze.materials) { + exclude(module = "activity-compose") + exclude(module = "activity") + exclude(module = "activity-ktx") + } /** DI */ implementation(deps.hilt.android) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 8d96777c9e..199094b424 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ExperimentalDecomposeApi import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss @@ -14,6 +15,7 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent @@ -23,6 +25,7 @@ import com.tangem.feature.wallet.child.wallet.model.WalletModel import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen +import com.tangem.feature.wallet.presentation.wallet.ui.WalletScreen2 import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.features.biometry.AskBiometryComponent @@ -38,6 +41,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.launch +@OptIn(ExperimentalDecomposeApi::class) @Suppress("LongParameterList") internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @@ -50,6 +54,7 @@ internal class WalletComponent @AssistedInject constructor( private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory, private val feedFeatureToggle: FeedFeatureToggle, + private val designFeatureToggles: DesignFeatureToggles, ) : ComposableContentComponent, AppComponentContext by appComponentContext { private val model: WalletModel = getOrCreateModel() @@ -148,18 +153,33 @@ internal class WalletComponent @AssistedInject constructor( var headerSize by remember { mutableStateOf(0.dp) } val dialog by dialog.subscribeAsState() - WalletScreen( - state = model.uiState.collectAsStateWithLifecycle().value, - bottomSheetContent = { - BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = { headerSize = it }, - modifier = modifier, - ) - }, - bottomSheetHeaderHeightProvider = { headerSize }, - onBottomSheetStateChange = { bottomSheetState.value = it }, - ) + if (designFeatureToggles.isRedesignEnabled) { + WalletScreen2( + state = model.uiState.collectAsStateWithLifecycle().value, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, + ) + } else { + WalletScreen( + state = model.uiState.collectAsStateWithLifecycle().value, + bottomSheetContent = { + BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = { headerSize = it }, + modifier = modifier, + ) + }, + bottomSheetHeaderHeightProvider = { headerSize }, + onBottomSheetStateChange = { bottomSheetState.value = it }, + ) + } when (val dialog = dialog.child?.instance) { is ComposableDialogComponent -> dialog.Dialog() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index ba99940e19..be88fcbccd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,6 +1,5 @@ package com.tangem.feature.wallet.presentation.common -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.event.consumedEvent @@ -186,17 +185,6 @@ internal object WalletPreviewData { ) } - val bottomSheet by lazy { - TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = WalletBottomSheetConfig.UnlockWallets( - onUnlockClick = {}, - onScanClick = {}, - ), - ) - } - val actionsBottomSheet = ActionsBottomSheetConfig( actions = listOf( TokenActionButtonConfig( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt new file mode 100644 index 0000000000..0d83d630b8 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt @@ -0,0 +1,38 @@ +package com.tangem.feature.wallet.presentation.preview + +import com.tangem.core.ui.extensions.combinedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.styledStringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM + +internal object WalletBalancePreview { + + val content: WalletBalanceUM.Content = WalletBalanceUM.Content( + id = UserWalletId("0"), + name = "My Wallet", + balance = combinedReference( + stringReference("1,234"), + styledStringReference( + ".56", + { + TangemTheme.typography2.headingRegular28.toSpanStyle() + }, + ), + stringReference(" $"), + ), + isBalanceFlickering = false, + isZeroBalance = false, + ) + + val loading: WalletBalanceUM.Loading = WalletBalanceUM.Loading( + id = UserWalletId("1"), + name = "My Wallet", + ) + + val error: WalletBalanceUM.Error = WalletBalanceUM.Error( + id = UserWalletId("2"), + name = "My Wallet", + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt deleted file mode 100644 index adfede4a1c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBottomSheetConfig.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -import androidx.annotation.DrawableRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.feature.wallet.impl.R - -/** - * Wallet bottom sheet config - * -[REDACTED_AUTHOR] - */ -sealed class WalletBottomSheetConfig( - open val title: TextReference, - open val subtitle: TextReference, - @DrawableRes open val iconResId: Int, - val primaryButtonConfig: ButtonConfig, - val secondaryButtonConfig: ButtonConfig, -) : TangemBottomSheetConfigContent { - - data class ButtonConfig( - val text: TextReference, - val onClick: () -> Unit, - @DrawableRes val iconResId: Int? = null, - ) - - data class UnlockWallets(val onUnlockClick: () -> Unit, val onScanClick: () -> Unit) : WalletBottomSheetConfig( - title = resourceReference(id = R.string.common_access_denied), - subtitle = resourceReference( - id = R.string.unlock_wallet_description_full, - formatArgs = wrappedList( - resourceReference(R.string.common_biometrics), - ), - ), - iconResId = R.drawable.ic_locked_24, - primaryButtonConfig = ButtonConfig( - text = resourceReference( - id = R.string.user_wallet_list_unlock_all_with, - formatArgs = wrappedList(resourceReference(R.string.common_biometrics)), - ), - onClick = onUnlockClick, - ), - secondaryButtonConfig = ButtonConfig( - text = resourceReference(id = R.string.welcome_unlock_card), - onClick = onScanClick, - iconResId = R.drawable.ic_tangem_24, - ), - ) -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt deleted file mode 100644 index bb5c7ca1cf..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TypedWalletStateTransformer.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.transformers - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState -import kotlin.reflect.KClass - -internal abstract class TypedWalletStateTransformer( - userWalletId: UserWalletId, - protected val targetStateClass: KClass, -) : WalletStateTransformer(userWalletId) { - - abstract fun transformTyped(prevState: S): WalletState - - @Suppress("UNCHECKED_CAST") - final override fun transform(prevState: WalletState): WalletState { - return if (prevState::class == targetStateClass) { - transformTyped(prevState as S) - } else { - prevState - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index ec373aea09..6bdd528c1b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -720,7 +720,6 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { when (bottomSheetConfig.content) { - is WalletBottomSheetConfig -> WalletBottomSheet(config = bottomSheetConfig) is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt new file mode 100644 index 0000000000..8a878d9111 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -0,0 +1,440 @@ +package com.tangem.feature.wallet.presentation.wallet.ui + +import android.content.res.Configuration +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.ExperimentalDecomposeApi +import com.tangem.core.ui.components.atoms.Hand +import com.tangem.core.ui.components.atoms.handComposableComponentHeight +import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.components.rememberIsKeyboardVisible +import com.tangem.core.ui.components.sheetscaffold.* +import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar +import com.tangem.core.ui.components.snackbar.TangemSnackbar +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.event.StateEvent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.* +import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenWithEmptyTokensState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState +import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState +import dev.chrisbanes.haze.HazeProgressive +import kotlinx.coroutines.launch + +@OptIn(ExperimentalDecomposeApi::class) +@Composable +internal fun WalletScreen2( + state: WalletScreenState, + bottomSheetContent: @Composable (() -> Unit), + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, +) { + // It means that screen is still initializing + if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return + + val walletsListState = rememberLazyListState(initialFirstVisibleItemIndex = state.selectedWalletIndex) + val snackbarHostState = remember(::SnackbarHostState) + val isAutoScroll = remember { mutableStateOf(value = false) } + + WalletContent2( + state = state, + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + isAutoScroll = isAutoScroll, + onAutoScrollReset = { isAutoScroll.value = false }, + bottomSheetContent = bottomSheetContent, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, + ) + + WalletEventEffect( + walletsListState = walletsListState, + snackbarHostState = snackbarHostState, + event = state.event, + onAutoScrollSet = { isAutoScroll.value = true }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalDecomposeApi::class) +@Suppress("LongMethod", "LongParameterList", "UnusedPrivateMember") +@Composable +private fun WalletContent2( + state: WalletScreenState, + walletsListState: LazyListState, + snackbarHostState: SnackbarHostState, + isAutoScroll: State, + onAutoScrollReset: () -> Unit, + bottomSheetHeaderHeightProvider: () -> Dp, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + bottomSheetContent: @Composable (() -> Unit), +) { + /* + * Don't pass key to remember, because it will brake scroll animation. + * selectedWalletIndex will be changed in WalletsListEffects. + */ + // val selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } + // val selectedWallet = state.wallets2.getOrElse(selectedWalletIndex) { state.wallets2[state.selectedWalletIndex] } + + val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getBottom(this).toDp() } + + val listState = rememberLazyListState() + + val partialCollapsedHeight = 64.dp + statusBarHeight + + val scaffoldContent: @Composable (PaddingValues?) -> Unit = { _ -> + + val pagerState = rememberPagerState( + initialPage = state.selectedWalletIndex, + pageCount = { state.wallets2.size }, + ) + + LaunchedEffect(pagerState.currentPage) { + if (pagerState.currentPage != state.selectedWalletIndex) { + state.onWalletChange(pagerState.currentPage, false) + } + } + } + + BaseScaffoldWithMarkets( + state = state, + listState = listState, + snackbarHostState = snackbarHostState, + bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, + onBottomSheetStateChange = onBottomSheetStateChange, + bottomSheetContent = bottomSheetContent, + content = scaffoldContent, + ) +} + +@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod", "UnusedPrivateMember") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private inline fun BaseScaffoldWithMarkets( + state: WalletScreenState, + snackbarHostState: SnackbarHostState, + listState: LazyListState, + bottomSheetHeaderHeightProvider: () -> Dp, + modifier: Modifier = Modifier, + noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, + crossinline bottomSheetContent: @Composable () -> Unit, + crossinline content: @Composable (PaddingValues) -> Unit, +) { + val bottomSheetState = rememberTangemStandardBottomSheetState() + val isPowerSaving by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsStateWithLifecycle() + + val isKeyboardVisible by rememberIsKeyboardVisible() + + val scaffoldState = rememberTangemBottomSheetScaffoldState( + bottomSheetState = bottomSheetState, + snackbarHostState = snackbarHostState, + ) + + val density = LocalDensity.current + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(density = this).toDp() } + val statusBarHeight = with(density) { WindowInsets.statusBars.getTop(density = this).toDp() } + val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + val maxHeight = LocalWindowSize.current.height + + val coroutineScope = rememberCoroutineScope() + val background = if (state.isNewMarketEnabled) { + TangemTheme.colors.background.tertiary + } else { + TangemTheme.colors.background.primary + } + + CompositionLocalProvider( + LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) }, + ) { + val backgroundColor = LocalMainBottomSheetColor.current + var isSearchFieldFocused by remember { mutableStateOf(false) } + val isNavBarVisible = remember { mutableStateOf(true) } + + BottomSheetStateEffects( + bottomSheetState = bottomSheetState, + onBottomSheetStateChange = onBottomSheetStateChange, + navigationBarVisible = isNavBarVisible, + isSearchFieldFocused = isSearchFieldFocused, + ) + + Box(modifier = modifier) { + TangemBottomSheetScaffold( + modifier = Modifier.background( + brush = Brush.verticalGradient( + listOf( + TangemTheme.colors2.surface.level1, + TangemTheme.colors2.surface.level2, + ), + ), + ), + snackbarHost = { snackbarHostState -> + WalletSnackbarHost( + snackbarHostState = snackbarHostState, + event = state.event, + modifier = Modifier + .padding(bottom = TangemTheme.dimens.spacing4) + .navigationBarsPadding(), + ) + }, + containerColor = Color.Unspecified, + sheetContainerColor = backgroundColor.value, + scaffoldState = scaffoldState, + sheetPeekHeight = peekHeight, + sheetShape = TangemTheme.shapes.bottomSheetLarge, + sheetContent = { + // hide bottom sheet when back pressed + BackHandler( + isKeyboardVisible.not() && + bottomSheetState.currentValue == TangemSheetValue.Expanded, + ) { + coroutineScope.launch { bottomSheetState.partialExpand() } + } + + Column( + modifier = Modifier + // expand bottom sheet when clicked on the header + .clickable( + enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded, + indication = null, + interactionSource = null, + ) { + coroutineScope.launch { bottomSheetState.expand() } + } + .sizeIn(maxHeight = maxHeight - statusBarHeight), + ) { + Hand(Modifier.drawBehind { drawRect(backgroundColor.value) }) + + Box( + modifier = Modifier + .onFocusChanged { + isSearchFieldFocused = it.isFocused + }, + ) { + bottomSheetContent() + } + } + }, + content = { paddingValues -> + Box { + Column( + modifier = Modifier.hazeSourceTangem(-1f), + ) { + content(paddingValues) + } + + Surface( + color = Color.Unspecified, + contentColor = Color.Unspecified, + modifier = Modifier + .hazeEffectTangem { + progressive = + HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) + }, + ) { + TangemTopBar( + title = stringReference(""), // todo balance + startIconRes = R.drawable.ic_tangem_24, + endIconRes = R.drawable.ic_more_default_24, + onEndContentClick = state.topBarConfig.onDetailsClick, + isGhostButtons = !isPowerSaving, + modifier = Modifier + .testTag(MainScreenTestTags.TOP_BAR), + ) + } + + BottomSheetScrim( + color = if (state.showMarketsOnboarding) { + Color.Black.copy(alpha = .65f) + } else { + Color.Black.copy(alpha = .40f) + }, + visible = bottomSheetState.targetValue == TangemSheetValue.Expanded || + state.showMarketsOnboarding, + onDismissRequest = { + coroutineScope.launch { bottomSheetState.partialExpand() } + state.onDismissMarketsTooltip() + }, + ) + } + }, + ) + + AnimatedVisibility( + modifier = Modifier.align(Alignment.BottomCenter), + visible = isNavBarVisible.value, + ) { + Box( + Modifier + .align(Alignment.BottomCenter) + .background(backgroundColor.value) + .height(bottomBarHeight) + .fillMaxWidth(), + ) + } + } + + LaunchedEffect(state.showMarketsOnboarding, bottomSheetState.targetValue) { + if (state.showMarketsOnboarding && bottomSheetState.targetValue == TangemSheetValue.Expanded) { + state.onDismissMarketsTooltip() + } + } + } +} + +@Composable +private fun BottomSheetScrim(color: Color, visible: Boolean, onDismissRequest: () -> Unit) { + val alpha by animateFloatAsState( + targetValue = if (visible) 1f else 0f, + animationSpec = tween(), + label = "scrim", + ) + val dismissSheet = if (visible) { + Modifier + .pointerInput(onDismissRequest) { + detectTapGestures { + onDismissRequest() + } + } + .clearAndSetSemantics {} + } else { + Modifier + } + Canvas( + Modifier + .fillMaxSize() + .then(dismissSheet), + ) { + drawRect(color = color, alpha = alpha) + } +} + +@Suppress("CyclomaticComplexMethod", "MagicNumber", "LongMethod") +@Composable +private fun BottomSheetStateEffects( + bottomSheetState: TangemSheetState, + navigationBarVisible: MutableState, + onBottomSheetStateChange: (BottomSheetState) -> Unit, + isSearchFieldFocused: Boolean, +) { + LaunchedEffect(bottomSheetState.targetValue) { + when (bottomSheetState.targetValue) { + TangemSheetValue.Hidden, + TangemSheetValue.Expanded, + -> navigationBarVisible.value = false + TangemSheetValue.PartiallyExpanded, + -> navigationBarVisible.value = true + } + } + + // expand bottom sheet when keyboard appears + val isKeyboardVisible by rememberIsKeyboardVisible() + + LaunchedEffect(isKeyboardVisible) { + if (isKeyboardVisible && isSearchFieldFocused) { + bottomSheetState.expand() + } + } + + val keyboardController = LocalSoftwareKeyboardController.current + // hide keyboard when bottom sheet is about to be hidden + LaunchedEffect(Unit) { + snapshotFlow { + bottomSheetState.currentValue == TangemSheetValue.Expanded && + bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded + }.collect { sheetHasBeenHidden -> + if (sheetHasBeenHidden) { + keyboardController?.hide() + } + } + } + + val isSheetHidden = bottomSheetState.targetValue == TangemSheetValue.PartiallyExpanded + LaunchedEffect(isSheetHidden) { + onBottomSheetStateChange( + if (isSheetHidden) { + BottomSheetState.COLLAPSED + } else { + BottomSheetState.EXPANDED + }, + ) + } +} + +@Composable +private fun WalletSnackbarHost( + snackbarHostState: SnackbarHostState, + event: StateEvent, + modifier: Modifier = Modifier, +) { + SnackbarHost(hostState = snackbarHostState, modifier = modifier) { data -> + if (event is StateEvent.Triggered && event.data is WalletEvent.CopyAddress) { + CopiedTextSnackbar(data) + } else { + TangemSnackbar(data) + } + } +} + +// region Preview +@OptIn(ExperimentalDecomposeApi::class) +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun WalletScreen2_Preview(@PreviewParameter(WalletScreen2PreviewProvider::class) data: WalletScreenState) { + TangemThemePreviewRedesign { + WalletScreen2( + state = data, + bottomSheetContent = { + Text("Markets Content") + }, + bottomSheetHeaderHeightProvider = { 10.dp }, + onBottomSheetStateChange = {}, + ) + } +} + +private class WalletScreen2PreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + walletScreenState, + walletScreenState.copy(selectedWalletIndex = 1), + accountScreenState.copy(selectedWalletIndex = 1), + accountScreenWithEmptyTokensState.copy(selectedWalletIndex = 1), + ) +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt deleted file mode 100644 index ff2d45177d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBottomSheet.kt +++ /dev/null @@ -1,141 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components.common - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.PrimaryButtonIconStart -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SecondaryButtonIconStart -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBottomSheetConfig - -/** - * Wallet bottom sheet with detail notification information - * - * @param config component config - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun WalletBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config) { content: WalletBottomSheetConfig -> - BottomSheetContent(config = content) - } -} - -@Composable -private fun BottomSheetContent(config: WalletBottomSheetConfig) { - Column( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .padding(top = TangemTheme.dimens.spacing40, bottom = TangemTheme.dimens.spacing16), - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing40), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Icon( - painter = painterResource(id = config.iconResId), - contentDescription = null, - modifier = Modifier.size(size = TangemTheme.dimens.size48), - tint = when (config) { - is WalletBottomSheetConfig.UnlockWallets -> TangemTheme.colors.icon.primary1 - }, - ) - - Column( - verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing16), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = config.title.resolveReference(), - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.Center, - style = TangemTheme.typography.h2, - ) - - Text( - text = config.subtitle.resolveReference(), - color = TangemTheme.colors.text.secondary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.body2, - ) - } - - Column(verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing10)) { - val buttonModifier = Modifier.fillMaxWidth() - - PrimaryButton(config = config.primaryButtonConfig, modifier = buttonModifier) - - SecondaryButton(config = config.secondaryButtonConfig, modifier = buttonModifier) - } - } -} - -@Composable -private fun PrimaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) { - if (config.iconResId == null) { - PrimaryButton( - text = config.text.resolveReference(), - onClick = config.onClick, - modifier = modifier, - ) - } else { - PrimaryButtonIconStart( - text = config.text.resolveReference(), - iconResId = config.iconResId, - onClick = config.onClick, - modifier = modifier, - ) - } -} - -@Composable -private fun SecondaryButton(config: WalletBottomSheetConfig.ButtonConfig, modifier: Modifier = Modifier) { - if (config.iconResId == null) { - SecondaryButton( - text = config.text.resolveReference(), - onClick = config.onClick, - modifier = modifier, - ) - } else { - SecondaryButtonIconStart( - text = config.text.resolveReference(), - iconResId = config.iconResId, - onClick = config.onClick, - modifier = modifier, - ) - } -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun WalletBottomSheetContent_Preview( - @PreviewParameter(WalletBottomSheetConfigProvider::class) - config: WalletBottomSheetConfig, -) { - TangemThemePreview { - // Use preview of content because ModalBottomSheet isn't supported in Preview mode - BottomSheetContent(config = config) - } -} - -private class WalletBottomSheetConfigProvider : CollectionPreviewParameterProvider( - collection = listOf(WalletPreviewData.bottomSheet.content as WalletBottomSheetConfig), -) -// endregion \ No newline at end of file From 3c866e122b613c419a384fa3c37fc1d0907be65e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 19:56:50 +0500 Subject: [PATCH 26/97] Updated on 2026-08-14 --- .../kotlin/com/tangem/tests/SendTest.kt | 28 +++++++++---------- .../tests/send/warnings/KusamaWarningsTest.kt | 2 +- .../send/warnings/PolkadotWarningsTest.kt | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt index 9bf7545be1..105eac0ecc 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/SendTest.kt @@ -1,9 +1,7 @@ package com.tangem.tests import com.tangem.common.BaseTestCase -import com.tangem.common.extensions.SwipeDirection import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.extensions.swipeVertical import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.setWireMockScenarioState import com.tangem.scenarios.openMainScreen @@ -22,30 +20,32 @@ class SendTest : BaseTestCase() { @DisplayName("Send: check fee notification") @Test fun checkFeeNotificationTest() { - val currencyName = "POL (ex-MATIC)" - val feeCurrencyName = "Ethereum" - val feeCurrencySymbol = "ETH" - val scenarioName = "eth_network_balance" - val scenarioState = "Empty" + val currencyName = "USDC" + val feeCurrencyName = "Solana" + val feeCurrencySymbol = "SOL" + val balanceScenarioName = "solana_balance" + val tokensScenarioName = "user_tokens_api" + val balanceState = "Empty" + val tokensState = "SolanaUSDC" setupHooks( additionalAfterSection = { - resetWireMockScenarioState(scenarioName) + resetWireMockScenarioState(balanceScenarioName) + resetWireMockScenarioState(tokensScenarioName) } ).run { - step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { - setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + step("Set WireMock scenario: '$tokensScenarioName' to state: '$tokensState'") { + setWireMockScenarioState(scenarioName = tokensScenarioName, state = tokensState) + } + step("Set WireMock scenario: '$balanceScenarioName' to state: '$balanceState'") { + setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceState) } - step("Open 'Main Screen'") { openMainScreen() } step("Synchronize addresses") { synchronizeAddresses() } - step("Swipe up") { - swipeVertical(SwipeDirection.UP) - } step("Click on token with name: $currencyName") { onMainScreen { tokenWithTitleAndAddress(currencyName).clickWithAssertion() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt index c545a4100b..5105441296 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/KusamaWarningsTest.kt @@ -23,7 +23,7 @@ class KusamaWarningsTest : BaseTestCase() { private val tokenName = "Kusama" private val amountToLeaveLessThanDeposit = "0.300333" private val amountToLeaveGreaterThanDeposit = "0.1" - private val depositAmount = "KSM 0.000333333333" + private val depositAmount = "KSM 0.000003333" private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title) private val warningMessage = getResourceString( diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt index be40b64a78..31cf64adce 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/PolkadotWarningsTest.kt @@ -23,7 +23,7 @@ class PolkadotWarningsTest : BaseTestCase() { private val tokenName = "Polkadot" private val amountToLeaveLessThanDeposit = "1.299" private val amountToLeaveGreaterThanDeposit = "0.2" - private val depositAmount = "DOT 1.00" + private val depositAmount = "DOT 0.01" private val warningTitle = getResourceString(R.string.send_notification_existential_deposit_title) private val warningMessage = getResourceString( From 25c81596f939fe80965a99e0cc5e6fa7ff7f7d22 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 12:56:59 +0100 Subject: [PATCH 27/97] Updated on 2026-08-14 --- .../kotlin/com/tangem/features/feed/model/earn/EarnModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index 6d74dd4195..c9385e437b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -116,7 +116,6 @@ internal class EarnModel @Inject constructor( ) { items, error, paginationStatus -> val hasActiveFilters = state.value.earnFilterUM.selectedTypeFilter != EarnFilterTypeUM.All || state.value.earnFilterUM.selectedNetworkFilter !is EarnFilterNetworkUM.AllNetworks - error?.let(::handleBestOpportunitiesErrorAnalytics) EarnListStateManager.calculateState( items = items, error = error, @@ -128,9 +127,10 @@ internal class EarnModel @Inject constructor( }, onLoadMore = { batchFlowManager.loadMore() }, onClearFiltersClick = ::onClearFiltersClick, - ) - }.onEach { bestOpportunitiesState -> + ) to error + }.onEach { (bestOpportunitiesState, error) -> stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) + error?.let(::handleBestOpportunitiesErrorAnalytics) }.launchIn(modelScope) } From 7cb01dd6d8f4a25fc3ebc18d7bf14aeaa12006fd Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 15:59:06 +0100 Subject: [PATCH 28/97] Updated on 2026-08-14 --- .../features/feed/ui/feed/components/NewsBlock.kt | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt index 0acec46dcd..ff2921118b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt @@ -8,8 +8,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState 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 import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -66,12 +64,6 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend @Composable private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { val listState = rememberLazyListState() - val articlesReadStatus = remember(news.content) { - news.content.map { it.isViewed } - } - LaunchedEffect(articlesReadStatus) { - listState.requestScrollToItem(0) - } Column { Header( title = { @@ -138,7 +130,7 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, ) { itemsIndexed( items = news.content, - key = { _, article -> article.id }, + key = { index, _ -> index }, contentType = { _, _ -> "article" }, ) { index, article -> val articleModifier = if (index == FOURTH_ITEM_INDEX) { From d808fbe88108b3fd55f837845846914dd7286d9e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 19 Feb 2026 20:33:29 +0500 Subject: [PATCH 29/97] Updated on 2026-08-14 --- .../core/ui/ds/row/TangemRowContainer.kt | 4 +- .../img_nft_empty_collection.webp | Bin 0 -> 2684 bytes .../drawable/ic_chevron_small_right_24.xml | 12 + .../drawable/img_nft_empty_collection.webp | Bin 0 -> 3910 bytes .../RemoveNFTCollectionsTransformer.kt | 7 +- .../SetNFTCollectionsTransformer.kt | 11 +- .../wallet/ui/components/WalletNFTItem2.kt | 422 ++++++++++++++++++ 7 files changed, 452 insertions(+), 4 deletions(-) create mode 100644 core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp create mode 100644 core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml create mode 100644 core/ui/src/main/res/drawable/img_nft_empty_collection.webp create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt index b1a24c2067..553edfc0e9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt @@ -16,7 +16,7 @@ import kotlin.math.max /** * A custom layout composable that arranges its children in a row with specific layout IDs. */ -internal enum class TangemRowLayoutId { +enum class TangemRowLayoutId { HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP } @@ -29,7 +29,7 @@ internal enum class TangemRowLayoutId { */ @Suppress("LongMethod") @Composable -internal fun TangemRowContainer( +fun TangemRowContainer( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3), content: @Composable () -> Unit, diff --git a/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp b/core/ui/src/main/res/drawable-night/img_nft_empty_collection.webp new file mode 100644 index 0000000000000000000000000000000000000000..8844ed902d756d8d62bf3c5c8503108d36f386ee GIT binary patch literal 2684 zcmV-?3WN1hNk&F=3IG6CMM6+kP&il$0000G0001=005r=06|PpNKOX;00A6-hHrpl$fp9_y1VIP`he6CBVNfWAAP9ya1~!9b{@AvCKjp9g5itSj zhyVZg%$s@paCdyMRPob)<#0RS0H(aTz1<&aySJ*SVwVnnfeNZMpO$~VMO!52P+6-j@2|g+$D89I z=}^HElqH{j8?Rq=Kp|?B<(xhE^qcq;DH0*&`Ot~*`dj$KK$!&<(pAtw`S$n?yh1Wd z&M64Vvzc$dzWh#+gib-)L<&*PpTql)7?vl?%=w^D_&No5uE=e*1>NL>cd_Ma$uW^x z$!$TMn&a&h+EJA?NK2WWB2NavgUEiX4T6M-G;X7-9J55WRWmz8Lq{RAk`8TrI|Wu$ zA#2Dn<=K+TG0{1$tb&%zqD3PjMHyxY>BG2gxtfAp7BZ7H=nx`2#&zfq^Q)Rx=^5h78e3Vj>bHBDPo14$b>Co_wQOkt^DrXbR| z4Jkqw5yTu9s2Y2Mpo7e0ZA%r(CN3M~x>DtsQcg3Wthd9et{##gYs(r$5tIz%EUX1I zp`|D@sZ)eR8pys2>xJU%B$T!6m3(M5F2Xv3Mo3U{a)HuE0RA4<-u~DllH@|xIuwG$ z64p7QXe*1T8zhLLk#h}e>t}}(a-WYp;Z;~OO-W>p=$Q`X4XQML9n%c6zz5Ws|ZA-Sumdg;)m?eqiG78b# z;}>v&N*n~C>O_vAtRYz&AtLGDpKoVp4Joph8c`z}MW~QQkv+(l$IrGEXd$xJ_`obg zh8hxAGrT{3vfJ7#C5Do2^D=J&ZG58+o=OM6?`}%*@Q7 zoI9m`J5DMpG*Us>Q zq>;PAVw$N3^aXPqL7d^Ee_}G2{lCaq@qyt zYM}I)kO~2fTAJ|DyrR3P@|R5AWAnO z!e+db&a2ph*abc-GeKk@2qMzf!4zUXRuplqB^rs?gb*5PL6sm-ml#s|ZCeynSZmWz zR5Huqm_2)ryoe&Dly4u(@)oVji(^HwXTLF$G*Sy{bUF{@3;(p~P~%u42-({*Q;Ty{ z2qMW^+a8&x*_NU~4r-$)Wv`J=dqIS(kpR3-LEoZB+n!lME(_7PMN3qB*Hff!qzEmv zoZHlks9uQLf+UejimXvV7kkRXsEYlRwHO5DTrOK$BF<6OPD%uo%b;y-LqUK}aY3^u zq%CA`3oRKTx}><;LOCB~mf4UsL5?LfB&Y+SrKM0ZgP@V@!YT-cgwPTSleQc(*E8hd7WHnJzhDYC6? zk~L389Xh0qQcDWGhy-OVlH8ZZjHt}#vCW-*q_H=ODrd5tdR_m;ME%HHB;-MqcT|9Dqdp|GV$Qi5d0<2Uf~c=v$?iNjhW zqww#`Z!s?opM&_B*s>z7qf0uKdsP%sZ21xeaTOJ<`DCPAu#2du(y#&wf+(U|b2_DS zX}~WmX_`p9UCTnT)93yhssX&tm1#n@!bDa{F2oT{^$8e z%@-TL;5|gYq5D<$2kpnO*Yl6^ztlf<|Fm)$k4F7AG{67LA*{>|+5t6%nJN`&MU9P@lke9th2ghc$q>YK!Zr)tchmgo_F1)}?r3^COf01w-ls z^xNC1j(1<`2@>eR3&0Ypc5LD5Ta}b|8#SK;-59emn^!)?fSoAV$vef%Bf5lMDD}@KASVIs0;4Q6<^TKX3 z@F}@a>+&%WW>){E#*h9)&C(CGL(1PS2!!|D7i2KP@|s) z=j5o0aDG3Ig8ZOmME34{tg#kDkBwc5+`8>Ms%FHPKg;@xwiarC{hRcdUPwppiG5CZ zr{}!^G+0(jP2ZcpxI#QL{Iyc9|0y~~k>#2?>HHmp&6H8w15f|kOE81&p<96Hgsk@?u6LHxl;psIeszC-hBdJXH*(D*r(uH&(8Y07uS$t9Ss^q}$ zys>2*i(7f^QL(Rgq>oozsyf&lNeO6=%)hZ<=$DKkmDbDfQI{nFfn8Q~XzGApK8xH3 z%;VY40e}LTIAc@4KQcY|AYW?%sktxUnH^x8kqq)mSn~d&X_kC!h3B~dR|XFlqg-;# qn*3B4VMQ5M=f%N_&?V3AwTNj0002#qX2FI literal 0 HcmV?d00001 diff --git a/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml b/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml new file mode 100644 index 0000000000..6ee9686437 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_chevron_small_right_24.xml @@ -0,0 +1,12 @@ + + + diff --git a/core/ui/src/main/res/drawable/img_nft_empty_collection.webp b/core/ui/src/main/res/drawable/img_nft_empty_collection.webp new file mode 100644 index 0000000000000000000000000000000000000000..9d18888c548da0ff01bb1ed8177827c92243b753 GIT binary patch literal 3910 zcmV-M54rGCNk&FK4*&pHMM6+kP&il$0000G0001=005r=06|PpNIMGv009{VZQDq4 zxBK}cJ`xfA4-bimw-U~WBM?a1Uwep%2%;WSAW4#~TIq&(xVyXUe^Jbx?+3T8$CQW( zU?547p;`{kTHFgP&9<$)k#x^jwm#dkH+GdD9c)2MsI@Sm7*#td_2red_6BtuFD z-3YyN&if+D>S;#=MA=rVP?Uv)csOX?#~-NtdEwuGTFKMr{9+(w5^VZX0cwhodt+ZIKsIthW zelv64^7{EO>j?BH4M}D%Xh?Py2tR*%CfZBPVwU1qB6tn?p;(7Fb`Ri-;JY zCBxQYfn&*mBMbM|f5u;%=!FHc873fh*@|M7`?Svpe~ah!%VqggHXv9}v)qhBc~-6C z-URylb~zAS20enl8b0cV-5qn>qyoylff-+Ce#!j0kOvi}B(T zySTp(i30`z7>x+J9?V;sxW*{wgk9)Hj%y=x)RS?be@>8x->d8u4fMAy;Q=AmAqSrW1Ia6oYsw9|j%z%U>cz$q-e62;@0fb_3jW0?M`FI1L!d z1K&49WcnL2j=bX@q+Ub;grQ`jblmVuk)}`?P6-v8g=L#_Zz9uNN?iGT74L}n^f>u# zVbr?9&GthwZOkG;N!HbIFqL2ZDk7zq;Ri@07;$&Ov-g!{9vv@E`vjjw9q*l8nbw zK_tz>71E4PzOx^<_J!{oxG#uBFx-WC0ax zYh!(LeT1|$VLuFREW3lpEv!wCl@iaFF$2Sm&CQJ=y#ZZQY?H;s=5TTphuQUn7+k+L zSQ}Ids369F`r|0l4MJokfy2e)r>{GCYGJO~KXNCGv@JNkT%#S_au>_++cGf7+M&g$ z?C@SYSUY$9$z#2vRl=lxlU$}IriKLcXSe z6b=rK%!f5gGGeE4H9_M*kyhT@PNQxCNeCrg>D4Y*QP~Ld$Il-dkB?9XEYU;owVnXe2ug9X{t{nqyoYMjn%7AusLn4fnBYpuH!2P2E@`BRB|Bu zVGwITlPpH+iG736__0W&Ce6QRZtrqBb`q$FEZPv{Vvv-Ym`oeVj3qWBgL)DUnSuxg z-^DQtZEp(+3}#afexJKcY&+XJOoP~5xZc2bz+6a}y1 zEkq)#j*Ks95$&%}s>l@pD74AAS-JRMNt9C1y1-_M+(`33Nt3hzms9)J6;u#;6a{W6 z5;Q!%*Jd^;DIj1(AY?pV5);j`XmIX7k+jgv*lgI&mh{Nl-aqf}XvzkuAFiHS^a&vV z!$PyUO#}pluue0TWaH!?@0r&OxMxvFQsYXF!HY?uVD|}!N1!l&u9#*MBv{<_d`gLP z1=3_$e?L_gw-^xnj~-gm^ye?mf#DQ+yYG_W4f58JL`4uh8i{Iy_iG*>Ig4;nBf;L~ zQTlJ|QPLU2xz92f4>H(pz$m!pu^or7%A90S?6pYGKEK}NQu1$1l^}QWmCaWx$75Ft zP6R9&b-jpLS2*7C(qu7g__XMitIuS6Fr>*NQ@t@o5$d9{K?ro|%m2EBD2C+4;4=dv zwIhFlQEWR;=vUdsm6MZ5h5HEb^U`uLQ&_gUjLX#~8zI(>O;Fer*Vka+07 z`dCSzNhCO17*P!BPpNKRJQT=Tv9hUZmJR`gJMZcX1+>;(UcBUlEY9@mrL9axW?nXo z14yP$fOGd<22kFAwjY|hF>8t-`(!d1jYlK#FNK@{hCZSR!3IUfNfEJh_SVyU+G!96 zv43zlIXFBxq(694)^=QlLMcL(ovBO;46Rg$4UrVy+{$%~JfO-q+z{YJliH*{UX%uz z*WyV=Q@ED9Pnrx}zS*Q=Aq-#zmJGpS2rC^_?puY4CmEWXuMs>3n0OF-;R?@R&!}-W4`6UhBNOtF>cm>PD$J3nc8Yp35;+G3{Vnoig?L2m2ycW`aHNt458sqXk{jW z)B)z^^CfqQr&)JgVPZjelPI^qtlsCcM=B8=hH9h+UEoNEz0yQ*SU=R45?wfA$)hi) z@5rvTvS_kGM}jQ`1JnkDU36SXsji4@v?UOYlBGk@d$Z8Zx=3is4%1k+TwEBqE7#b> zE$2J|(^iAw_iWU>MP2TIGk5<|X!4bERVIKLuecOII~ICq?^`hI#<5T^@B_yJObmwy zGa%94--Z|~t0)sRPRI!O@q#AX;@yU-dD{IghpDLU`-cb7ig?db1Sa5y><~pzRrW=} zgzZ1Xj2t)IV9Vvb2mrD(-0@#Vvm(*nzKZNNZ-Scs{34i;+GgljyFgN3O)IA9MYs#s1P9l`9)Ap^`LwdOiME91CrKu()KWi8$D}(AlHaH`-EvzK6tk zv~y)A{?Fba09H^qAlw500B{)qodGJK0H6Rq83=+wAqCgG1OQp({<*-ulz04*AjO$TV-il+6S{vfB^pgU>E@-|ME}A@-h%3y*Tdk#sMR`WP(f2-G_Y8 zygkKl&RAQ10dCM~Cg7EpB>{CW|L(6ZI(q)0<|>W?vE+HUdY)Q**H3?mXamzW)J;0* zu)EL!f$;Nc%a4HS10I4_%gl&5YfQKm0oD-(wl_YIKy-W0Q}8h8vVW058si-FevG#p zsDK=)OJRrDEFQT>^slX{_(CiEhzl{anMfG+1J?-&$#L1?)$mvUEh{e;pnY`x<=Ppl z2cWBk^rRFLpVo|~7%FnL!`aTm0Jvzori54a5Rq>*Nj9#(ArlWZA)aNfym*?DqdQX; zo0IkB1wUV^$E#V9v*TYJZ}nz^xTtf%AFTXzNb)#eiyiWChwkaSSXf_AosT{P@G1px zRO+}g7D*du+qAdtC|;YoveBAy-$b>j8~EHfPgj=>4jF!+M0{P95i!}sHfAR>aqJdN ztG3Z|bk7R0dfv<1?#wo~zL~lHk7;}kqNVx4DDXFOto~bD{h%*?p&sL#pP1C{67rvS zEBDNgQvTu$zRaGxc>m`-rQ0IbbSNC2C6KB>49j}`hXkszPdOgzSy z55*E0$;g^KVxAOl%SjHh)0Y4E`S6b%jm;F@k`u3PnffQvaJR$Wx0uev7z~+`a$My+ zx%+=f>m`E1KJt;%V}YnJW1)qDlvx}`q-Qu6LAO$TJR02jWr-EPHU+`=NJ(7~ngbF+ zlpILPE1SJMw&9OuO7hd;={khnJmY0>#5e7= zE`Mo6^$0x}BI@+vW++re)I42)hfcW9G0|a2modhNq)x9Bm zJEfNMFXfpN walletUM.copy( + nftState = WalletNFTItemUM.Hidden, + ) + is WalletUM.Locked -> walletUM + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt index 10a816e576..caa9d7ec25 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetNFTCollectionsTransformer.kt @@ -30,7 +30,16 @@ internal class SetNFTCollectionsTransformer( } override fun transform(walletUM: WalletUM): WalletUM { - return walletUM // todo redesign main + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + nftState = when { + nftCollections.allLoadedCollectionsEmpty() -> + WalletNFTItemUM.Empty(onItemClick) + else -> createContentNFTItemUM(onItemClick) + }, + ) + is WalletUM.Locked -> walletUM + } } private fun createContentNFTItemUM(onItemClick: () -> Unit): WalletNFTItemUM.Content { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt new file mode 100644 index 0000000000..8759ecdf11 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletNFTItem2.kt @@ -0,0 +1,422 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import coil.compose.SubcomposeAsyncImage +import com.tangem.core.ui.R +import com.tangem.core.ui.components.RectangleShimmer +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM.Content.CollectionPreview +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun WalletNFTItem2(state: WalletNFTItemUM, modifier: Modifier = Modifier) { + val nftModifier = modifier + .clip(RoundedCornerShape(18.dp)) + .background(TangemTheme.colors2.surface.level3) + when (state) { + is WalletNFTItemUM.Hidden -> Unit + is WalletNFTItemUM.Empty -> WalletNFTItemEmpty( + modifier = nftModifier, + onClick = state.onItemClick, + ) + is WalletNFTItemUM.Failed -> WalletNFTItemFailed(modifier = nftModifier) + is WalletNFTItemUM.Loading -> WalletNFTItemLoading(modifier = nftModifier) + + is WalletNFTItemUM.Content -> WalletNFTItemContent( + state = state, + onClick = state.onItemClick, + modifier = nftModifier, + ) + } +} + +@Composable +private fun WalletNFTItemEmpty(onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier.clickableSingle( + onClick = onClick, + ), + ) { + Image( + painter = painterResource(R.drawable.img_nft_empty_collection), + contentDescription = null, + modifier = Modifier + .size(TangemTheme.dimens2.x10) + .layoutId(TangemRowLayoutId.HEAD), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_receive_nft), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2), + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + modifier = Modifier.layoutId(TangemRowLayoutId.TAIL), + ) + } +} + +@Composable +private fun WalletNFTItemContent(state: WalletNFTItemUM.Content, onClick: () -> Unit, modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier.clickableSingle( + onClick = onClick, + ), + ) { + Box(modifier = Modifier.layoutId(TangemRowLayoutId.HEAD)) { + CollectionsPreviews( + previews = state.previews, + ) + } + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16.applyBladeBrush( + isEnabled = state.isFlickering, + textColor = TangemTheme.colors2.text.neutral.primary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(horizontal = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe( + id = R.string.nft_wallet_count, + state.allAssetsCount, + state.collectionsCount, + ), + style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( + isEnabled = state.isFlickering, + textColor = TangemTheme.colors2.text.neutral.secondary, + ), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(horizontal = TangemTheme.dimens2.x2), + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_chevron_small_right_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + modifier = Modifier.layoutId(TangemRowLayoutId.TAIL), + ) + } +} + +@Composable +private fun WalletNFTItemFailed(modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier, + ) { + Box( + modifier = Modifier + .layoutId(TangemRowLayoutId.HEAD) + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)) + .background(TangemTheme.colors2.skeleton.backgroundPrimary), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x5), + painter = painterResource(R.drawable.ic_error_sync_24), + tint = TangemTheme.colors.icon.informative, + contentDescription = null, + ) + } + Text( + text = stringResourceSafe(R.string.nft_wallet_title), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2), + ) + Text( + text = stringResourceSafe(R.string.nft_wallet_unable_to_load), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2), + ) + } +} + +@Composable +private fun WalletNFTItemLoading(modifier: Modifier = Modifier) { + TangemRowContainer( + modifier = modifier, + ) { + Box( + modifier = Modifier + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)) + .background(TangemTheme.colors2.skeleton.backgroundPrimary) + .layoutId(TangemRowLayoutId.HEAD), + ) + TextShimmer( + style = TangemTheme.typography2.bodySemibold16, + radius = TangemTheme.dimens2.x25, + + modifier = Modifier + .layoutId(TangemRowLayoutId.START_TOP) + .padding(start = TangemTheme.dimens2.x2) + .width(TangemTheme.dimens.size110), + ) + TextShimmer( + style = TangemTheme.typography2.captionSemibold12, + radius = TangemTheme.dimens2.x25, + modifier = Modifier + .layoutId(TangemRowLayoutId.START_BOTTOM) + .padding(start = TangemTheme.dimens2.x2) + .width(TangemTheme.dimens.size80), + ) + } +} + +@Composable +@Suppress("MagicNumber", "ReusedModifierInstance") +private fun BoxScope.CollectionsPreviews(previews: ImmutableList, modifier: Modifier = Modifier) { + val modifiers = when (previews.size) { + 1 -> previews1Modifiers() + 2 -> previews2Modifiers() + 3 -> previews3Modifiers() + else -> previews4Modifiers() + } + Box( + modifier = modifier + .size(TangemTheme.dimens2.x10), + ) { + previews.take(modifiers.size).forEachIndexed { index, s -> + val previewModifier = modifiers[index] + when (s) { + is CollectionPreview.Image -> { + SubcomposeAsyncImage( + modifier = previewModifier, + model = s.url, + loading = { + RectangleShimmer() + }, + error = { + Box( + modifier = previewModifier.background(TangemTheme.colors2.surface.level2), + ) + }, + contentDescription = null, + ) + } + is CollectionPreview.More -> { + Icon( + modifier = previewModifier + .background(TangemTheme.colors2.surface.level2), + imageVector = ImageVector.vectorResource(R.drawable.ic_nft_preview_more_16), + tint = TangemTheme.colors2.text.neutral.secondary, + contentDescription = null, + ) + } + } + } + } +} + +@Composable +private fun previews1Modifiers(): List = listOf( + Modifier + .size(TangemTheme.dimens2.x10) + .clip(RoundedCornerShape(TangemTheme.dimens2.x3)), +) + +@Composable +private fun BoxScope.previews2Modifiers(): List = listOf( + Modifier + .padding(start = TangemTheme.dimens2.x0_5, top = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x6) + .clip(RoundedCornerShape(TangemTheme.dimens2.x2)) + .align(Alignment.TopStart), + Modifier + .zIndex(1f) + .padding(TangemTheme.dimens2.x0_5) + .clip(RoundedCornerShape(topStart = 10.dp)) + .background(TangemTheme.colors2.surface.level3) + .padding(start = TangemTheme.dimens2.x0_5, top = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x6) + .clip(RoundedCornerShape(TangemTheme.dimens2.x2)) + .align(Alignment.BottomEnd), +) + +@Composable +private fun BoxScope.previews3Modifiers(): List = listOf( + Modifier + .padding(start = 3.dp, top = 3.dp) + .size(17.8.dp) + .clip(RoundedCornerShape(6.dp)) + .align(Alignment.TopStart), + Modifier + .zIndex(1f) + .padding(top = 10.dp, end = 1.dp) + .clip(RoundedCornerShape(8.dp)) + .background(TangemTheme.colors2.surface.level3) + .padding(TangemTheme.dimens2.x0_5) + .size(18.dp) + .clip(RoundedCornerShape(6.dp)) + .align(Alignment.TopEnd), + Modifier + .padding(start = 9.dp, top = 2.dp) + .size(14.dp) + .clip(RoundedCornerShape(4.dp)) + .align(Alignment.BottomStart), +) + +@Composable +private fun BoxScope.previews4Modifiers(): List = listOf( + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.TopStart), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.TopEnd), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.BottomStart), + Modifier + .clip(RoundedCornerShape(6.dp)) + .size(18.dp) + .align(Alignment.BottomEnd), +) + +@Preview(widthDp = 360) +@Preview(widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_WalletNFTItem(@PreviewParameter(WalletNFTItemProvider2::class) state: WalletNFTItemUM) { + TangemThemePreviewRedesign { + WalletNFTItem2( + state = state, + modifier = Modifier + .background(TangemTheme.colors2.surface.level1), + ) + } +} + +private class WalletNFTItemProvider2 : CollectionPreviewParameterProvider( + collection = listOf( + WalletNFTItemUM.Empty( + onItemClick = { }, + ), + WalletNFTItemUM.Loading, + WalletNFTItemUM.Failed, + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = true, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + CollectionPreview.Image("img4"), + ), + allAssetsCount = 125, + collectionsCount = 11, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + WalletNFTItemUM.Content( + previews = persistentListOf( + CollectionPreview.Image("img1"), + CollectionPreview.Image("img2"), + CollectionPreview.Image("img3"), + CollectionPreview.More, + ), + allAssetsCount = 125, + collectionsCount = 11, + isFlickering = true, + noCollectionAssetsCount = 0, + onItemClick = { }, + ), + ), +) \ No newline at end of file From dea7034aadc42ddd78fc2e6ebb826013f5d40c20 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 11:56:20 +0500 Subject: [PATCH 30/97] Updated on 2026-08-14 --- .../alerts/TransactionErrorAlertConverter.kt | 56 ------ .../alerts/TransactionErrorDialogFactory.kt | 83 ++++++++ .../ui/alerts/models/AlertDemoModeUM.kt | 13 -- .../alerts/models/AlertTransactionErrorUM.kt | 21 -- .../tangem/common/ui/alerts/models/AlertUM.kt | 12 -- .../v2/common/ui/OnboardingDialogUM.kt | 11 +- .../selecttoken/model/OnrampOperationModel.kt | 16 +- .../send/v2/common/SendConfirmAlertFactory.kt | 32 +--- .../impl/presentation/model/StakingModel.kt | 19 +- .../state/StakingStateController.kt | 14 -- .../impl/presentation/state/StakingUiState.kt | 3 - .../state/events/StakingAlertUM.kt | 121 ++++++------ .../presentation/state/events/StakingEvent.kt | 13 -- .../state/events/StakingEventFactory.kt | 44 ++--- .../presentation/ui/StakingEventEffect.kt | 80 -------- .../impl/presentation/ui/StakingScreen.kt | 7 - .../swap/v2/impl/common/SwapAlertFactory.kt | 33 +--- .../SwapTransactionErrorStateConverter.kt | 19 +- .../tangem/feature/swap/model/SwapModel.kt | 179 +++++++++++------- .../tangem/feature/swap/models/SwapAlertUM.kt | 61 +++--- .../feature/swap/models/SwapStateHolder.kt | 4 - .../swap/models/states/events/SwapEvent.kt | 9 - .../tangem/feature/swap/ui/StateBuilder.kt | 118 ------------ .../tangem/feature/swap/ui/SwapEventEffect.kt | 61 ------ .../feature/swap/ui/SwapScreenContent.kt | 4 - .../impl/common/YieldSupplyAlertFactory.kt | 32 +--- 26 files changed, 349 insertions(+), 716 deletions(-) delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt create mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt delete mode 100644 common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt delete mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt delete mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt delete mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt deleted file mode 100644 index 89855cf6ce..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorAlertConverter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.common.ui.alerts - -import com.tangem.common.ui.alerts.models.AlertDemoModeUM -import com.tangem.common.ui.alerts.models.AlertTransactionErrorUM -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.utils.converter.Converter - -class TransactionErrorAlertConverter( - private val popBackStack: () -> Unit, - private val onFailedTxEmailClick: (String) -> Unit, -) : Converter { - override fun convert(value: SendTransactionError): AlertUM? { - return when (value) { - is SendTransactionError.DemoCardError -> AlertDemoModeUM( - onConfirmClick = popBackStack, - ) - is SendTransactionError.TangemSdkError -> AlertTransactionErrorUM( - code = value.code.toString(), - cause = null, - causeTextReference = resourceReference(value.messageRes, wrappedList(value.args)), - onConfirmClick = { onFailedTxEmailClick(value.code.toString()) }, - ) - is SendTransactionError.BlockchainSdkError -> AlertTransactionErrorUM( - code = value.code.toString(), - cause = value.message, - onConfirmClick = { onFailedTxEmailClick("${value.code}: ${value.message.orEmpty()}") }, - ) - is SendTransactionError.DataError -> AlertTransactionErrorUM( - code = "", - cause = value.message, - onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, - ) - is SendTransactionError.NetworkError -> AlertTransactionErrorUM( - code = value.code.orEmpty(), - cause = value.message.orEmpty(), - onConfirmClick = { onFailedTxEmailClick(value.message.orEmpty()) }, - ) - is SendTransactionError.UnknownError -> AlertTransactionErrorUM( - code = "", - cause = value.ex?.localizedMessage, - onConfirmClick = { onFailedTxEmailClick(value.ex?.localizedMessage.orEmpty()) }, - ) - is SendTransactionError.CreateAccountUnderfunded -> AlertTransactionErrorUM( - code = "", - cause = null, - causeTextReference = resourceReference(R.string.no_account_polkadot, wrappedList(value.amount)), - onConfirmClick = popBackStack, - ) - else -> null - } - } -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt new file mode 100644 index 0000000000..c5476fa263 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/alerts/TransactionErrorDialogFactory.kt @@ -0,0 +1,83 @@ +package com.tangem.common.ui.alerts + +import com.tangem.common.ui.R +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.domain.transaction.error.SendTransactionError +import javax.inject.Inject + +class TransactionErrorDialogFactory @Inject constructor() { + + fun create( + error: SendTransactionError, + popBackStack: () -> Unit, + onFailedTxEmailClick: (String) -> Unit, + ): DialogMessage? { + return when (error) { + is SendTransactionError.DemoCardError -> demoModeDialog(popBackStack) + is SendTransactionError.TangemSdkError -> transactionErrorDialog( + causeTextReference = resourceReference(error.messageRes, wrappedList(error.args)), + code = error.code.toString(), + onConfirmClick = { onFailedTxEmailClick(error.code.toString()) }, + ) + is SendTransactionError.BlockchainSdkError -> transactionErrorDialog( + cause = error.message, + code = error.code.toString(), + onConfirmClick = { onFailedTxEmailClick("${error.code}: ${error.message.orEmpty()}") }, + ) + is SendTransactionError.DataError -> transactionErrorDialog( + cause = error.message, + code = "", + onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) }, + ) + is SendTransactionError.NetworkError -> transactionErrorDialog( + cause = error.message.orEmpty(), + code = error.code.orEmpty(), + onConfirmClick = { onFailedTxEmailClick(error.message.orEmpty()) }, + ) + is SendTransactionError.UnknownError -> transactionErrorDialog( + cause = error.ex?.localizedMessage, + code = "", + onConfirmClick = { onFailedTxEmailClick(error.ex?.localizedMessage.orEmpty()) }, + ) + is SendTransactionError.CreateAccountUnderfunded -> transactionErrorDialog( + causeTextReference = resourceReference( + R.string.no_account_polkadot, + wrappedList(error.amount), + ), + code = "", + onConfirmClick = popBackStack, + ) + else -> null + } + } + + private fun demoModeDialog(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) + + private fun transactionErrorDialog( + cause: String? = null, + causeTextReference: TextReference? = null, + code: String, + onConfirmClick: () -> Unit, + ): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.send_alert_transaction_failed_title), + message = resourceReference( + id = R.string.send_alert_transaction_failed_text, + formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), + ), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt deleted file mode 100644 index e63ccb0229..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertDemoModeUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import com.tangem.common.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference - -data class AlertDemoModeUM( - override val onConfirmClick: () -> Unit, -) : AlertUM { - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - override val title: TextReference = resourceReference(id = R.string.warning_demo_mode_title) - override val message: TextReference = resourceReference(id = R.string.warning_demo_mode_message) -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt deleted file mode 100644 index 6d9cea587a..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertTransactionErrorUM.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import com.tangem.common.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList - -data class AlertTransactionErrorUM( - val code: String, - val cause: String?, - val causeTextReference: TextReference? = null, - override val onConfirmClick: () -> Unit, -) : AlertUM { - override val title: TextReference = resourceReference(id = R.string.send_alert_transaction_failed_title) - override val message: TextReference = resourceReference( - id = R.string.send_alert_transaction_failed_text, - formatArgs = wrappedList(causeTextReference ?: cause.orEmpty(), code), - ) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) -} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt b/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt deleted file mode 100644 index 1cf955bebe..0000000000 --- a/common/ui/src/main/java/com/tangem/common/ui/alerts/models/AlertUM.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.common.ui.alerts.models - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference - -@Immutable -interface AlertUM { - val title: TextReference? - val message: TextReference - val confirmButtonText: TextReference - val onConfirmClick: (() -> Unit)? -} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt index 53e6ba8d82..f6f073d569 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/common/ui/OnboardingDialogUM.kt @@ -1,15 +1,14 @@ package com.tangem.features.onboarding.v2.common.ui -import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.extensions.TextReference internal data class OnboardingDialogUM( - override val title: TextReference, - override val message: TextReference, + val title: TextReference, + val message: TextReference, val dismissButtonText: TextReference, - override val confirmButtonText: TextReference, + val confirmButtonText: TextReference, val dismissWarningColor: Boolean = false, - override val onConfirmClick: () -> Unit, + val onConfirmClick: () -> Unit, val onDismissButtonClick: () -> Unit, val onDismiss: () -> Unit, -) : AlertUM \ No newline at end of file +) \ No newline at end of file diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt index fedeec4e55..a9ac694ab9 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/selecttoken/model/OnrampOperationModel.kt @@ -3,7 +3,6 @@ package com.tangem.features.onramp.selecttoken.model import arrow.core.getOrElse import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent @@ -13,8 +12,8 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.EventMessageAction import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.demo.IsDemoCardUseCase @@ -159,18 +158,9 @@ internal class OnrampOperationModel @Inject constructor( private fun showErrorIfDemoModeOrElse(action: () -> Unit) { if (selectedUserWallet is UserWallet.Cold && isDemoCardUseCase(cardId = selectedUserWallet.cardId)) { - val alertUM = AlertDemoModeUM(onConfirmClick = {}) - val message = DialogMessage( - title = alertUM.title, - message = alertUM.message, - firstActionBuilder = { - EventMessageAction( - title = alertUM.confirmButtonText, - onClick = alertUM.onConfirmClick, - ) - }, - secondActionBuilder = { cancelAction() }, + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), ) messageSender.send(message) diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt index 78b805b69a..9a9ee70d57 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/common/SendConfirmAlertFactory.kt @@ -1,7 +1,6 @@ package com.tangem.features.send.v2.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -14,6 +13,7 @@ import javax.inject.Inject @ModelScoped internal class SendConfirmAlertFactory @Inject constructor( private val messageSender: UiMessageSender, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, ) { fun getGenericErrorState(onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { @@ -31,34 +31,16 @@ internal class SendConfirmAlertFactory @Inject constructor( } fun getSendTransactionErrorState( - error: SendTransactionError?, + error: SendTransactionError, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - messageSender.send( - DialogMessage( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + messageSender.send(errorDialog) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 3ace74e0f1..2ef0911b14 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -67,7 +67,6 @@ import com.tangem.features.staking.impl.navigation.InnerStakingRouter import com.tangem.features.staking.impl.presentation.state.* import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType import com.tangem.features.staking.impl.presentation.state.events.StakingAlertUM -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.events.StakingEventFactory import com.tangem.features.staking.impl.presentation.state.helpers.StakingBalanceUpdater import com.tangem.features.staking.impl.presentation.state.helpers.StakingFeeLoader @@ -249,7 +248,7 @@ internal class StakingModel @Inject constructor( private val stakingEventFactory: StakingEventFactory get() = StakingEventFactory( - stateController = stateController, + messageSender = messageSender, popBackStack = ::onBackClick, onFailedTxEmailClick = ::onFailedTxEmailClick, ) @@ -503,11 +502,7 @@ internal class StakingModel @Inject constructor( cryptoCurrencyStatus = cryptoCurrencyStatus, ), ) - stateController.updateEvent( - StakingEvent.ShowAlert( - StakingAlertUM.FeeIncreased(stateController::dismissAlert), - ), - ) + messageSender.send(StakingAlertUM.feeIncreased {}) updateNotifications() }, onTransactionExpired = { @@ -571,9 +566,7 @@ internal class StakingModel @Inject constructor( override fun onAmountEnterClick() { if (integration.preferredTargets.isEmpty()) { - stateController.updateEvent( - StakingEvent.ShowAlert(StakingAlertUM.NoAvailableValidators), - ) + messageSender.send(StakingAlertUM.noAvailableValidators()) } else { if (uiState.value.actionType is StakingActionCommonType.Enter) { stateController.updateAll( @@ -1047,11 +1040,7 @@ internal class StakingModel @Inject constructor( } override fun showPrimaryClickAlert() { - stateController.updateEvent( - StakingEvent.ShowAlert( - StakingAlertUM.StakeMoreClickUnavailable(cryptoCurrencyStatus.currency), - ), - ) + messageSender.send(StakingAlertUM.stakeMoreClickUnavailable(cryptoCurrencyStatus.currency)) } override fun onOpenLearnMoreAboutApproveClick() { 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 00409eed5c..7a02bd36ef 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 @@ -3,12 +3,9 @@ package com.tangem.features.staking.impl.presentation.state import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer import com.tangem.features.staking.impl.presentation.state.transformers.SetTitleTransformer @@ -72,16 +69,6 @@ internal class StakingStateController @Inject constructor( mutableUiState.update(function = titleTransformer::transform) } - fun updateEvent(event: StakingEvent?) { - mutableUiState.update { - it.copy(event = event?.let { triggeredEvent(event, ::dismissAlert) } ?: consumedEvent()) - } - } - - fun dismissAlert() { - mutableUiState.update { it.copy(event = consumedEvent()) } - } - private fun getInitialState(): StakingUiState { return StakingUiState( title = TextReference.EMPTY, @@ -98,7 +85,6 @@ internal class StakingStateController @Inject constructor( rewardsValidatorsState = StakingStates.RewardsValidatorsState.Empty(), confirmationState = StakingStates.ConfirmationState.Empty(), isBalanceHidden = false, - event = consumedEvent(), bottomSheetConfig = null, actionType = StakingActionCommonType.Enter(skipEnterAmount = false), buttonsState = NavigationButtonsState.Empty, 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 699729948f..8624ce9a72 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 @@ -6,14 +6,12 @@ import com.tangem.common.ui.navigationButtons.NavigationButtonsState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.list.RoundedListWithDividersItemData -import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.models.staking.PendingAction import com.tangem.domain.staking.model.StakingTarget import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.features.staking.impl.presentation.state.bottomsheet.InfoType -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent import com.tangem.features.staking.impl.presentation.model.StakingClickIntents import kotlinx.collections.immutable.ImmutableList import java.math.BigDecimal @@ -40,7 +38,6 @@ internal data class StakingUiState( val bottomSheetConfig: TangemBottomSheetConfig?, val actionType: StakingActionCommonType, val buttonsState: NavigationButtonsState, - val event: StateEvent, val balanceState: BalanceState?, val showColdWalletInteractionIcon: Boolean, val shouldShowHoldToConfirmButton: Boolean, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt index 3e757a6adc..acba4a78d1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingAlertUM.kt @@ -1,85 +1,74 @@ package com.tangem.features.staking.impl.presentation.state.events -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM -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.domain.models.currency.CryptoCurrency import com.tangem.features.staking.impl.R -@Immutable -internal sealed class StakingAlertUM : AlertUM { +internal object StakingAlertUM { - data class GenericError( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.common_error) - override val message: TextReference = resourceReference(R.string.common_unknown_error) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) - } + fun genericError(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.common_unknown_error), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class StakingError( - val code: String, - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.common_error) - override val message: TextReference = resourceReference(R.string.generic_error_code, wrappedList(code)) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_support) - } + fun stakingError(code: String, onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.generic_error_code, wrappedList(code)), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data object NoAvailableValidators : StakingAlertUM() { - override val title = resourceReference(R.string.common_error) - override val message = resourceReference(R.string.staking_no_validators_error_message) - override val confirmButtonText = resourceReference(R.string.common_ok) - override val onConfirmClick = null - } + fun noAvailableValidators(): DialogMessage = DialogMessage( + title = resourceReference(R.string.common_error), + message = resourceReference(R.string.staking_no_validators_error_message), + ) - data class FeeIncreased( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference? = null - override val message: TextReference = resourceReference(id = R.string.send_notification_high_fee_title) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun feeIncreased(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = null, + message = resourceReference(id = R.string.send_notification_high_fee_title), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) - data object ValidatorsUnavailable : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference = resourceReference(id = R.string.staking_error_no_validators_title) - override val message: TextReference = resourceReference(id = R.string.staking_error_no_validators_message) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun validatorsUnavailable(): DialogMessage = DialogMessage( + title = resourceReference(id = R.string.staking_error_no_validators_title), + message = resourceReference(id = R.string.staking_error_no_validators_message), + ) - data class StakeMoreClickUnavailable( - val cryptoCurrency: CryptoCurrency, - ) : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference? = null - override val message: TextReference = resourceReference( + fun stakeMoreClickUnavailable(cryptoCurrency: CryptoCurrency): DialogMessage = DialogMessage( + title = null, + message = resourceReference( id = R.string.staking_stake_more_button_unavailability_reason, wrappedList(cryptoCurrency.name, cryptoCurrency.symbol), - ) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + ), + ) - data class RewardsMinimumRequirementsError( - val cryptoCurrencyName: String, - val cryptoAmountValue: String, - ) : StakingAlertUM() { - override val onConfirmClick: (() -> Unit)? = null - override val title: TextReference? = null - override val message: TextReference = resourceReference( - id = R.string.staking_details_min_rewards_notification, - formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue), + fun rewardsMinimumRequirementsError(cryptoCurrencyName: String, cryptoAmountValue: String): DialogMessage = + DialogMessage( + title = null, + message = resourceReference( + id = R.string.staking_details_min_rewards_notification, + formatArgs = wrappedList(cryptoCurrencyName, cryptoAmountValue), + ), ) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } - data class NetworkFeeUpdated( - override val onConfirmClick: () -> Unit, - ) : StakingAlertUM() { - override val title: TextReference = resourceReference(R.string.staking_alert_network_fee_updated_title) - override val message: TextReference = resourceReference(R.string.staking_alert_network_fee_updated_message) - override val confirmButtonText: TextReference = resourceReference(id = R.string.common_ok) - } + fun networkFeeUpdated(onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.staking_alert_network_fee_updated_title), + message = resourceReference(R.string.staking_alert_network_fee_updated_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt deleted file mode 100644 index 7e7700595e..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEvent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.staking.impl.presentation.state.events - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.extensions.TextReference - -@Immutable -internal sealed class StakingEvent { - - data class ShowSnackBar(val text: TextReference) : StakingEvent() - - data class ShowAlert(val alert: AlertUM) : StakingEvent() -} \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt index 4ba7fb1dd9..fd94adadd2 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/events/StakingEventFactory.kt @@ -1,69 +1,63 @@ package com.tangem.features.staking.impl.presentation.state.events -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.features.staking.impl.presentation.state.StakingStateController internal class StakingEventFactory( - private val stateController: StakingStateController, + private val messageSender: UiMessageSender, private val popBackStack: () -> Unit, private val onFailedTxEmailClick: (String) -> Unit, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory = TransactionErrorDialogFactory(), ) { fun createGenericErrorAlert(error: String) { - val alert = StakingEvent.ShowAlert( - StakingAlertUM.GenericError( + messageSender.send( + StakingAlertUM.genericError( onConfirmClick = { onFailedTxEmailClick(error) }, ), ) - stateController.updateEvent(alert) } fun createSendTransactionErrorAlert(error: SendTransactionError?) { val alert = error?.let { - TransactionErrorAlertConverter( + transactionErrorDialogFactory.create( + error = error, popBackStack = popBackStack, onFailedTxEmailClick = onFailedTxEmailClick, - ).convert(error) - }?.let { - StakingEvent.ShowAlert(it) + ) } - stateController.updateEvent(alert) + alert?.let { messageSender.send(it) } } fun createStakingErrorAlert(error: StakingError) { - val alert = StakingEvent.ShowAlert( - StakingAlertUM.StakingError( + messageSender.send( + StakingAlertUM.stakingError( code = error.toString(), onConfirmClick = { onFailedTxEmailClick(error.toString()) }, ), ) - stateController.updateEvent(alert) } fun createStakingValidatorsUnavailableAlert() { - val alert = StakingEvent.ShowAlert(alert = StakingAlertUM.ValidatorsUnavailable) - stateController.updateEvent(alert) + messageSender.send(StakingAlertUM.validatorsUnavailable()) } fun createStakingRewardsMinimumRequirementsErrorAlert(cryptoCurrencyName: String, cryptoAmountValue: String) { - stateController.updateEvent( - StakingEvent.ShowAlert( - alert = StakingAlertUM.RewardsMinimumRequirementsError( - cryptoCurrencyName = cryptoCurrencyName, - cryptoAmountValue = cryptoAmountValue, - ), + messageSender.send( + StakingAlertUM.rewardsMinimumRequirementsError( + cryptoCurrencyName = cryptoCurrencyName, + cryptoAmountValue = cryptoAmountValue, ), ) } fun createNetworkFeeUpdatedAlert(onConfirm: () -> Unit) { - val alert = StakingEvent.ShowAlert( - alert = StakingAlertUM.NetworkFeeUpdated( + messageSender.send( + StakingAlertUM.networkFeeUpdated( onConfirmClick = onConfirm, ), ) - stateController.updateEvent(alert) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt deleted file mode 100644 index 96234a843e..0000000000 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingEventEffect.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.tangem.features.staking.impl.presentation.ui - -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.features.staking.impl.R -import com.tangem.features.staking.impl.presentation.state.events.StakingEvent - -@Composable -internal fun StakingEventEffect(event: StateEvent, snackbarHostState: SnackbarHostState) { - val resources = LocalContext.current.resources - var alertConfig by remember { mutableStateOf(value = null) } - - val keyboardController = LocalSoftwareKeyboardController.current - LaunchedEffect(key1 = alertConfig) { - keyboardController?.hide() - } - - alertConfig?.let { - StakingAlert(state = it, onDismiss = { alertConfig = null }) - } - - EventEffect( - event = event, - onTrigger = { value -> - when (value) { - is StakingEvent.ShowSnackBar -> { - snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) - } - is StakingEvent.ShowAlert -> { - alertConfig = value.alert - } - } - }, - ) -} - -@Composable -internal fun StakingAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton: DialogButtonUM - val dismissButton: DialogButtonUM? - - val onActionClick = state.onConfirmClick - if (onActionClick != null) { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = { - onActionClick() - onDismiss() - }, - ) - - dismissButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_cancel), - onClick = onDismiss, - ) - } else { - confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = onDismiss, - ) - dismissButton = null - } - - BasicDialog( - message = state.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = state.title?.resolveReference(), - dismissButton = dismissButton, - ) -} \ No newline at end of file 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 0196216ea0..836d37dec7 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 @@ -7,7 +7,6 @@ import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,7 +38,6 @@ import kotlinx.coroutines.flow.withIndex @Composable internal fun StakingScreen(uiState: StakingUiState) { - val snackbarHostState = remember { SnackbarHostState() } val confirmationState = uiState.confirmationState as? StakingStates.ConfirmationState.Data BackHandler(onBack = uiState.clickIntents::onPrevClick) @@ -71,11 +69,6 @@ internal fun StakingScreen(uiState: StakingUiState) { ) StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig) } - - StakingEventEffect( - event = uiState.event, - snackbarHostState = snackbarHostState, - ) } @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt index 0994844d9b..79c2503262 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/common/SwapAlertFactory.kt @@ -1,7 +1,6 @@ package com.tangem.features.swap.v2.impl.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -25,6 +24,7 @@ internal class SwapAlertFactory @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, ) { fun getGenericErrorState(expressError: ExpressError, onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { uiMessageSender.send( @@ -44,36 +44,21 @@ internal class SwapAlertFactory @Inject constructor( ) } + @Suppress("CanBeNonNullable") fun getSendTransactionErrorState( error: SendTransactionError?, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + if (error == null) return + + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - uiMessageSender.send( - DialogMessage.Companion( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + uiMessageSender.send(errorDialog) } suspend fun onFailedTxEmailClick( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt index 646e3a3218..653bb2b5e3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/SwapTransactionErrorStateConverter.kt @@ -1,7 +1,7 @@ package com.tangem.feature.swap.converters -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory +import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.feature.swap.domain.models.ui.SwapTransactionState import com.tangem.feature.swap.models.SwapAlertUM @@ -11,24 +11,25 @@ import com.tangem.utils.converter.Converter internal class SwapTransactionErrorStateConverter( private val onDismiss: () -> Unit, private val onSupportClick: (String) -> Unit, -) : Converter { - override fun convert(value: SwapTransactionState.Error): AlertUM? { + private val transactionErrorDialogFactory: TransactionErrorDialogFactory = TransactionErrorDialogFactory(), +) : Converter { + override fun convert(value: SwapTransactionState.Error): DialogMessage? { return when (value) { is SwapTransactionState.Error.TransactionError -> { when (val error = value.error) { is SendTransactionError.UserCancelledError -> return null - null -> SwapAlertUM.GenericError(onDismiss) - else -> TransactionErrorAlertConverter(onDismiss, onSupportClick).convert(error) + null -> SwapAlertUM.genericError(onDismiss) + else -> transactionErrorDialogFactory.create(error, onDismiss, onSupportClick) } } is SwapTransactionState.Error.ExpressError -> { - SwapAlertUM.ExpressErrorAlert( + SwapAlertUM.expressErrorAlert( message = getExpressErrorMessage(value.error), onConfirmClick = { onSupportClick(value.error.code.toString()) }, ) } - SwapTransactionState.Error.UnknownError -> SwapAlertUM.GenericError(onDismiss) - is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.GenericError( + SwapTransactionState.Error.UnknownError -> SwapAlertUM.genericError(onDismiss) + is SwapTransactionState.Error.TangemPayWithdrawalError -> SwapAlertUM.genericError( onConfirmClick = { onSupportClick(value.txId) }, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index fb4e74e1ff..b929ec77f6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -23,11 +23,21 @@ import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.toWrappedList +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.utils.InputNumberFormatter +import com.tangem.core.ui.utils.parseBigDecimal +import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter +import com.tangem.feature.swap.models.SwapAlertUM import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus @@ -165,6 +175,7 @@ internal class SwapModel @Inject constructor( private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, private val appsFlyerStore: AppsFlyerStore, private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, + private val messageSender: UiMessageSender, ) : Model() { private val params = paramsContainer.require() @@ -349,7 +360,8 @@ internal class SwapModel @Inject constructor( } if (fromAccountStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) + showAlert() + swapRouter.back() } else { fromAccountCurrencyStatus = fromAccountStatus toAccountCurrencyStatus = toAccountStatus @@ -367,7 +379,8 @@ internal class SwapModel @Inject constructor( } if (fromStatus == null) { - uiState = stateBuilder.addAlert(uiState = uiState, onDismiss = swapRouter::back) + showAlert() + swapRouter.back() } else { initialFromStatus = fromStatus initialToStatus = toStatus @@ -1032,7 +1045,7 @@ internal class SwapModel @Inject constructor( val fee = getSelectedFee() if (fee == null && tangemPayInput?.isWithdrawal != true) { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) modelScope.launch { delay(SWAP_IN_PROGRESS_DELAY) startLoadingQuotesFromLastState() @@ -1058,7 +1071,7 @@ internal class SwapModel @Inject constructor( when (swapTransactionState) { is SwapTransactionState.TxSent -> { if (fee == null) { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) return@onSuccess } sendSuccessSwapEvent( @@ -1102,21 +1115,11 @@ internal class SwapModel @Inject constructor( swapRouter.openScreen(SwapNavScreen.Success) } SwapTransactionState.DemoMode -> { - uiState = stateBuilder.createDemoModeAlert( - uiState = uiState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showDemoModeAlert() } is SwapTransactionState.Error -> { startLoadingQuotesFromLastState() - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, - error = swapTransactionState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = ::onFailedTxEmailClick, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showTransactionErrorAlert(swapTransactionState) } is SwapTransactionState.TangemPayWithdrawalData -> { processTangemPayWithdrawal(swapTransactionState = swapTransactionState) @@ -1125,7 +1128,7 @@ internal class SwapModel @Inject constructor( }.onFailure { error -> Timber.e(error) startLoadingQuotesFromLastState() - makeDefaultAlert() + showAlert() } } } @@ -1217,7 +1220,7 @@ internal class SwapModel @Inject constructor( } val feeForPermission = when (val fee = approveDataModel.fee) { TxFeeState.Empty -> { - makeDefaultAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) + showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) Timber.e("Fee should not be Empty") return@launch } @@ -1247,29 +1250,19 @@ internal class SwapModel @Inject constructor( startLoadingQuotesFromLastState(isSilent = true) } is SwapTransactionState.Error -> { - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, - error = swapTransactionState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = ::onFailedTxEmailClick, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showTransactionErrorAlert(swapTransactionState) } SwapTransactionState.DemoMode -> { - uiState = stateBuilder.createDemoModeAlert( - uiState = uiState, - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - isReverseSwapPossible = isReverseSwapPossible(), - ) + showDemoModeAlert() } is SwapTransactionState.TangemPayWithdrawalData -> { processTangemPayWithdrawal(swapTransactionState = swapTransactionState) } } - }.onFailure { makeDefaultAlert() } + }.onFailure { showAlert() } }.onFailure { error -> Timber.e(error.message.orEmpty()) - makeDefaultAlert() + showAlert() } } } @@ -1652,12 +1645,94 @@ internal class SwapModel @Inject constructor( return inputNumberFormatter.getValidatedNumberWithFixedDecimals(amount, maxDecimals) } - private fun makeDefaultAlert() { - uiState = stateBuilder.addAlert(uiState) + private fun showAlert(message: TextReference = resourceReference(R.string.common_unknown_error)) { + messageSender.send(SwapAlertUM.genericError(onConfirmClick = { }, message = message)) } - private fun makeDefaultAlert(message: TextReference) { - uiState = stateBuilder.addAlert(uiState, message) + private fun showDemoModeAlert() { + messageSender.send( + DialogMessage( + title = resourceReference(id = R.string.warning_demo_mode_title), + message = resourceReference(id = R.string.warning_demo_mode_message), + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = {}, + ), + ), + ) + } + + private fun showTransactionErrorAlert( + error: SwapTransactionState.Error, + onSupportClick: (String) -> Unit = ::onFailedTxEmailClick, + ) { + val errorAlert = SwapTransactionErrorStateConverter( + onDismiss = {}, + onSupportClick = onSupportClick, + ).convert(error) + errorAlert?.let { messageSender.send(it) } + } + + private fun onTangemPaySupportClick(txId: String) { + modelScope.launch { + val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch + val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull().orEmpty() + val email = FeedbackEmailType.Visa.Withdrawal( + walletMetaInfo = metaInfo, + customerId = customerId, + providerName = dataState.selectedProvider?.name.orEmpty(), + txId = txId, + ) + sendFeedbackEmailUseCase(email) + } + } + + private fun showSwapInfoAlert(isPriceImpact: Boolean, token: String, provider: SwapProvider) { + messageSender.send( + SwapAlertUM.informationAlert( + message = buildSwapInfoMessage(isPriceImpact, token, provider), + onConfirmClick = {}, + ), + ) + } + + private fun buildSwapInfoMessage(isPriceImpact: Boolean, token: String, provider: SwapProvider): TextReference { + val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}%" } + val messages = buildList { + when (provider.type) { + ExchangeProviderType.CEX -> { + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_cex_description_with_slippage, + formatArgs = wrappedList(token, slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) + } + } + ExchangeProviderType.DEX, + ExchangeProviderType.DEX_BRIDGE, + -> { + if (isPriceImpact) { + add(resourceReference(R.string.swapping_high_price_impact_description)) + add(stringReference("\n\n")) + } + if (slippage != null) { + add( + resourceReference( + id = R.string.swapping_alert_dex_description_with_slippage, + formatArgs = wrappedList(slippage), + ), + ) + } else { + add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(token))) + } + } + } + } + return combinedReference(messages.toWrappedList()) } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -1777,14 +1852,7 @@ internal class SwapModel @Inject constructor( val selectedProvider = dataState.selectedProvider ?: return@UiActions val currencySymbol = dataState.toCryptoCurrency?.currency?.symbol ?: return@UiActions val isPriceImpact = uiState.priceImpact is PriceImpact.Value - uiState = stateBuilder.createAlert( - uiState = uiState, - isPriceImpact = isPriceImpact, - token = currencySymbol, - provider = selectedProvider, - isReverseSwapPossible = isReverseSwapPossible(), - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - ) + showSwapInfoAlert(isPriceImpact, currencySymbol, selectedProvider) }, onLinkClick = urlOpener::openUrl, onSelectTokenClick = { @@ -2090,31 +2158,12 @@ internal class SwapModel @Inject constructor( } private fun onTangemPayWithdrawalError(txId: String?) { - uiState = stateBuilder.createErrorTransactionAlert( - uiState = uiState, + showTransactionErrorAlert( error = SwapTransactionState.Error.TangemPayWithdrawalError(txId.orEmpty()), - onDismiss = { uiState = stateBuilder.clearAlert(uiState) }, - onSupportClick = { - val customerId = getTangemPayCustomerIdUseCase(userWallet.walletId).getOrNull() ?: "Unknown" - onTangemPaySupportClick(customerId = customerId, txId = txId) - }, - isReverseSwapPossible = isReverseSwapPossible(), + onSupportClick = ::onTangemPaySupportClick, ) } - private fun onTangemPaySupportClick(customerId: String, txId: String?) { - modelScope.launch { - val metaInfo = getWalletMetaInfoUseCase(userWallet.walletId).getOrNull() ?: return@launch - val email = FeedbackEmailType.Visa.Withdrawal( - walletMetaInfo = metaInfo, - customerId = customerId, - providerName = dataState.selectedProvider?.name.orEmpty(), - txId = txId.orEmpty(), - ) - sendFeedbackEmailUseCase(email) - } - } - private fun onFailedTxEmailClick(errorMessage: String) { modelScope.launch { val transaction = dataState.swapDataModel?.transaction diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt index bb84996732..fcf6277b98 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapAlertUM.kt @@ -1,38 +1,43 @@ package com.tangem.feature.swap.models -import com.tangem.common.ui.alerts.models.AlertUM import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction -sealed class SwapAlertUM : AlertUM { +internal object SwapAlertUM { - data class GenericError( - override val onConfirmClick: (() -> Unit), - override val message: TextReference = resourceReference(R.string.common_unknown_error), - ) : SwapAlertUM() { - override val title: TextReference? = null - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) - } + fun genericError( + onConfirmClick: () -> Unit, + message: TextReference = resourceReference(R.string.common_unknown_error), + ): DialogMessage = DialogMessage( + title = null, + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class ExpressErrorAlert( - override val message: TextReference = resourceReference(R.string.common_unknown_error), - override val onConfirmClick: (() -> Unit), - ) : SwapAlertUM() { - override val title: TextReference? = null - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_support) - } + fun expressErrorAlert( + message: TextReference = resourceReference(R.string.common_unknown_error), + onConfirmClick: () -> Unit, + ): DialogMessage = DialogMessage( + title = null, + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_support), + onClick = onConfirmClick, + ), + ) - data class InformationAlert( - override val message: TextReference, - override val onConfirmClick: (() -> Unit), - ) : SwapAlertUM() { - override val title: TextReference = resourceReference( - R.string.swapping_alert_title, - ) - override val confirmButtonText: TextReference = - resourceReference(id = R.string.common_ok) - } + fun informationAlert(message: TextReference, onConfirmClick: () -> Unit): DialogMessage = DialogMessage( + title = resourceReference(R.string.swapping_alert_title), + message = message, + firstAction = EventMessageAction( + title = resourceReference(id = R.string.common_ok), + onClick = onConfirmClick, + ), + ) } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 47517e2801..02b46b544c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -7,14 +7,11 @@ import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.swapStoriesScreen.SwapStoriesUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState -import com.tangem.feature.swap.models.states.events.SwapEvent import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -24,7 +21,6 @@ internal data class SwapStateHolder( val blockchainId: String, // not the same as networkId, its local id in app val notifications: ImmutableList = persistentListOf(), val isInsufficientFunds: Boolean, - val event: StateEvent = consumedEvent(), val changeCardsButtonState: ChangeCardsButtonState, val providerState: ProviderState, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt deleted file mode 100644 index a00fed2610..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/models/states/events/SwapEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.feature.swap.models.states.events - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.alerts.models.AlertUM - -@Immutable -internal sealed class SwapEvent { - data class ShowAlert(val alert: AlertUM) : SwapEvent() -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 6c955e7c86..ed0a2dcfe8 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -5,21 +5,17 @@ import androidx.compose.ui.text.input.TextFieldValue import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM -import com.tangem.common.ui.alerts.models.AlertDemoModeUM import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.swapStoriesScreen.SwapStoriesFactory import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent import com.tangem.core.ui.extensions.* import com.tangem.core.ui.format.bigdecimal.anyDecimals import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency @@ -29,7 +25,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.promo.models.StoryContent import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork -import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.converters.TokensDataConverterV2 import com.tangem.feature.swap.domain.models.ExpressDataError @@ -43,12 +38,10 @@ import com.tangem.feature.swap.model.SwapNotificationsFactory import com.tangem.feature.swap.model.SwapProcessDataState import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* -import com.tangem.feature.swap.models.states.events.SwapEvent import com.tangem.feature.swap.presentation.R import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.utils.Provider import com.tangem.utils.StringsSigns.DASH_SIGN -import com.tangem.utils.StringsSigns.PERCENT import com.tangem.utils.StringsSigns.TILDE_SIGN import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -972,117 +965,6 @@ internal class StateBuilder( ) } - fun createErrorTransactionAlert( - uiState: SwapStateHolder, - error: SwapTransactionState.Error, - onDismiss: () -> Unit, - onSupportClick: (String) -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - val errorAlert = SwapTransactionErrorStateConverter( - onSupportClick = onSupportClick, - onDismiss = onDismiss, - ).convert(error) - return uiState.copy( - event = errorAlert?.let { - triggeredEvent( - data = SwapEvent.ShowAlert(errorAlert), - onConsume = onDismiss, - ) - } ?: consumedEvent(), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - fun createDemoModeAlert( - uiState: SwapStateHolder, - onDismiss: () -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - return uiState.copy( - event = triggeredEvent( - data = SwapEvent.ShowAlert(AlertDemoModeUM(onDismiss)), - onConsume = onDismiss, - ), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - @Suppress("LongParameterList") - fun createAlert( - uiState: SwapStateHolder, - isPriceImpact: Boolean, - token: String, - provider: SwapProvider, - onDismiss: () -> Unit, - isReverseSwapPossible: Boolean, - ): SwapStateHolder { - val slippage = provider.slippage?.let { "${it.parseBigDecimal(1)}$PERCENT" } - val combinedMessage = buildList { - when (provider.type) { - ExchangeProviderType.CEX -> { - if (slippage != null) { - add( - resourceReference( - id = R.string.swapping_alert_cex_description_with_slippage, - formatArgs = wrappedList(token, slippage), - ), - ) - } else { - add(resourceReference(R.string.swapping_alert_cex_description, wrappedList(token))) - } - } - ExchangeProviderType.DEX, - ExchangeProviderType.DEX_BRIDGE, - -> { - if (isPriceImpact) { - add(resourceReference(R.string.swapping_high_price_impact_description)) - add(stringReference("\n\n")) - } - if (slippage != null) { - add( - resourceReference( - id = R.string.swapping_alert_dex_description_with_slippage, - formatArgs = wrappedList(slippage), - ), - ) - } else { - add(resourceReference(R.string.swapping_alert_dex_description, wrappedList(token))) - } - } - } - } - return uiState.copy( - event = triggeredEvent( - SwapEvent.ShowAlert( - SwapAlertUM.InformationAlert( - message = combinedReference(combinedMessage.toWrappedList()), - onConfirmClick = onDismiss, - ), - ), - onConsume = onDismiss, - ), - changeCardsButtonState = getChangeCardsButtonState(isReverseSwapPossible), - ) - } - - fun addAlert( - uiState: SwapStateHolder, - message: TextReference = resourceReference(R.string.common_unknown_error), - onDismiss: () -> Unit = { clearAlert(uiState) }, - ): SwapStateHolder { - return uiState.copy( - event = triggeredEvent( - SwapEvent.ShowAlert( - SwapAlertUM.GenericError(onDismiss, message), - ), - onConsume = onDismiss, - ), - ) - } - - fun clearAlert(uiState: SwapStateHolder): SwapStateHolder = uiState.copy(event = consumedEvent()) - fun addNotification(uiState: SwapStateHolder, message: TextReference?, onClick: () -> Unit): SwapStateHolder { return uiState.copy( notifications = notificationsFactory.getGeneralErrorStateNotifications( diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt deleted file mode 100644 index 3a7455b5b0..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapEventEffect.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.tangem.feature.swap.ui - -import androidx.compose.runtime.* -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import com.tangem.common.ui.alerts.models.AlertUM -import com.tangem.core.ui.components.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.feature.swap.models.states.events.SwapEvent -import com.tangem.feature.swap.presentation.R - -@Composable -internal fun SwapEventEffect(event: StateEvent) { - var alertConfig by remember { mutableStateOf(value = null) } - - val keyboardController = LocalSoftwareKeyboardController.current - LaunchedEffect(key1 = alertConfig) { - keyboardController?.hide() - } - - alertConfig?.let { - SwapAlert(state = it, onDismiss = { alertConfig = null }) - } - - EventEffect( - event = event, - onTrigger = { value -> - when (value) { - is SwapEvent.ShowAlert -> { - alertConfig = value.alert - } - } - }, - ) -} - -@Composable -internal fun SwapAlert(state: AlertUM, onDismiss: () -> Unit) { - val confirmButton = DialogButtonUM( - title = state.confirmButtonText.resolveReference(), - onClick = { - state.onConfirmClick?.invoke() - onDismiss() - }, - ) - val dismissButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_cancel), - onClick = onDismiss, - ) - - BasicDialog( - message = state.message.resolveReference(), - confirmButton = confirmButton, - onDismissDialog = onDismiss, - title = state.title?.resolveReference(), - dismissButton = dismissButton, - ) -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index b4d3c15097..813e71cc11 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -117,10 +117,6 @@ internal fun SwapScreenContent( textAlign = TextAlign.Start, ) } - - SwapEventEffect( - event = state.event, - ) } } diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt index 00958d2ec1..078a18f3e9 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/common/YieldSupplyAlertFactory.kt @@ -1,7 +1,6 @@ package com.tangem.features.yield.supply.impl.common -import com.tangem.common.ui.alerts.TransactionErrorAlertConverter -import com.tangem.common.ui.alerts.models.AlertDemoModeUM +import com.tangem.common.ui.alerts.TransactionErrorDialogFactory import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference @@ -24,6 +23,7 @@ class YieldSupplyAlertFactory @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, + private val transactionErrorDialogFactory: TransactionErrorDialogFactory, ) { fun getGenericErrorState(onFailedTxEmailClick: () -> Unit, popBack: () -> Unit = {}) { @@ -41,35 +41,17 @@ class YieldSupplyAlertFactory @Inject constructor( } fun getSendTransactionErrorState( - error: SendTransactionError?, + error: SendTransactionError, popBack: () -> Unit, onFailedTxEmailClick: (String) -> Unit, ) { - val transactionErrorAlertConverter = TransactionErrorAlertConverter( + val errorDialog = transactionErrorDialogFactory.create( + error = error, popBackStack = popBack, onFailedTxEmailClick = onFailedTxEmailClick, - ) + ) ?: return - val errorAlert = error?.let { transactionErrorAlertConverter.convert(error) } ?: return - val onConfirmClick = errorAlert.onConfirmClick ?: return - - uiMessageSender.send( - DialogMessage.Companion( - title = errorAlert.title, - message = errorAlert.message, - firstActionBuilder = { - EventMessageAction( - title = errorAlert.confirmButtonText, - onClick = onConfirmClick, - ) - }, - secondActionBuilder = if (errorAlert !is AlertDemoModeUM) { - { cancelAction() } - } else { - null - }, - ), - ) + uiMessageSender.send(errorDialog) } suspend fun onFailedTxEmailClick(userWallet: UserWallet, cryptoCurrency: CryptoCurrency?, errorMessage: String?) { From b94e3f814b29c5d3fe297c207b2829672fe903eb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 11:46:23 +0400 Subject: [PATCH 31/97] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../GiveTxPermisssionBottomSheet.kt | 1 + .../configs/feature_toggles_config.json | 4 + features/approval/api/build.gradle.kts | 28 ++ .../approval/api/GiveApprovalComponent.kt | 30 ++ .../api/GiveApprovalFeatureToggles.kt | 6 + features/approval/impl/build.gradle.kts | 57 +++ .../impl/DefaultGiveApprovalComponent.kt | 105 ++++++ .../impl/DefaultGiveApprovalFeatureToggles.kt | 13 + .../impl/di/GiveApprovalBindsModule.kt | 33 ++ .../approval/impl/model/GiveApprovalModel.kt | 203 ++++++++++ .../approval/impl/model/GiveApprovalUM.kt | 12 + .../approval/impl/ui/GiveApprovalContent.kt | 353 ++++++++++++++++++ .../ui/PreviewFeeSelectorBlockComponent.kt | 15 + .../api/analytics/CommonSendAnalyticEvents.kt | 2 + settings.gradle.kts | 3 + 16 files changed, 867 insertions(+) create mode 100644 features/approval/api/build.gradle.kts create mode 100644 features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt create mode 100644 features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt create mode 100644 features/approval/impl/build.gradle.kts create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt create mode 100644 features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7b732b629d..53abf2183b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -305,6 +305,8 @@ dependencies { implementation(projects.features.tokenRecieve.impl) implementation(projects.features.yieldSupply.api) implementation(projects.features.yieldSupply.impl) + implementation(projects.features.approval.api) + implementation(projects.features.approval.impl) /** AndroidX libraries */ implementation(deps.androidx.core.ktx) diff --git a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt index 4b4ca2eb11..7af3019d50 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/bottomsheet/permission/GiveTxPermisssionBottomSheet.kt @@ -37,6 +37,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import kotlinx.collections.immutable.ImmutableList +@Deprecated("Use GiveApprovalComponent") @Composable fun GiveTxPermissionBottomSheet(config: TangemBottomSheetConfig) { var isPermissionAlertShow by remember { mutableStateOf(false) } diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 23de2f59f4..23b909ebad 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -55,5 +55,9 @@ { "name": "WALLET_REORDER_FEATURE_ENABLED", "version": "5.34" + }, + { + "name": "GASLESS_APPROVAL_ENABLED", + "version": "undefined" } ] diff --git a/features/approval/api/build.gradle.kts b/features/approval/api/build.gradle.kts new file mode 100644 index 0000000000..0401b93110 --- /dev/null +++ b/features/approval/api/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.approval.api" +} + +dependencies { + /** Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) + + /** Common */ + implementation(projects.common.ui) + + /** Other */ + implementation(deps.kotlin.immutable.collections) + + /** Compose */ + implementation(deps.compose.runtime) +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt new file mode 100644 index 0000000000..b680b9470b --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt @@ -0,0 +1,30 @@ +package com.tangem.features.approval.api + +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet + +interface GiveApprovalComponent : ComposableBottomSheetComponent { + + data class Params( + val userWallet: UserWallet, + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val feeCryptoCurrencyStatus: CryptoCurrencyStatus, + val amount: String, + val spenderAddress: String, + val subtitle: TextReference, + val callback: Callback, + ) + + interface Callback { + fun onApproveDone() + fun onApproveFailed() + fun onCancelClick() + } + + interface Factory { + fun create(context: AppComponentContext, params: Params): GiveApprovalComponent + } +} \ No newline at end of file diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt new file mode 100644 index 0000000000..46410d3fbf --- /dev/null +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalFeatureToggles.kt @@ -0,0 +1,6 @@ +package com.tangem.features.approval.api + +interface GiveApprovalFeatureToggles { + + val isGaslessApprovalEnabled: Boolean +} \ No newline at end of file diff --git a/features/approval/impl/build.gradle.kts b/features/approval/impl/build.gradle.kts new file mode 100644 index 0000000000..01c43ad67f --- /dev/null +++ b/features/approval/impl/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.approval.impl" +} + +dependencies { + + /** Feature */ + implementation(projects.features.approval.api) + implementation(projects.features.sendV2.api) + + /** Core */ + implementation(projects.core.configToggles) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.ui) + implementation(projects.core.utils) + + /** Common */ + implementation(projects.common.ui) + + /** SDK */ + implementation(tangemDeps.blockchain) { + exclude(module = "joda-time") + } + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.wallets.models) + implementation(projects.domain.transaction.models) + implementation(projects.domain.transaction) + + /** Compose */ + implementation(deps.compose.foundation) + implementation(deps.compose.runtime) + implementation(deps.compose.material3) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + + /** Other */ + implementation(deps.decompose) + implementation(deps.decompose.ext.compose) + implementation(deps.timber) + implementation(deps.kotlin.immutable.collections) + implementation(deps.arrow.core) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt new file mode 100644 index 0000000000..51c2fe3986 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt @@ -0,0 +1,105 @@ +package com.tangem.features.approval.impl + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.common.ui.userwallet.ext.walletInterationIcon +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.appbar.models.TopAppBarButtonUM +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.impl.model.GiveApprovalModel +import com.tangem.features.approval.impl.ui.GiveApprovalContent +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.features.send.v2.api.params.FeeSelectorParams +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import com.tangem.common.ui.R as CommonUiR + +internal class DefaultGiveApprovalComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: GiveApprovalComponent.Params, + feeSelectorBlockComponentFactory: FeeSelectorBlockComponent.Factory, +) : GiveApprovalComponent, AppComponentContext by appComponentContext { + + private val model: GiveApprovalModel = getOrCreateModel(params = params) + + private val feeSelectorBlockComponent = feeSelectorBlockComponentFactory.create( + context = child("giveApprovalFeeSelector"), + params = FeeSelectorParams.FeeSelectorBlockParams( + state = FeeSelectorUM.Loading, + onLoadFee = { model.loadFee() }, + onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) }, + feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus, + cryptoCurrencyStatus = params.cryptoCurrencyStatus, + feeStateConfiguration = FeeSelectorParams.FeeStateConfiguration.None, + feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, + analyticsCategoryName = CommonSendAnalyticEvents.APPROVE_CATEGORY, + analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Approve, + userWalletId = params.userWallet.walletId, + ), + onResult = model::onFeeResult, + ) + + private val currency: String = params.cryptoCurrencyStatus.currency.symbol + + override fun dismiss() { + params.callback.onCancelClick() + } + + @Composable + override fun BottomSheet() { + val uiState by model.uiState.collectAsStateWithLifecycle() + + val config = remember { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ) + } + + TangemBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.secondary, + titleText = resourceReference(CommonUiR.string.give_permission_title), + titleAction = TopAppBarButtonUM.Icon( + iconRes = CommonUiR.drawable.ic_information_24, + onClicked = model::showPermissionInfoDialog, + ), + ) { + GiveApprovalContent( + currency = currency, + subtitle = params.subtitle, + approveType = uiState.approveType, + approveItems = uiState.approveItems, + onChangeApproveType = model::onChangeApproveType, + walletInteractionIcon = walletInterationIcon(params.userWallet), + isApproveEnabled = uiState.isApproveButtonEnabled, + isApproveLoading = uiState.isApproveLoading, + onApproveClick = model::onApproveClick, + onCancelClick = model::onCancelClick, + onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) + } + } + + @AssistedFactory + interface Factory : GiveApprovalComponent.Factory { + override fun create( + context: AppComponentContext, + params: GiveApprovalComponent.Params, + ): DefaultGiveApprovalComponent + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt new file mode 100644 index 0000000000..07867cc6a6 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt @@ -0,0 +1,13 @@ +package com.tangem.features.approval.impl + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.approval.api.GiveApprovalFeatureToggles + +internal class DefaultGiveApprovalFeatureToggles( + private val featureToggles: FeatureTogglesManager, +) : GiveApprovalFeatureToggles { + + // Remove GiveTxPermissionBottomSheet and all dependencies with this toggle + override val isGaslessApprovalEnabled: Boolean + get() = featureToggles.isFeatureEnabled("GASLESS_APPROVAL_ENABLED") +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt new file mode 100644 index 0000000000..94d432e9b9 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt @@ -0,0 +1,33 @@ +package com.tangem.features.approval.impl.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.impl.DefaultGiveApprovalComponent +import com.tangem.features.approval.impl.model.GiveApprovalModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap +import javax.inject.Singleton + +@InstallIn(SingletonComponent::class) +@Module +internal interface GiveApprovalFeatureModule { + + @Binds + @Singleton + fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory +} + +@Module +@InstallIn(ModelComponent::class) +internal interface GiveApprovalModelModule { + + @Binds + @IntoMap + @ClassKey(GiveApprovalModel::class) + fun bindModel(model: GiveApprovalModel): Model +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt new file mode 100644 index 0000000000..2b3fb3fa8f --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -0,0 +1,203 @@ +package com.tangem.features.approval.impl.model + +import androidx.compose.runtime.Stable +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.left +import com.tangem.blockchain.common.TransactionData +import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.transaction.models.TransactionFeeExtended +import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase +import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase +import com.tangem.domain.transaction.usecase.gasless.GetFeeForGaslessUseCase +import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback +import com.tangem.features.send.v2.api.entity.FeeSelectorUM +import com.tangem.core.navigation.url.UrlOpener +import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import timber.log.Timber +import java.math.BigDecimal +import javax.inject.Inject + +@Stable +@ModelScoped +@Suppress("LongParameterList") +internal class GiveApprovalModel @Inject constructor( + override val dispatchers: CoroutineDispatcherProvider, + paramsContainer: ParamsContainer, + private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, + private val getFeeUseCase: GetFeeUseCase, + private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase, + private val getFeeForTokenUseCase: GetFeeForTokenUseCase, + private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase, + private val uiMessageSender: UiMessageSender, + private val urlOpener: UrlOpener, +) : Model(), FeeSelectorModelCallback { + + private val params: GiveApprovalComponent.Params = paramsContainer.require() + + val uiState: StateFlow + field = MutableStateFlow( + GiveApprovalUM( + approveType = ApproveType.LIMITED, + isApproveButtonEnabled = false, + isApproveLoading = false, + ), + ) + + private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading + + override fun onFeeResult(feeSelectorUM: FeeSelectorUM) { + this.feeSelectorUM = feeSelectorUM + uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) } + } + + fun onApproveClick() { + uiState.update { it.copy(isApproveLoading = true) } + modelScope.launch(dispatchers.main) { + val isSuccess = sendApprovalTransaction() + uiState.update { it.copy(isApproveLoading = false) } + if (isSuccess) { + params.callback.onApproveDone() + } else { + params.callback.onApproveFailed() + } + } + } + + fun onCancelClick() { + params.callback.onCancelClick() + } + + fun onChangeApproveType(approveType: ApproveType) { + uiState.update { it.copy(approveType = approveType) } + } + + fun onOpenLearnMoreAboutApproveClick() { + urlOpener.openUrl(RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP) + } + + fun showPermissionInfoDialog() { + uiMessageSender.send( + DialogMessage( + message = resourceReference(com.tangem.common.ui.R.string.give_permission_staking_footer), + title = resourceReference(com.tangem.common.ui.R.string.common_approve), + ), + ) + } + + suspend fun prepareApprovalTransaction(): Either { + val cryptoCurrencyStatus = params.cryptoCurrencyStatus + val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token + ?: return Either.Left(IllegalStateException("Currency is not a token")) + + return createApprovalTransactionUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, + amount = getApprovalAmount(), + contractAddress = tokenCurrency.contractAddress, + spenderAddress = params.spenderAddress, + ) + } + + suspend fun loadFee(): Either { + val approvalTransaction = prepareApprovalTransaction() + .getOrElse { return GetFeeError.DataError(it).left() } + + return getFeeUseCase( + transactionData = approvalTransaction, + userWallet = params.userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ) + } + + suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either { + val approvalTransaction = prepareApprovalTransaction() + .getOrElse { return GetFeeError.DataError(it).left() } + + return if (maybeToken == null) { + getFeeForGaslessUseCase( + transactionData = approvalTransaction, + userWallet = params.userWallet, + network = params.cryptoCurrencyStatus.currency.network, + ) + } else { + getFeeForTokenUseCase( + transactionData = approvalTransaction, + userWallet = params.userWallet, + token = maybeToken.currency, + ) + } + } + + private suspend fun sendApprovalTransaction(): Boolean { + val cryptoCurrencyStatus = params.cryptoCurrencyStatus + val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false + + val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false + val selectedFee = feeContent.selectedFeeItem.fee + val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended + + val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency + + val transactionData = createApprovalTransactionUseCase( + cryptoCurrencyStatus = cryptoCurrencyStatus, + userWalletId = params.userWallet.walletId, + amount = getApprovalAmount(), + fee = selectedFee, + contractAddress = tokenCurrency.contractAddress, + spenderAddress = params.spenderAddress, + ).getOrElse { error -> + Timber.e(error, "Failed to create approval transaction") + return false + } + + return if (isFeeInTokenCurrency) { + createAndSendGaslessTransactionUseCase( + userWallet = params.userWallet, + transactionData = transactionData, + fee = feeExtended, + ) + } else { + sendTransactionUseCase( + txData = transactionData, + userWallet = params.userWallet, + network = tokenCurrency.network, + ) + }.fold( + ifLeft = { error -> + Timber.e("Failed to send approval transaction: $error") + false + }, + ifRight = { true }, + ) + } + + private fun getApprovalAmount(): BigDecimal? { + return if (uiState.value.approveType == ApproveType.LIMITED) { + params.amount.toBigDecimalOrNull() + } else { + null + } + } +} \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt new file mode 100644 index 0000000000..83bf60054d --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt @@ -0,0 +1,12 @@ +package com.tangem.features.approval.impl.model + +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +internal data class GiveApprovalUM( + val approveType: ApproveType, + val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), + val isApproveButtonEnabled: Boolean, + val isApproveLoading: Boolean, +) \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt new file mode 100644 index 0000000000..2d3386f430 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/GiveApprovalContent.kt @@ -0,0 +1,353 @@ +package com.tangem.features.approval.impl.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withLink +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.DpOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.window.PopupProperties +import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.containers.FooterContainer +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import com.tangem.common.ui.R as CommonUiR + +@Composable +@Suppress("LongParameterList") +internal fun GiveApprovalContent( + currency: String, + subtitle: TextReference, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, + walletInteractionIcon: Int?, + isApproveEnabled: Boolean, + isApproveLoading: Boolean, + onApproveClick: () -> Unit, + onCancelClick: () -> Unit, + onOpenLearnMoreAboutApproveClick: () -> Unit, + feeSelectorBlockComponent: FeeSelectorBlockComponent, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(color = TangemTheme.colors.background.secondary) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = subtitle.resolveReference(), + color = TangemTheme.colors.text.secondary, + style = TangemTheme.typography.body2, + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing24), + ) + + SpacerH16() + + ApprovalInfo( + currency = currency, + approveType = approveType, + approveItems = approveItems, + onChangeApproveType = onChangeApproveType, + onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick, + feeSelectorBlockComponent = feeSelectorBlockComponent, + ) + + SpacerH(height = TangemTheme.dimens.spacing20) + + PrimaryButtonIconEnd( + text = stringResourceSafe(id = CommonUiR.string.common_approve), + iconResId = walletInteractionIcon, + showProgress = isApproveLoading, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = onApproveClick, + enabled = isApproveEnabled, + ) + + SpacerH12() + + SecondaryButton( + text = stringResourceSafe(id = CommonUiR.string.common_cancel), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + onClick = onCancelClick, + ) + + SpacerH16() + } +} + +@Suppress("LongParameterList") +@Composable +private fun ApprovalInfo( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, + onOpenLearnMoreAboutApproveClick: () -> Unit, + feeSelectorBlockComponent: FeeSelectorBlockComponent, +) { + FooterContainer( + footer = annotatedReference { + append(stringResourceSafe(CommonUiR.string.swap_approve_description)) + append(" ") + withLink( + link = LinkAnnotation.Clickable( + tag = "APPROVE_TAG", + linkInteractionListener = { onOpenLearnMoreAboutApproveClick() }, + ), + block = { + appendColored( + text = stringResourceSafe(CommonUiR.string.common_learn_more), + color = TangemTheme.colors.text.accent, + ) + }, + ) + }, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + AmountItem( + currency = currency, + approveType = approveType, + onChangeApproveType = onChangeApproveType, + approveItems = approveItems, + ) + } + SpacerH16() + FooterContainer( + footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer), + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) { + feeSelectorBlockComponent.Content( + modifier = Modifier + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action), + ) + } +} + +@Composable +private fun AmountItem( + currency: String, + approveType: ApproveType, + approveItems: ImmutableList, + onChangeApproveType: (ApproveType) -> Unit, +) { + var isExpandSelector by remember { mutableStateOf(false) } + var amountSize by remember { mutableStateOf(IntSize.Zero) } + Box( + modifier = Modifier + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(), + onClick = { isExpandSelector = true }, + ), + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { amountSize = it } + .padding( + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle1, + maxLines = 1, + ) + SpacerWMax() + Text( + text = approveType.text.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + Icon( + painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)), + contentDescription = null, + tint = TangemTheme.colors.icon.informative, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing2), + ) + } + DropdownSelector( + isExpanded = isExpandSelector, + onDismiss = { isExpandSelector = false }, + onItemClick = { type -> + isExpandSelector = false + onChangeApproveType(type) + }, + items = approveItems, + selectedType = approveType, + amountSize = amountSize, + ) + } +} + +@Suppress("LongParameterList") +@Composable +private fun DropdownSelector( + isExpanded: Boolean, + onDismiss: () -> Unit, + onItemClick: (ApproveType) -> Unit, + items: ImmutableList, + selectedType: ApproveType, + amountSize: IntSize, +) { + var dropDownWidth by remember { mutableStateOf(IntSize.Zero) } + val offsetY = amountSize.height.times(-1) + val offsetX = amountSize.width - dropDownWidth.width + + MaterialTheme( + colorScheme = MaterialTheme.colorScheme.copy(surface = TangemTheme.colors.background.action), + shapes = MaterialTheme.shapes.copy(extraSmall = RoundedCornerShape(TangemTheme.dimens.radius16)), + ) { + DropdownMenu( + expanded = isExpanded, + onDismissRequest = onDismiss, + properties = PopupProperties(clippingEnabled = false), + offset = with(LocalDensity.current) { + DpOffset(x = offsetX.toDp(), y = offsetY.toDp()) + }, + modifier = Modifier + .wrapContentSize() + .background(TangemTheme.colors.background.action) + .onSizeChanged { dropDownWidth = it }, + ) { + items.forEach { item -> + val color = if (item == selectedType) TangemTheme.colors.icon.accent else Color.Transparent + + DropdownMenuItem( + modifier = Modifier.fillMaxWidth(), + text = { + Row { + Text( + text = when (item) { + ApproveType.LIMITED -> stringResourceSafe( + id = CommonUiR.string.give_permission_current_transaction, + ) + ApproveType.UNLIMITED -> stringResourceSafe( + id = CommonUiR.string.give_permission_unlimited, + ) + }, + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.body1, + maxLines = 1, + ) + SpacerWMax() + Icon( + painter = rememberVectorPainter( + image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24), + ), + tint = color, + contentDescription = null, + modifier = Modifier.padding(start = TangemTheme.dimens.size20), + ) + } + }, + onClick = { + onItemClick.invoke(item) + }, + ) + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun GiveApprovalContentPreview( + @PreviewParameter(GiveApprovalContentPreviewProvider::class) params: GiveApprovalPreviewParams, +) { + TangemThemePreview { + GiveApprovalContent( + currency = params.currency, + subtitle = params.subtitle, + approveType = params.approveType, + approveItems = params.approveItems, + onChangeApproveType = {}, + walletInteractionIcon = params.walletInteractionIcon, + isApproveEnabled = params.isApproveEnabled, + isApproveLoading = params.isApproveLoading, + onApproveClick = {}, + onCancelClick = {}, + onOpenLearnMoreAboutApproveClick = {}, + feeSelectorBlockComponent = PreviewFeeSelectorBlockComponent(), + ) + } +} + +private data class GiveApprovalPreviewParams( + val currency: String, + val subtitle: TextReference, + val approveType: ApproveType, + val approveItems: ImmutableList, + val walletInteractionIcon: Int?, + val isApproveEnabled: Boolean, + val isApproveLoading: Boolean, +) + +private class GiveApprovalContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + GiveApprovalPreviewParams( + currency = "USDT", + subtitle = stringReference("Allow this app to access your USDT"), + approveType = ApproveType.LIMITED, + approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED), + walletInteractionIcon = CommonUiR.drawable.ic_tangem_24, + isApproveEnabled = true, + isApproveLoading = false, + ), + GiveApprovalPreviewParams( + currency = "USDC", + subtitle = stringReference("Allow this app to access your USDC"), + approveType = ApproveType.UNLIMITED, + approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED), + walletInteractionIcon = CommonUiR.drawable.ic_tangem_24, + isApproveEnabled = false, + isApproveLoading = true, + ), + ) +} +// endregion \ No newline at end of file diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt new file mode 100644 index 0000000000..a08d796627 --- /dev/null +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/ui/PreviewFeeSelectorBlockComponent.kt @@ -0,0 +1,15 @@ +package com.tangem.features.approval.impl.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.features.send.v2.api.FeeSelectorBlockComponent +import com.tangem.features.send.v2.api.entity.FeeSelectorUM + +internal class PreviewFeeSelectorBlockComponent : FeeSelectorBlockComponent { + override fun updateState(feeSelectorUM: FeeSelectorUM) { + } + + @Composable + override fun Content(modifier: Modifier) { + } +} \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt index 597de0c5d3..855c07c8e2 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/analytics/CommonSendAnalyticEvents.kt @@ -211,6 +211,7 @@ sealed class CommonSendAnalyticEvents( const val SEND_CATEGORY = "Token / Send" const val SWAP_CATEGORY = "Swap" const val NFT_SEND_CATEGORY = "NFT" + const val APPROVE_CATEGORY = "Approve" } enum class SendScreenSource { @@ -226,5 +227,6 @@ sealed class CommonSendAnalyticEvents( SendWithSwap("Send&Swap"), WalletConnect("WalletConnect"), NFT("NFT"), + Approve("Approve"), } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 88b32b89bb..f77aab113a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -300,6 +300,9 @@ include(":features:token-recieve:impl") include(":features:yield-supply:api") include(":features:yield-supply:impl") +include(":features:approval:api") +include(":features:approval:impl") + include(":features:feed:api") include(":features:feed:impl") // endregion Feature modules From 5e10c943a043a74b6f611d17145ed3020503a4a8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 14:48:19 +0500 Subject: [PATCH 32/97] Updated on 2026-08-14 --- .../tap/di/domain/HotWalletDomainModule.kt | 9 ++++++ .../hotwallet/DefaultHotWalletRepository.kt | 7 ++--- .../CheckHotWalletUpgradeBannerUseCase.kt | 2 +- ...GetUpgradeBannerClosureTimestampUseCase.kt | 13 +++++++++ .../repository/HotWalletRepository.kt | 2 +- .../CheckHotWalletUpgradeBannerUseCaseTest.kt | 28 ++++++++++--------- .../domain/GetMultiWalletWarningsFactory.kt | 12 ++++++-- 7 files changed, 52 insertions(+), 21 deletions(-) create mode 100644 domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt index c99860711a..f3ee5bef10 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/HotWalletDomainModule.kt @@ -3,6 +3,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.IsHotWalletCreationSupported import com.tangem.domain.hotwallet.IsAccessCodeSimpleUseCase import com.tangem.domain.hotwallet.SetAccessCodeSkippedUseCase @@ -67,4 +68,12 @@ internal object HotWalletDomainModule { ): ShouldShowUpgradeHotWalletBannerUseCase { return ShouldShowUpgradeHotWalletBannerUseCase(hotWalletRepository) } + + @Provides + @Singleton + fun provideGetUpgradeBannerClosureTimestampUseCase( + hotWalletRepository: HotWalletRepository, + ): GetUpgradeBannerClosureTimestampUseCase { + return GetUpgradeBannerClosureTimestampUseCase(hotWalletRepository) + } } \ No newline at end of file diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index dbbe66db29..c112597c17 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -53,10 +53,9 @@ internal class DefaultHotWalletRepository( } } - override suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? { - return appPreferencesStore - .getObjectMapSync(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY)[userWalletId.stringValue] - } + override fun upgradeBannerClosureTimestamp(userWalletId: UserWalletId): Flow = appPreferencesStore + .getObjectMap(PreferencesKeys.UPGRADE_BANNER_CLOSURE_TIMESTAMP_KEY) + .map { it[userWalletId.stringValue] } override suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) { appPreferencesStore.editData { mutablePreferences -> diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt index 1038773b7c..28f7be6bd3 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCase.kt @@ -14,6 +14,7 @@ class CheckHotWalletUpgradeBannerUseCase( walletId: UserWalletId, hasBalance: Boolean, shouldShowUpgradeBanner: Boolean, + closureTimestamp: Long?, ): Either = try { val currentTime = System.currentTimeMillis() val creationTimestamp = hotWalletRepository.getWalletCreationTimestamp(walletId) @@ -27,7 +28,6 @@ class CheckHotWalletUpgradeBannerUseCase( creationTimestamp } - val closureTimestamp = hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) val hasHadFirstTopUp = hotWalletRepository.hasHadFirstTopUp(walletId) val daysSinceCreation = TimeUnit.MILLISECONDS.toDays(currentTime - creationTimestampActual) diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt new file mode 100644 index 0000000000..dbe9d2f2af --- /dev/null +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/GetUpgradeBannerClosureTimestampUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.hotwallet + +import com.tangem.domain.hotwallet.repository.HotWalletRepository +import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.coroutines.flow.Flow + +class GetUpgradeBannerClosureTimestampUseCase( + private val hotWalletRepository: HotWalletRepository, +) { + operator fun invoke(userWalletId: UserWalletId): Flow { + return hotWalletRepository.upgradeBannerClosureTimestamp(userWalletId) + } +} \ No newline at end of file diff --git a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt index 01229d8d3a..2d63b01cde 100644 --- a/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt +++ b/domain/hot-wallet/src/main/kotlin/com/tangem/domain/hotwallet/repository/HotWalletRepository.kt @@ -17,7 +17,7 @@ interface HotWalletRepository { suspend fun setShouldShowUpgradeBanner(userWalletId: UserWalletId, shouldShow: Boolean) - suspend fun getUpgradeBannerClosureTimestamp(userWalletId: UserWalletId): Long? + fun upgradeBannerClosureTimestamp(userWalletId: UserWalletId): Flow suspend fun setUpgradeBannerClosureTimestamp(userWalletId: UserWalletId, timestamp: Long?) diff --git a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt index 6e415efea4..447864f896 100644 --- a/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt +++ b/domain/hot-wallet/src/test/kotlin/com/tangem/domain/hotwallet/CheckHotWalletUpgradeBannerUseCaseTest.kt @@ -23,7 +23,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { @Test fun `GIVEN creation timestamp is null WHEN invoke THEN set timestamp and return false`() = runTest { coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns null - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -31,6 +30,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -42,7 +42,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN shouldShowUpgradeBanner is true and hasBalance WHEN invoke THEN return true`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -50,6 +49,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -60,7 +60,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN shouldShowUpgradeBanner is true WHEN invoke THEN return true regardless of balance`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -68,6 +67,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -79,7 +79,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -87,6 +86,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -99,7 +99,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -107,7 +106,8 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, - ) + closureTimestamp = closureTimestamp, + ) assertThat(result).isInstanceOf(Either.Right::class.java) assertThat((result as Either.Right).value).isFalse() @@ -117,7 +117,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN no flags set and no closure and 30 days since creation WHEN invoke THEN return true`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -125,6 +124,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -135,7 +135,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN no flags set but less than 30 days since creation WHEN invoke THEN return false`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(15) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -143,6 +142,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -154,7 +154,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(60) val closureTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns closureTimestamp coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -162,6 +161,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = closureTimestamp, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -172,7 +172,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN first top-up detected WHEN invoke THEN return false and mark session`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -180,6 +179,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -194,7 +194,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN first top-up detected this session WHEN invoke THEN return false`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns true @@ -202,6 +201,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) assertThat(result).isInstanceOf(Either.Right::class.java) @@ -212,7 +212,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN already had first top-up WHEN invoke with balance THEN do not set flags again`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(5) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns true every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -220,6 +219,7 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = true, shouldShowUpgradeBanner = true, + closureTimestamp = null, ) coVerify(exactly = 0) { hotWalletRepository.setHasHadFirstTopUp(any(), any()) } @@ -230,7 +230,6 @@ class CheckHotWalletUpgradeBannerUseCaseTest { fun `GIVEN multiple re-emissions with same state WHEN invoke THEN return same result`() = runTest { val creationTimestamp = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(31) coEvery { hotWalletRepository.getWalletCreationTimestamp(walletId) } returns creationTimestamp - coEvery { hotWalletRepository.getUpgradeBannerClosureTimestamp(walletId) } returns null coEvery { hotWalletRepository.hasHadFirstTopUp(walletId) } returns false every { hotWalletRepository.isFirstTopUpDetectedThisSession(walletId) } returns false @@ -238,16 +237,19 @@ class CheckHotWalletUpgradeBannerUseCaseTest { walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) val result2 = useCase( walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) val result3 = useCase( walletId = walletId, hasBalance = false, shouldShowUpgradeBanner = false, + closureTimestamp = null, ) assertThat((result1 as Either.Right).value).isTrue() 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 6218c6257a..a243ddef48 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 @@ -15,6 +15,7 @@ import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase +import com.tangem.domain.hotwallet.GetUpgradeBannerClosureTimestampUseCase import com.tangem.domain.hotwallet.ShouldShowUpgradeHotWalletBannerUseCase import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TotalFiatBalance @@ -62,10 +63,11 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val accountDependencies: AccountDependencies, private val getAccessCodeSkippedUseCase: GetAccessCodeSkippedUseCase, private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, + private val getUpgradeBannerClosureTimestampUseCase: GetUpgradeBannerClosureTimestampUseCase, private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, ) { - @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod") + @Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType") fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver @@ -105,8 +107,10 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), shouldShowUpgradeHotWalletBannerUseCase.invoke(userWallet.walletId) .distinctUntilChanged(), + getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) + .distinctUntilChanged(), ) { array -> array } - .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = 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 } @@ -119,6 +123,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldAccessCodeSkipped = array[6] as Boolean val shouldShowYieldPromo = array[7] as Boolean val shouldShowUpgradeBanner = array[8] as Boolean + val closureTimestamp = array[9] as? Long buildList { addUsedOutdatedDataNotification(totalFiatBalance) @@ -130,6 +135,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies = flattenCurrencies, clickIntents = clickIntents, shouldShowUpgradeBanner = shouldShowUpgradeBanner, + closureTimestamp = closureTimestamp, ) addFinishWalletActivationNotification( @@ -469,6 +475,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flattenCurrencies: Lce>, clickIntents: WalletClickIntents, shouldShowUpgradeBanner: Boolean, + closureTimestamp: Long?, ) { if (userWallet !is UserWallet.Hot) return @@ -479,6 +486,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( walletId = userWallet.walletId, hasBalance = hasBalance, shouldShowUpgradeBanner = shouldShowUpgradeBanner, + closureTimestamp = closureTimestamp, ).getOrNull() ?: return addIf( From 4d1902af26d05519f484e64939e54b48df19bc7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 13:05:29 +0100 Subject: [PATCH 33/97] Updated on 2026-08-14 --- .../ui/ds/opportunities/OpportunitiesBG.kt | 244 ++++++++++++++++++ .../features/feed/ui/earn/EarnContent.kt | 61 +---- .../feed/ui/earn/components/MostlyUsedCard.kt | 198 ++++++++++++++ 3 files changed, 447 insertions(+), 56 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt new file mode 100644 index 0000000000..f0059fff8b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt @@ -0,0 +1,244 @@ +package com.tangem.core.ui.ds.opportunities + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.draw.innerShadow +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.shadow.Shadow +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemThemePreview +import dev.chrisbanes.haze.HazeStyle + +/** + * Container that draws a blurred background (from URL or solid color) and + * applies a semi‑transparent overlay on top of it, then renders foreground content. + * + * Figma https://www.figma.com/design/X0IMgSMOT5rWWgiSIeZQwC/Bottom-sheet--Redesign-?node-id=3360-64755&m=dev + * + * @param icon Background configuration (URL, solid color or none). + * @param modifier Modifier applied to the outer container. + * @param content Foreground content rendered on top of the overlay. + * @param shape Shape used for inner shadow and border (e.g. rounded corners). + */ +@Suppress("MagicNumber") +@Composable +fun OpportunitiesBG( + icon: TangemIconUM, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(16.dp), + content: @Composable BoxScope.() -> Unit, +) { + val isInDarkTheme = LocalIsInDarkTheme.current + val overlayColor = remember(isInDarkTheme) { + if (isInDarkTheme) { + Color(OVERLAY_DARK) + } else { + Color.White + } + } + + Box(modifier = modifier) { + BackgroundLayer(icon = icon) + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(shape) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 30.dp, + spread = 5.dp, + color = Color(INNER_SHADOW_COLOR_START).copy(alpha = .3f), + offset = DpOffset(0.dp, 0.dp), + ), + ) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 100.dp, + spread = (-39).dp, + color = Color(INNER_SHADOW_COLOR_END).copy(.3f), + offset = DpOffset(0.dp, (-56).dp), + ), + ) + .innerShadow( + shape = shape, + shadow = Shadow( + radius = 40.dp, + spread = (-19).dp, + color = Color(INNER_SHADOW_COLOR_END).copy(alpha = .25f), + offset = DpOffset(0.dp, (-16).dp), + ), + ) + .drawWithContent { + drawRect(color = overlayColor.copy(alpha = .7f)) + drawContent() + val outline = shape.createOutline(size, layoutDirection, this) + drawOutline(outline, Color(BORDER_COLOR).copy(alpha = .1f), style = Stroke(width = 1.dp.toPx())) + }, + content = content, + ) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp) { + when (icon) { + is TangemIconUM.Currency -> CurrencyIconBackgroundLayer(icon.currencyIconState, blurRadius) + is TangemIconUM.Icon -> SolidColorBackground(icon.tintReference(), blurRadius) + is TangemIconUM.Ident -> Unit + is TangemIconUM.Image -> ResBackground(icon.imageRes, blurRadius) + } +} + +@Suppress("CyclomaticComplexMethod") +@Composable +private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurRadius: Dp) { + when (state) { + is CurrencyIconState.CryptoPortfolio.Icon -> SolidColorBackground( + color = state.color, + blurRadius = blurRadius, + ) + is CurrencyIconState.CryptoPortfolio.Letter -> SolidColorBackground( + color = state.color, + blurRadius = blurRadius, + ) + is CurrencyIconState.CustomTokenIcon -> SolidColorBackground( + color = state.background, + blurRadius = blurRadius, + ) + is CurrencyIconState.Empty -> ResBackground(res = state.resId, blurRadius = blurRadius) + is CurrencyIconState.CoinIcon -> { + state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + ResBackground(res = state.fallbackResId, blurRadius = blurRadius) + } + } + is CurrencyIconState.FiatIcon -> state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + ResBackground(res = state.fallbackResId, blurRadius = blurRadius) + } + is CurrencyIconState.TokenIcon -> state.url?.let { + UrlBackground(imageUrl = state.url, blurRadius = blurRadius) + } ?: run { + SolidColorBackground( + color = state.fallbackBackground, + blurRadius = blurRadius, + ) + } + CurrencyIconState.Loading -> Unit + CurrencyIconState.Locked -> Unit + } +} + +@Composable +private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) { + val context = LocalContext.current + + val imageRequest = remember(imageUrl) { + if (imageUrl.isNullOrBlank()) { + null + } else { + ImageRequest.Builder(context) + .data(imageUrl) + .crossfade(true) + .build() + } + } + + if (imageRequest != null) { + AsyncImage( + model = imageRequest, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .scale(SCALE_FACTOR) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) + } +} + +@Composable +private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) { + Image( + painter = painterResource(res), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .matchParentSize() + .scale(SCALE_FACTOR) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) +} + +@Composable +private fun BoxScope.SolidColorBackground(color: Color, blurRadius: Dp) { + Box( + modifier = Modifier + .matchParentSize() + .background(color = color) + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + ) +} + +private const val SCALE_FACTOR = 1.5f +private const val INNER_SHADOW_COLOR_START = 0x00000000 +private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF + +private const val BORDER_COLOR = 0xF0F0F0 +private const val OVERLAY_DARK = 0x141414 + +// region Previews + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun OpportunitiesBGPreview() { + TangemThemePreview { + OpportunitiesBG( + modifier = Modifier.size(400.dp), + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_solana_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + content = {}, + ) + } +} + +// endregion \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index 4939a58ede..fb0a94723b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -3,7 +3,6 @@ package com.tangem.features.feed.ui.earn import android.content.res.Configuration import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material3.Icon @@ -14,21 +13,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.R -import com.tangem.core.ui.components.* +import com.tangem.core.ui.components.SmallButtonShimmer +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerWMax +import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.list.InfiniteListHandler import com.tangem.core.ui.decorations.roundedShapeItemDecoration @@ -39,6 +38,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.earn.components.EarnItemPlaceholder import com.tangem.features.feed.ui.earn.components.EarnListItem +import com.tangem.features.feed.ui.earn.components.MostlyUsedCard import com.tangem.features.feed.ui.earn.components.MostlyUsedPlaceholder import com.tangem.features.feed.ui.earn.state.* import kotlinx.collections.immutable.persistentListOf @@ -161,57 +161,6 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { } } -@Composable -private fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .width(148.dp) - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .clickable(onClick = onClick) - .padding(12.dp), - ) { - CurrencyIcon( - modifier = Modifier.size(32.dp), - state = item.currencyIconState, - shouldDisplayNetwork = true, - networkBadgeSize = 12.dp, - networkBadgeBackground = TangemTheme.colors.background.action, - ) - - SpacerH(8.dp) - - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(weight = 1f, fill = false), - text = item.tokenName.resolveReference(), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.subtitle2, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - SpacerW(4.dp) - Text( - text = item.symbol.resolveReference(), - color = TangemTheme.colors.text.tertiary, - style = TangemTheme.typography.subtitle2, - maxLines = 1, - ) - } - - SpacerH(2.dp) - - Text( - text = item.earnValue.resolveReference(), - color = TangemTheme.colors.text.accent, - style = TangemTheme.typography.caption1, - maxLines = 1, - ) - } -} - @Composable private fun BestOpportunitiesFilters( state: EarnBestOpportunitiesUM, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt new file mode 100644 index 0000000000..21fdbcc0a4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt @@ -0,0 +1,198 @@ +package com.tangem.features.feed.ui.earn.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.ds.opportunities.OpportunitiesBG +import com.tangem.core.ui.components.currency.icon.CurrencyIcon +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.* +import com.tangem.features.feed.ui.earn.state.EarnListItemUM + +@Composable +internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + val isRedesignEnabled = LocalRedesignEnabled.current + + if (isRedesignEnabled) { + MostlyUsedCardV2( + modifier = modifier, + item = item, + onClick = onClick, + ) + } else { + MostlyUsedCardV1( + modifier = modifier, + item = item, + onClick = onClick, + ) + } +} + +@Composable +private fun MostlyUsedCardV2(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + OpportunitiesBG( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .clickable(onClick = onClick), + icon = TangemIconUM.Currency(item.currencyIconState), + ) { + Column(modifier = Modifier.padding(12.dp)) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(22.dp) + + Row( + verticalAlignment = Alignment.Bottom, + ) { + Text( + modifier = Modifier.weight(weight = 1f, fill = false), + text = item.tokenName.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography2.bodySemibold16, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + SpacerW(4.dp) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors2.text.status.positive, + style = TangemTheme.typography2.captionSemibold12, + maxLines = 1, + ) + } + } +} + +@Composable +private fun MostlyUsedCardV1(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .width(148.dp) + .clip(TangemTheme.shapes.roundedCornersXMedium) + .background(TangemTheme.colors.background.action) + .clickable(onClick = onClick) + .padding(12.dp), + ) { + CurrencyIcon( + modifier = Modifier.size(32.dp), + state = item.currencyIconState, + shouldDisplayNetwork = true, + networkBadgeSize = 12.dp, + networkBadgeBackground = TangemTheme.colors.background.action, + ) + + SpacerH(8.dp) + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + modifier = Modifier.weight(weight = 1f, fill = false), + text = item.tokenName.resolveReference(), + color = TangemTheme.colors.text.primary1, + style = TangemTheme.typography.subtitle2, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + SpacerW(4.dp) + Text( + text = item.symbol.resolveReference(), + color = TangemTheme.colors.text.tertiary, + style = TangemTheme.typography.subtitle2, + maxLines = 1, + ) + } + + SpacerH(2.dp) + + Text( + text = item.earnValue.resolveReference(), + color = TangemTheme.colors.text.accent, + style = TangemTheme.typography.caption1, + maxLines = 1, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EarnListItemPreviewV1() { + TangemThemePreview { + MostlyUsedCardV1( + EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("USDT"), + tokenName = stringReference("Tether"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ), + onClick = {}, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun EarnListItemPreviewV2() { + TangemThemePreviewRedesign { + MostlyUsedCardV2( + EarnListItemUM( + network = stringReference("Ethereum"), + symbol = stringReference("USDT"), + tokenName = stringReference("Tether"), + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_eth_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + earnValue = stringReference("APY 6.54%"), + earnType = stringReference("Yield"), + onItemClick = {}, + ), + onClick = {}, + ) + } +} \ No newline at end of file From bf4a7a9e4e75fca8d527ecfbf356fce6faa19868 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 16:25:40 +0400 Subject: [PATCH 34/97] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 41 ++ .../GeneratedEnvironmentConfigConverter.kt | 181 +++++++ gradle/dependencies.toml | 2 + plugins/configuration/build.gradle.kts | 9 + .../EnvironmentConfigGenerator.kt | 165 ++++++ .../plugin/configuration/model/BuildType.kt | 25 +- .../EnvironmentConfigGeneratorTest.kt | 473 ++++++++++++++++++ 7 files changed, 891 insertions(+), 5 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt create mode 100644 plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt create mode 100644 plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 8df1d5a9f9..7e80091db4 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -1,4 +1,6 @@ +import com.tangem.plugin.configuration.configurations.EnvironmentConfigGenerator import com.tangem.plugin.configuration.configurations.extension.kaptForObfuscatingVariants +import com.tangem.plugin.configuration.model.BuildType plugins { alias(deps.plugins.android.library) @@ -10,14 +12,53 @@ plugins { id("configuration") } +abstract class GenerateEnvironmentConfigTask : DefaultTask() { + + @get:InputFile + abstract val configFile: RegularFileProperty + + @get:OutputDirectory + abstract val outputDir: DirectoryProperty + + @TaskAction + fun generate() { + val input = configFile.get().asFile + require(input.exists()) { "Config file not found: ${input.absolutePath}" } + logger.lifecycle("Generating EnvironmentConfig from ${input.name}") + EnvironmentConfigGenerator.generate(input, outputDir.get().asFile) + } +} + android { namespace = "com.tangem.datasource" + sourceSets["main"].java.srcDir(layout.buildDirectory.dir("generated/source/environment-config")) + room { schemaDirectory("$projectDir/schemas") } } +androidComponents { + onVariants { variant -> + val buildType = BuildType.values().firstOrNull { it.id == variant.buildType } ?: BuildType.Debug + val configFile = rootProject.file( + "app/src/main/assets/tangem-app-config/config_${buildType.environment}.json", + ) + + tasks.register( + "generateEnvironmentConfig${variant.name.replaceFirstChar { it.uppercaseChar() }}", + ) { + this.configFile.set(configFile) + outputDir.set(layout.buildDirectory.dir("generated/source/environment-config")) + } + } +} + +tasks.named("preBuild") { + dependsOn(tasks.matching { it.name.startsWith("generateEnvironmentConfig") }) +} + tasks.withType().configureEach { useJUnitPlatform() } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt new file mode 100644 index 0000000000..56df165e72 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/GeneratedEnvironmentConfigConverter.kt @@ -0,0 +1,181 @@ +package com.tangem.datasource.local.config.environment.converter + +import com.tangem.blockchain.common.* +import com.tangem.datasource.local.config.environment.EnvironmentConfig +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.AppsFlyer +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.DevExpress +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.Express +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.GetBlockAccessTokens +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.P2pApiKey +import com.tangem.datasource.local.config.environment.generated.GeneratedEnvironmentConfig.TonCenterApiKey +import com.tangem.datasource.local.config.environment.models.ExpressModel +import com.tangem.datasource.local.config.environment.models.P2PKeys + +/** + * Converts [GeneratedEnvironmentConfig] to [EnvironmentConfig] + * + * This converter maps the auto-generated config (from JSON) to the domain model. + * The generated config has nested objects that mirror the JSON structure. + */ +internal object GeneratedEnvironmentConfigConverter { + + fun convert(): EnvironmentConfig { + return EnvironmentConfig( + moonPayApiKey = GeneratedEnvironmentConfig.moonPayApiKey, + moonPayApiSecretKey = GeneratedEnvironmentConfig.moonPayApiSecretKey, + mercuryoWidgetId = GeneratedEnvironmentConfig.mercuryoWidgetId, + mercuryoSecret = GeneratedEnvironmentConfig.mercuryoSecret, + blockchainSdkConfig = createBlockchainSdkConfig(), + amplitudeApiKey = GeneratedEnvironmentConfig.amplitudeApiKey, + appsFlyerApiKey = AppsFlyer.appsFlyerDevKey, + appsAppId = AppsFlyer.appsFlyerAppID, + walletConnectProjectId = GeneratedEnvironmentConfig.walletConnectProjectId, + express = createExpressModel( + apiKey = Express.apiKey, + signVerifierPublicKey = Express.signVerifierPublicKey, + ), + devExpress = createExpressModel( + apiKey = DevExpress.apiKey, + signVerifierPublicKey = DevExpress.signVerifierPublicKey, + ), + stakeKitApiKey = GeneratedEnvironmentConfig.stakeKitApiKey, + p2pApiKey = createP2PKeys(), + blockAidApiKey = GeneratedEnvironmentConfig.blockaidApiKey, + tangemApiKey = GeneratedEnvironmentConfig.tangemApiKey, + tangemApiKeyDev = GeneratedEnvironmentConfig.tangemApiKeyDev, + tangemApiKeyStage = GeneratedEnvironmentConfig.tangemApiKeyStage, + yieldModuleApiKey = GeneratedEnvironmentConfig.yieldModuleApiKey, + yieldModuleApiKeyDev = GeneratedEnvironmentConfig.yieldModuleApiKeyDev, + bffStaticToken = GeneratedEnvironmentConfig.bffStaticToken, + bffStaticTokenDev = GeneratedEnvironmentConfig.bffStaticTokenDev, + gaslessTxApiKeyDev = GeneratedEnvironmentConfig.gaslessTxApiKeyDev, + gaslessTxApiKey = GeneratedEnvironmentConfig.gaslessTxApiKey, + ) + } + + private fun createExpressModel(apiKey: String?, signVerifierPublicKey: String?): ExpressModel? { + return if (!apiKey.isNullOrEmpty() && !signVerifierPublicKey.isNullOrEmpty()) { + ExpressModel(apiKey = apiKey, signVerifierPublicKey = signVerifierPublicKey) + } else { + null + } + } + + private fun createP2PKeys(): P2PKeys? { + val mainnet = P2pApiKey.mainnet + val hoodi = P2pApiKey.hoodi + return if (mainnet.isNotEmpty() && hoodi.isNotEmpty()) { + P2PKeys(mainnet = mainnet, hoodi = hoodi) + } else { + null + } + } + + private fun createBlockchainSdkConfig(): BlockchainSdkConfig { + return BlockchainSdkConfig( + blockchairCredentials = BlockchairCredentials( + apiKey = GeneratedEnvironmentConfig.blockchairApiKeys, + authToken = GeneratedEnvironmentConfig.blockchairAuthorizationToken, + ), + blockcypherTokens = GeneratedEnvironmentConfig.blockcypherTokens.toSet(), + quickNodeSolanaCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodeApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodeSubdomain, + ), + quickNodeBscCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.bscQuiknodeApiKey, + subdomain = GeneratedEnvironmentConfig.bscQuiknodeSubdomain, + ), + quickNodePlasmaCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodePlasmaApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodePlasmaSubdomain, + ), + quickNodeMonadCredentials = QuickNodeCredentials( + apiKey = GeneratedEnvironmentConfig.quiknodeMonadApiKey, + subdomain = GeneratedEnvironmentConfig.quiknodeMonadSubdomain, + ), + infuraProjectId = GeneratedEnvironmentConfig.infuraProjectId, + tronGridApiKey = GeneratedEnvironmentConfig.tronGridApiKey, + nowNodeCredentials = NowNodeCredentials(apiKey = GeneratedEnvironmentConfig.nowNodesApiKey), + getBlockCredentials = createGetBlockCredentials(), + kaspaSecondaryApiUrl = GeneratedEnvironmentConfig.kaspaSecondaryApiUrl, + tonCenterCredentials = TonCenterCredentials( + mainnetApiKey = TonCenterApiKey.mainnet, + testnetApiKey = TonCenterApiKey.testnet, + ), + chiaFireAcademyApiKey = GeneratedEnvironmentConfig.chiaFireAcademyApiKey, + chiaTangemApiKey = GeneratedEnvironmentConfig.chiaTangemApiKey, + hederaArkhiaApiKey = GeneratedEnvironmentConfig.hederaArkhiaKey, + polygonScanApiKey = GeneratedEnvironmentConfig.polygonScanApiKey, + bittensorDwellirApiKey = GeneratedEnvironmentConfig.bittensorDwellirKey, + bittensorOnfinalityApiKey = GeneratedEnvironmentConfig.bittensorOnfinalityKey, + dwellirApiKey = GeneratedEnvironmentConfig.dwellirApiKey, + koinosProApiKey = GeneratedEnvironmentConfig.koinosProApiKey, + alephiumApiKey = GeneratedEnvironmentConfig.alephiumTangemApiKey, + moralisApiKey = GeneratedEnvironmentConfig.moralisApiKey, + etherscanApiKey = GeneratedEnvironmentConfig.etherscanApiKey, + blinkApiKey = GeneratedEnvironmentConfig.blinkApiKey, + tatumApiKey = GeneratedEnvironmentConfig.tatumApiKey, + ) + } + + private fun createGetBlockCredentials(): GetBlockCredentials { + return GetBlockCredentials( + xrp = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xrp.jsonRpc), + cardano = GetBlockAccessToken(rosetta = GetBlockAccessTokens.Cardano.rosetta), + avalanche = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Avalanche.jsonRpc), + eth = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ethereum.jsonRpc), + etc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.EthereumClassic.jsonRpc), + fantom = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Fantom.jsonRpc), + rsk = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Rsk.jsonRpc), + bsc = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Bsc.jsonRpc), + polygon = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polygon.jsonRpc), + gnosis = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Xdai.jsonRpc), + cronos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Cronos.jsonRpc), + solana = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Solana.jsonRpc), + ton = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Ton.jsonRpc), + tron = GetBlockAccessToken(rest = GetBlockAccessTokens.Tron.rest), + cosmos = GetBlockAccessToken(rest = GetBlockAccessTokens.CosmosHub.rest), + near = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Near.jsonRpc), + aptos = GetBlockAccessToken(rest = GetBlockAccessTokens.Aptos.rest), + dogecoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Dogecoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Dogecoin.blockBookRest, + ), + litecoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Litecoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Litecoin.blockBookRest, + ), + dash = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Dash.jsonRpc, + blockBookRest = GetBlockAccessTokens.Dash.blockBookRest, + ), + bitcoin = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.Bitcoin.jsonRpc, + blockBookRest = GetBlockAccessTokens.Bitcoin.blockBookRest, + ), + algorand = GetBlockAccessToken(rest = GetBlockAccessTokens.Algorand.rest), + zkSyncEra = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Zksync.jsonRpc), + polygonZkEvm = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.PolygonZkevm.jsonRpc), + base = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Base.jsonRpc), + blast = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Blast.jsonRpc), + filecoin = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Filecoin.jsonRpc), + arbitrum = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.ArbitrumOne.jsonRpc), + bitcoinCash = GetBlockAccessToken( + jsonRpc = GetBlockAccessTokens.BitcoinCash.jsonRpc, + blockBookRest = GetBlockAccessTokens.BitcoinCash.blockBookRest, + ), + kusama = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Kusama.jsonRpc), + moonbeam = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Moonbeam.jsonRpc), + optimism = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Optimism.jsonRpc), + polkadot = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Polkadot.jsonRpc), + shibarium = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Shibarium.jsonRpc), + sui = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Sui.jsonRpc), + telos = GetBlockAccessToken(jsonRpc = GetBlockAccessTokens.Telos.jsonRpc), + tezos = GetBlockAccessToken(rest = GetBlockAccessTokens.Tezos.rest), + monad = GetBlockAccessToken(rest = GetBlockAccessTokens.Monad.rest), + stellar = GetBlockAccessToken(rest = GetBlockAccessTokens.Stellar.rest), + ) + } +} \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 4c87fdffe7..68f64e61cf 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -102,6 +102,7 @@ markdownComposeView = "0.5.4" usedesk = "4.4.0" sumsub = "1.38.0" haze = "1.7.1" +kotlinpoet = "1.18.1" # endregion Other libraries # region Tools @@ -149,6 +150,7 @@ agconnect = { id = "com.huawei.agconnect", version.ref = "agconnect" } gradle-android = { module = "com.android.tools.build:gradle", version.ref = "androidGradlePlugin" } gradle-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradle-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +gradle-kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } # end region Classpath # region AndroidX diff --git a/plugins/configuration/build.gradle.kts b/plugins/configuration/build.gradle.kts index a8bc7da8a0..4d9ccbce70 100644 --- a/plugins/configuration/build.gradle.kts +++ b/plugins/configuration/build.gradle.kts @@ -17,6 +17,15 @@ dependencies { implementation(deps.gradle.kotlin) implementation(deps.gradle.android) implementation(deps.gradle.detekt) + implementation(deps.gradle.kotlinpoet) + implementation(deps.kotlin.serialization) + + testImplementation(deps.test.junit5) + testImplementation(deps.test.truth) +} + +tasks.withType { + useJUnitPlatform() } gradlePlugin { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt new file mode 100644 index 0000000000..89206fda87 --- /dev/null +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt @@ -0,0 +1,165 @@ +package com.tangem.plugin.configuration.configurations + +import com.squareup.kotlinpoet.* +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import kotlinx.serialization.json.* +import java.io.File + +/** + * Generator for environment configuration Kotlin object from JSON file. + * Automatically parses JSON structure and generates corresponding Kotlin code. + * +[REDACTED_AUTHOR] + */ +object EnvironmentConfigGenerator { + + private const val PACKAGE_NAME = "com.tangem.datasource.local.config.environment.generated" + private const val CLASS_NAME = "GeneratedEnvironmentConfig" + + /** + * Generates GeneratedEnvironmentConfig object from JSON file. + * + * @param inputFile JSON configuration file + * @param outputDir Output directory for generated Kotlin file + */ + fun generate(inputFile: File, outputDir: File) { + val jsonText = inputFile.readText() + val json = Json.parseToJsonElement(jsonText).jsonObject + + val objectBuilder = TypeSpec.objectBuilder(CLASS_NAME) + .addKdoc("Generated from ${inputFile.name}\nAuto-generated - do not edit manually.") + + // Iterate over all JSON keys and generate properties + json.entries.forEach { (key, value) -> + addPropertyFromJsonValue(objectBuilder, key, value) + } + + val fileSpec = FileSpec.builder(PACKAGE_NAME, CLASS_NAME) + .indent(" ") // Use 4 spaces for indentation + .addType(objectBuilder.build()) + .build() + + outputDir.mkdirs() + fileSpec.writeTo(outputDir) + + // Post-process generated file + val generatedFile = File(outputDir, PACKAGE_NAME.replace('.', '/') + "/$CLASS_NAME.kt") + if (generatedFile.exists()) { + val content = generatedFile.readText() + val fixedContent = content + // Add suppress annotation at file level + .replaceFirst( + "package $PACKAGE_NAME", + "@file:Suppress(\n" + + " \"MaximumLineLength\",\n" + + " \"MaxLineLength\",\n" + + " \"Indentation\",\n" + + ")\n\npackage $PACKAGE_NAME" + ) + // Remove redundant public modifiers + .replace("public object ", "object ") + .replace("public val ", "val ") + .replace("public const val ", "const val ") + generatedFile.writeText(fixedContent) + } + } + + /** + * Adds a property to the TypeSpec based on the JSON value type + */ + private fun addPropertyFromJsonValue(builder: TypeSpec.Builder, name: String, value: JsonElement) { + when (value) { + is JsonPrimitive -> { + when { + value.isString -> { + val stringValue = value.content + val isNullable = stringValue.isEmpty() + val propertySpec = PropertySpec.builder(name, STRING.copy(nullable = isNullable)) + .initializer(if (isNullable) "null" else "%S", stringValue) + + // Add const modifier for non-nullable strings + if (!isNullable) { + propertySpec.addModifiers(KModifier.CONST) + } + + builder.addProperty(propertySpec.build()) + } + value.booleanOrNull != null -> { + builder.addProperty( + PropertySpec.builder(name, BOOLEAN) + .addModifiers(KModifier.CONST) + .initializer("%L", value.boolean) + .build() + ) + } + value.longOrNull != null -> { + builder.addProperty( + PropertySpec.builder(name, LONG) + .addModifiers(KModifier.CONST) + .initializer("%L", value.long) + .build() + ) + } + value.doubleOrNull != null -> { + builder.addProperty( + PropertySpec.builder(name, DOUBLE) + .addModifiers(KModifier.CONST) + .initializer("%L", value.double) + .build() + ) + } + else -> { + // Null value + builder.addProperty( + PropertySpec.builder(name, STRING.copy(nullable = true)) + .initializer("null") + .build() + ) + } + } + } + is JsonArray -> { + val listType = LIST.parameterizedBy(STRING) + val values = value.map { it.jsonPrimitive.content } + builder.addProperty( + PropertySpec.builder(name, listType) + .initializer( + CodeBlock.builder() + .add("listOf(\n") + .apply { + values.forEach { v -> + add(" %S,\n", v) + } + } + .add(")") + .build() + ) + .build() + ) + } + is JsonObject -> { + // Generate nested object with proper naming (convert dashes to camelCase) + val nestedClassName = name.toPascalCase() + val nestedObjectBuilder = TypeSpec.objectBuilder(nestedClassName) + + value.entries.forEach { (nestedKey, nestedValue) -> + addPropertyFromJsonValue(nestedObjectBuilder, nestedKey, nestedValue) + } + + builder.addType(nestedObjectBuilder.build()) + } + } + } + + /** + * Converts a string to PascalCase, handling dashes and underscores. + * Examples: "cosmos-hub" -> "CosmosHub", "polygon-zkevm" -> "PolygonZkevm" + */ + private fun String.toPascalCase(): String { + return this.split("-", "_") + .filter { it.isNotEmpty() } + .joinToString("") { part -> + part.replaceFirstChar { it.uppercase() } + } + } +} diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt index 0ab5e8f747..c15761c0d9 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/model/BuildType.kt @@ -1,11 +1,11 @@ package com.tangem.plugin.configuration.model -internal enum class BuildType( +enum class BuildType( val id: String, - val appIdSuffix: String? = null, - val versionSuffix: String? = null, - val obfuscating: Boolean = false, - val configFields: List, + internal val appIdSuffix: String? = null, + internal val versionSuffix: String? = null, + internal val obfuscating: Boolean = false, + internal val configFields: List, ) { /** @@ -117,4 +117,19 @@ internal enum class BuildType( BuildConfigField.ABTestsEnabled(isEnabled = false), ), ), + ; + + /** Returns the environment value (dev/prod) for this build type */ + val environment: String + get() { + val environmentField = configFields + .filterIsInstance() + .firstOrNull() + + requireNotNull(environmentField) { + "BuildType '$id' must have a BuildConfigField.Environment in configFields" + } + + return environmentField.value.removeSurrounding("\"") + } } \ No newline at end of file diff --git a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt new file mode 100644 index 0000000000..bfcccb71a8 --- /dev/null +++ b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt @@ -0,0 +1,473 @@ +package com.tangem.plugin.configuration.configurations + +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File + +/** + * Tests for [EnvironmentConfigGenerator] covering JSON parsing edge cases. + */ +class EnvironmentConfigGeneratorTest { + + @TempDir + lateinit var tempDir: File + + private lateinit var outputDir: File + + @BeforeEach + fun setup() { + outputDir = File(tempDir, "output") + } + + @Test + fun `generate handles string values correctly`() { + // Arrange + val json = """ + { + "apiKey": "test-api-key", + "baseUrl": "https://example.com" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("""const val apiKey: String = "test-api-key"""") + assertThat(generatedCode).contains("""const val baseUrl: String = "https://example.com"""") + } + + @Test + fun `generate handles empty string as nullable`() { + // Arrange + val json = """ + { + "emptyValue": "" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val emptyValue: String? = null") + } + + @Test + fun `generate handles null values`() { + // Arrange + val json = """ + { + "nullValue": null + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val nullValue: String? = null") + } + + @Test + fun `generate handles boolean values`() { + // Arrange + val json = """ + { + "isEnabled": true, + "isDisabled": false + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val isEnabled: Boolean = true") + assertThat(generatedCode).contains("const val isDisabled: Boolean = false") + } + + @Test + fun `generate handles integer values as Long`() { + // Arrange + val json = """ + { + "count": 42, + "negativeNumber": -100, + "largeNumber": 9223372036854775807 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val count: Long = 42") + assertThat(generatedCode).contains("const val negativeNumber: Long = -100") + // KotlinPoet formats large numbers with underscores + assertThat(generatedCode).contains("const val largeNumber: Long = 9_223_372_036_854_775_807") + } + + @Test + fun `generate handles double values`() { + // Arrange + val json = """ + { + "ratio": 3.14, + "negativeDouble": -2.5 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val ratio: Double = 3.14") + assertThat(generatedCode).contains("const val negativeDouble: Double = -2.5") + } + + @Test + fun `generate handles string arrays`() { + // Arrange + val json = """ + { + "items": ["one", "two", "three"] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val items: List = listOf(") + assertThat(generatedCode).contains(""""one",""") + assertThat(generatedCode).contains(""""two",""") + assertThat(generatedCode).contains(""""three",""") + } + + @Test + fun `generate handles empty arrays`() { + // Arrange + val json = """ + { + "emptyList": [] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val emptyList: List = listOf(") + } + + @Test + fun `generate handles nested objects`() { + // Arrange + val json = """ + { + "database": { + "host": "localhost", + "port": 5432 + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Database {") + assertThat(generatedCode).contains("""const val host: String = "localhost"""") + // KotlinPoet formats numbers >= 1000 with underscores + assertThat(generatedCode).contains("const val port: Long = 5_432") + } + + @Test + fun `generate handles deeply nested objects`() { + // Arrange + val json = """ + { + "level1": { + "level2": { + "level3": { + "deepValue": "deep" + } + } + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Level1 {") + assertThat(generatedCode).contains("object Level2 {") + assertThat(generatedCode).contains("object Level3 {") + assertThat(generatedCode).contains("""const val deepValue: String = "deep"""") + } + + @Test + fun `generate converts dash-separated names to PascalCase`() { + // Arrange + val json = """ + { + "cosmos-hub": { + "chainId": "cosmoshub-4" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object CosmosHub {") + } + + @Test + fun `generate converts underscore-separated names to PascalCase`() { + // Arrange + val json = """ + { + "api_config": { + "timeout": 30 + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object ApiConfig {") + } + + @Test + fun `generate handles consecutive dashes in names`() { + // Arrange + val json = """ + { + "cosmos--hub": { + "testValue": "test" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object CosmosHub {") + } + + @Test + fun `generate handles trailing dash in names`() { + // Arrange + val json = """ + { + "config-": { + "testValue": "test" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object Config {") + } + + @Test + fun `generate handles special characters in string values`() { + // Arrange + val json = """ + { + "query": "SELECT * FROM users WHERE name = 'John'", + "path": "C:\\Users\\test", + "newline": "line1\nline2", + "unicode": "Hello 世界" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("const val query: String") + assertThat(generatedCode).contains("const val path: String") + assertThat(generatedCode).contains("const val newline: String") + assertThat(generatedCode).contains("const val unicode: String") + } + + @Test + fun `generate handles arrays with special characters`() { + // Arrange + val json = """ + { + "urls": [ + "https://api.example.com/v1", + "https://api.example.com/v2?key=value&other=1" + ] + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("val urls: List = listOf(") + assertThat(generatedCode).contains(""""https://api.example.com/v1",""") + } + + @Test + fun `generate adds file suppress annotations`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("@file:Suppress(") + assertThat(generatedCode).contains(""""MaximumLineLength"""") + assertThat(generatedCode).contains(""""MaxLineLength"""") + assertThat(generatedCode).contains(""""Indentation"""") + } + + @Test + fun `generate creates proper package declaration`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("package com.tangem.datasource.local.config.environment.generated") + } + + @Test + fun `generate creates object with correct name`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object GeneratedEnvironmentConfig {") + } + + @Test + fun `generate adds kdoc with source file reference`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("Generated from") + assertThat(generatedCode).contains("Auto-generated - do not edit manually") + } + + @Test + fun `generate removes public modifiers`() { + // Arrange + val json = """ + { + "key": "value" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).doesNotContain("public object") + assertThat(generatedCode).doesNotContain("public val") + assertThat(generatedCode).doesNotContain("public const val") + } + + @Test + fun `generate handles complex real-world config`() { + // Arrange + val json = """ + { + "tangemComApiKey": "api-key-123", + "moonPayApiKey": "moon-pay-key", + "moonPayApiSecretKey": "secret-key", + "mercuryoWidgetId": "", + "blockchainSdkConfig": { + "blockchairApiKey": "blockchair-key", + "blockcypherTokens": ["token1", "token2"], + "quickNodeSolanaCredentials": { + "apiKey": "solana-key", + "subdomain": "solana-node" + } + }, + "isFeatureEnabled": true, + "maxRetryCount": 3 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + // Top-level properties + assertThat(generatedCode).contains("""const val tangemComApiKey: String = "api-key-123"""") + assertThat(generatedCode).contains("val mercuryoWidgetId: String? = null") + assertThat(generatedCode).contains("const val isFeatureEnabled: Boolean = true") + assertThat(generatedCode).contains("const val maxRetryCount: Long = 3") + + // Nested object + assertThat(generatedCode).contains("object BlockchainSdkConfig {") + assertThat(generatedCode).contains("""const val blockchairApiKey: String = "blockchair-key"""") + assertThat(generatedCode).contains("val blockcypherTokens: List") + + // Deeply nested object + assertThat(generatedCode).contains("object QuickNodeSolanaCredentials {") + } + + private fun generateAndReadOutput(jsonContent: String): String { + val inputFile = File(tempDir, "config.json").apply { + writeText(jsonContent) + } + + EnvironmentConfigGenerator.generate(inputFile, outputDir) + + val generatedFile = File( + outputDir, + "com/tangem/datasource/local/config/environment/generated/GeneratedEnvironmentConfig.kt" + ) + + assertThat(generatedFile.exists()).isTrue() + return generatedFile.readText() + } +} + + + + From a41dd9aacea3bd289191301462ff17581bf93200 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 15:23:26 +0100 Subject: [PATCH 35/97] Updated on 2026-08-14 --- .../tangem/tap/di/domain/EarnDomainModule.kt | 3 ++ .../earn/usecase/GetEarnNetworksUseCase.kt | 28 ++++++++++++++----- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt index bc3f3f9376..d6555cd643 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/EarnDomainModule.kt @@ -1,6 +1,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.account.supplier.MultiAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.earn.usecase.* import dagger.Module @@ -21,10 +22,12 @@ object EarnDomainModule { fun provideGetEarnNetworksUseCase( earnRepository: EarnRepository, multiAccountListSupplier: MultiAccountListSupplier, + userWalletsListRepository: UserWalletsListRepository, ): GetEarnNetworksUseCase { return GetEarnNetworksUseCase( earnRepository = earnRepository, multiAccountListSupplier = multiAccountListSupplier, + userWalletsListRepository = userWalletsListRepository, ) } diff --git a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt index 4fc006ec5c..e6a93c5605 100644 --- a/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt +++ b/domain/earn/src/main/java/com/tangem/domain/earn/usecase/GetEarnNetworksUseCase.kt @@ -3,13 +3,15 @@ package com.tangem.domain.earn.usecase import arrow.core.Either import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.supplier.MultiAccountListSupplier +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.earn.repository.EarnRepository import com.tangem.domain.models.earn.EarnNetwork import com.tangem.domain.models.earn.EarnNetworks +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isLocked import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map /** * Observes earn networks with [EarnNetwork.isAdded] enriched from user's active (non-archived) @@ -21,6 +23,7 @@ import kotlinx.coroutines.flow.map class GetEarnNetworksUseCase( private val earnRepository: EarnRepository, private val multiAccountListSupplier: MultiAccountListSupplier, + private val userWalletsListRepository: UserWalletsListRepository, ) { operator fun invoke(): Flow { @@ -37,12 +40,23 @@ class GetEarnNetworksUseCase( } private fun observeMyNetworkIds(): Flow> { - return multiAccountListSupplier() - .map { accountLists -> - accountLists - .flatMap(AccountList::flattenCurrencies) - .map { it.network.backendId } - .toSet() + return combine( + multiAccountListSupplier(), + userWalletsListRepository.userWallets, + ) { accountLists, wallets -> + val unlockedWalletsId = wallets + .orEmpty() + .filterNot(UserWallet::isLocked) + .mapTo(HashSet()) { it.walletId } + + if (unlockedWalletsId.isEmpty()) { + return@combine emptySet() } + + accountLists + .filter { it.userWalletId in unlockedWalletsId } + .flatMap(AccountList::flattenCurrencies) + .mapTo(HashSet()) { it.network.backendId } + } } } \ No newline at end of file From 41fb5d5bccba8be882af0b2a71611d3d5a3fa0c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 21:03:08 +0500 Subject: [PATCH 36/97] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 3 + .../cardsettings/model/CardSettingsModel.kt | 2 +- .../com/tangem/common/routing/AppRoute.kt | 4 +- .../configs/feature_toggles_config.json | 4 + .../api/pay/models/response/OrderResponse.kt | 17 +- .../pay/DefaultTangemPayEligibilityManager.kt | 2 +- .../tangem/data/pay/di/TangemPayDataModule.kt | 27 +++ .../DefaultPaymentAccountStatusFetcher.kt | 178 ++++++++++++++++++ .../DefaultPaymentAccountStatusProducer.kt | 37 ++++ .../DefaultCustomerOrderRepository.kt | 11 +- .../repository/DefaultOnboardingRepository.kt | 20 +- .../DefaultTangemPayCardDetailsRepository.kt | 8 +- .../DefaultTangemPayTxHistoryRepository.kt | 26 +-- .../pay/store/PaymentAccountStatusesStore.kt | 24 +++ .../account/supplier/SingleAccountSupplier.kt | 4 + domain/kyc/models/.gitignore | 1 + domain/kyc/models/build.gradle.kts | 9 + .../com/tangem/domain/models/kyc/KycStatus.kt | 33 ++++ domain/tokens/build.gradle.kts | 1 + .../domain/tokens/wallet/FetchingSource.kt | 1 + .../tokens/wallet/WalletBalanceFetcher.kt | 32 +++- .../implementor/MultiWalletBalanceFetcher.kt | 1 + .../tokens/wallet/WalletBalanceFetcherTest.kt | 101 ++++++++-- .../MultiWalletBalanceFetcherTest.kt | 7 +- domain/visa/build.gradle.kts | 2 - .../tangem/domain/pay/PaymentAccountStatus.kt | 66 +++++++ .../domain/pay/TangemPayDetailsConfig.kt | 1 - .../pay/flow/PaymentAccountStatusFetcher.kt | 8 + .../pay/flow/PaymentAccountStatusProducer.kt | 11 ++ .../pay/flow/PaymentAccountStatusSupplier.kt | 10 + .../tangem/domain/pay/model/CustomerInfo.kt | 18 +- .../tangem/domain/pay/model/OrderStatus.kt | 12 +- .../pay/repository/OnboardingRepository.kt | 2 +- .../TangemPayMainScreenCustomerInfoUseCase.kt | 7 +- .../model/TangemPayTxHistoryListConfig.kt | 2 +- .../account/AccountCreateEditComponent.kt | 2 +- .../account/AccountDetailsComponent.kt | 2 +- .../createedit/AccountCreateEditModel.kt | 7 +- .../account/details/AccountDetailsModel.kt | 35 ++-- .../onramp/hottokens/model/HotCryptoModel.kt | 5 +- .../tangempay/TangemPayFeatureToggles.kt | 5 + .../DefaultTangemPayFeatureToggles.kt | 10 + .../components/TangemPayDetailsComponent.kt | 1 - .../DefaultTangemPayTxHistoryComponent.kt | 1 - .../tangempay/di/TangemPayDetailsModule.kt | 21 +++ .../model/TangemPayTxHistoryModel.kt | 3 +- .../utils/TangemPayTxHistoryListManager.kt | 14 +- .../model/TangemPayOnboardingModel.kt | 2 +- features/tokendetails/impl/build.gradle.kts | 1 + .../DefaultTokenDetailsDeepLinkHandler.kt | 7 +- .../utils/AccountItemsDelegate.kt | 2 +- .../WalletTangemPayAnalyticsEventSender.kt | 4 +- .../wallet/domain/WalletContentFetcher.kt | 12 +- .../TangemPayUpdateInfoStateTransformer.kt | 43 ++--- .../visa/TangemPayMainScreenBlock.kt | 13 ++ 55 files changed, 709 insertions(+), 173 deletions(-) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt create mode 100644 domain/kyc/models/.gitignore create mode 100644 domain/kyc/models/build.gradle.kts create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt create mode 100644 features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 5bb200ad04..ef95af2a32 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -7,6 +7,7 @@ import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.promo.PromoRepository import com.tangem.domain.quotes.QuotesRepository import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher @@ -378,6 +379,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ): WalletBalanceFetcher { @@ -388,6 +390,7 @@ internal object TokensDomainModule { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt index 797ef6c056..2b26905caf 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/model/CardSettingsModel.kt @@ -221,7 +221,7 @@ internal class CardSettingsModel @Inject constructor( val card = scanResponse.card modelScope.launch { - val hasTangemPay = onboardingRepository.checkCustomerWallet(userWalletId).getOrNull() == true + val hasTangemPay = onboardingRepository.hasTangemPayInWallet(userWalletId).getOrNull() == true store.dispatchNavigationAction { push( route = AppRoute.ResetToFactory( 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 a514a89100..a926dcb3ec 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 @@ -408,12 +408,12 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class EditAccount( - val account: Account, + val account: Account.CryptoPortfolio, ) : AppRoute(path = "/edit_account/${account.accountId.value}") @Serializable data class AccountDetails( - val account: Account, + val account: Account.CryptoPortfolio, ) : AppRoute(path = "/account_details/${account.accountId.value}") @Serializable diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 23b909ebad..97d0aecaab 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -59,5 +59,9 @@ { "name": "GASLESS_APPROVAL_ENABLED", "version": "undefined" + }, + { + "name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED", + "version": "undefined" } ] diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt index 48cc8fc4fa..e8ce00ce4f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/OrderResponse.kt @@ -12,7 +12,7 @@ data class OrderResponse( @Json(name = "id") val id: String, @Json(name = "customer_id") val customerId: String?, @Json(name = "type") val type: String?, - @Json(name = "status") val status: String, + @Json(name = "status") val status: Status, @Json(name = "step") val step: String?, @Json(name = "data") val data: Data, @Json(name = "step_change_code") val stepChangeCode: Int?, @@ -29,5 +29,20 @@ data class OrderResponse( @Json(name = "payment_account_id") val paymentAccountId: String?, @Json(name = "transaction_hash") val transactionHash: String?, ) + + @JsonClass(generateAdapter = false) + enum class Status { + @Json(name = "NEW") + NEW, + + @Json(name = "PROCESSING") + PROCESSING, + + @Json(name = "COMPLETED") + COMPLETED, + + @Json(name = "CANCELED") + CANCELED, + } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt index 2d20b15a27..c3eb76035e 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayEligibilityManager.kt @@ -104,7 +104,7 @@ internal class DefaultTangemPayEligibilityManager @Inject constructor( map { wallet -> async { val isCustomer = onboardingRepository - .checkCustomerWallet(wallet.walletId) + .hasTangemPayInWallet(wallet.walletId) .getOrNull() == true wallet to isCustomer } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index 5868e4c0c6..d988e0088c 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -2,12 +2,17 @@ package com.tangem.data.pay.di import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory import com.tangem.data.pay.DefaultTangemPayEligibilityManager +import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher +import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.repository.* import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusProducer +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier import com.tangem.domain.pay.repository.* import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase @@ -75,7 +80,29 @@ internal interface TangemPayDataModule { @Singleton fun bindTangemPayEligibilityManager(impl: DefaultTangemPayEligibilityManager): TangemPayEligibilityManager + @Binds + @Singleton + fun bindPaymentAccountStatusProducerFactory( + impl: DefaultPaymentAccountStatusProducer.Factory, + ): PaymentAccountStatusProducer.Factory + + @Binds + @Singleton + fun bindPaymentAccountStatusFetcher(impl: DefaultPaymentAccountStatusFetcher): PaymentAccountStatusFetcher + companion object { + + @Provides + @Singleton + fun providePaymentAccountStatusSupplier( + factory: PaymentAccountStatusProducer.Factory, + ): PaymentAccountStatusSupplier { + return object : PaymentAccountStatusSupplier( + factory = factory, + keyCreator = { "payment_account_status_${it.userWalletId.stringValue}" }, + ) {} + } + @Provides @Singleton fun provideTangemPayMainScreenCustomerInfoUseCase( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt new file mode 100644 index 0000000000..cdfbf0d9b4 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -0,0 +1,178 @@ +package com.tangem.data.pay.flow + +import arrow.core.Either +import com.tangem.data.pay.store.PaymentAccountStatusesStore +import com.tangem.domain.core.utils.eitherOn +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.pay.model.OrderStatus +import com.tangem.domain.pay.repository.CustomerOrderRepository +import com.tangem.domain.pay.repository.OnboardingRepository +import com.tangem.domain.visa.error.VisaApiError +import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.security.isSecurityExposed +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import timber.log.Timber +import javax.inject.Inject + +private const val TAG = "PaymentAccountStatusFetcher" + +internal class DefaultPaymentAccountStatusFetcher @Inject constructor( + private val paymentAccountStatusesStore: PaymentAccountStatusesStore, + private val onboardingRepository: OnboardingRepository, + private val customerOrderRepository: CustomerOrderRepository, + private val deviceSecurity: DeviceSecurityInfoProvider, + private val dispatchers: CoroutineDispatcherProvider, +) : PaymentAccountStatusFetcher { + + override suspend fun invoke(params: PaymentAccountStatusFetcher.Params): Either = + eitherOn(dispatchers.default) { + Timber.tag(TAG).i("fetch: ${params.userWalletId.stringValue}") + + if (deviceSecurity.isSecurityExposed()) { + Timber.tag(TAG).i("fetch security info: rooted: ${deviceSecurity.isRooted}") + Timber.tag(TAG).i("fetch security info: xposed: ${deviceSecurity.isXposed}") + Timber.tag(TAG).i("fetch security info: bootloader unlocked: ${deviceSecurity.isBootloaderUnlocked}") + + return@eitherOn paymentAccountStatusesStore.store( + userWalletId = params.userWalletId, + status = PaymentAccountStatus.Error.ExposedDevice, + ) + } + + val status = onboardingRepository.hasTangemPayInWallet(userWalletId = params.userWalletId) + .fold( + ifLeft = { error -> + Timber.tag(TAG).e("Failed check wallet ${params.userWalletId}: ${error.javaClass.simpleName}") + when (error) { + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated + else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + }, + ifRight = { hasTangemPay -> + proceedHasTangemPayResult(userWalletId = params.userWalletId, hasTangemPay = hasTangemPay) + }, + ) + Timber.tag(TAG).i("invoke status ${params.userWalletId}: $status") + paymentAccountStatusesStore.store(userWalletId = params.userWalletId, status = status) + } + + private suspend fun proceedHasTangemPayResult( + userWalletId: UserWalletId, + hasTangemPay: Boolean, + ): PaymentAccountStatus { + Timber.tag(TAG).i("proceedHasTangemPayResult for $userWalletId hasTangemPay: $hasTangemPay") + return if (hasTangemPay) { + fetchTangemPayAccountStatus(userWalletId = userWalletId) + } else { + PaymentAccountStatus.NotCreated + } + } + + private suspend fun fetchTangemPayAccountStatus(userWalletId: UserWalletId): PaymentAccountStatus { + val prevResult = paymentAccountStatusesStore.getSyncOrNull(userWalletId) + if (prevResult == null || prevResult is PaymentAccountStatus.Error) { + paymentAccountStatusesStore.store(userWalletId = userWalletId, status = PaymentAccountStatus.Loading) + } + + return proceedWithOrderId(userWalletId = userWalletId) + } + + private suspend fun proceedWithOrderId(userWalletId: UserWalletId): PaymentAccountStatus { + return if (!onboardingRepository.isTangemPayInitialDataProduced(userWalletId)) { + PaymentAccountStatus.Error.NotSynced + } else { + val orderId = onboardingRepository.getOrderId(userWalletId) + if (orderId != null) { + proceedWithOrderId(userWalletId = userWalletId, orderId = orderId) + } else { + proceedWithoutOrder(userWalletId = userWalletId) + } + } + } + + private suspend fun proceedWithoutOrder(userWalletId: UserWalletId): PaymentAccountStatus { + return onboardingRepository.getCustomerInfo(userWalletId).fold( + ifLeft = { error -> + Timber.tag(TAG).e("proceedWithoutOrder $userWalletId error: $error") + error.mapToPaymentAccountStatus() + }, + ifRight = { customerInfo -> + Timber.tag(TAG).i("proceedWithoutOrder data customerInfo $userWalletId") + val status = customerInfo.mapToPaymentAccountStatus() + if (status is PaymentAccountStatus.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) { + // If order id wasn't saved -> start order creation and get customer info + onboardingRepository.createOrder(userWalletId) + } + status + }, + ) + } + + private suspend fun proceedWithOrderId(userWalletId: UserWalletId, orderId: String): PaymentAccountStatus { + return customerOrderRepository.getOrderData(userWalletId, orderId = orderId).fold( + ifLeft = { error -> + Timber.tag(TAG).e("proceedWithOrderId $userWalletId orderId: $orderId error: $error") + error.mapToPaymentAccountStatus() + }, + ifRight = { orderData -> + Timber.tag(TAG).i("proceedWithOrderId $userWalletId: $orderId status: ${orderData.status}") + when (orderData.status) { + // Kyc is passed and user waits for order creation -> no need to get customer info + OrderStatus.NEW, + OrderStatus.PROCESSING, + -> PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + + OrderStatus.CANCELED -> { + // If order was cancelled -> clear previous order from local storage and start order creation + onboardingRepository.clearOrderId(userWalletId) + onboardingRepository.createOrder(userWalletId) + PaymentAccountStatus.Error.CardIssueFailed + } + OrderStatus.COMPLETED -> { + // Order was completed -> clear order id and get customer info + onboardingRepository.clearOrderId(userWalletId) + onboardingRepository.getCustomerInfo(userWalletId = userWalletId) + .fold( + ifLeft = { it.mapToPaymentAccountStatus() }, + ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, + ) + } + OrderStatus.UNKNOWN -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + }, + ) + } + + private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatus { + val cardInfo = this.cardInfo + val productInstance = this.productInstance + return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { + PaymentAccountStatus.UnderReview(source = StatusSource.ACTUAL, kycStatus = kycStatus) + } else if (cardInfo != null && productInstance != null) { + PaymentAccountStatus.Loaded( + source = StatusSource.ACTUAL, + cardId = productInstance.cardId, + lastFourDigits = cardInfo.lastFourDigits, + balance = cardInfo.balance, + currencyCode = cardInfo.currencyCode, + depositAddress = cardInfo.depositAddress, + isPinSet = cardInfo.isPinSet, + ) + } else { + PaymentAccountStatus.IssuingCard(source = StatusSource.ACTUAL) + } + } + + private fun VisaApiError.mapToPaymentAccountStatus(): PaymentAccountStatus { + return when (this) { + is VisaApiError.RefreshTokenExpired -> PaymentAccountStatus.Error.NotSynced + is VisaApiError.NotPaeraCustomer -> PaymentAccountStatus.NotCreated + else -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt new file mode 100644 index 0000000000..cb021c1ee8 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusProducer.kt @@ -0,0 +1,37 @@ +package com.tangem.data.pay.flow + +import arrow.core.Option +import arrow.core.some +import com.tangem.data.pay.store.PaymentAccountStatusesStore +import com.tangem.domain.core.flow.FlowProducerTools +import com.tangem.domain.models.StatusSource +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.domain.pay.flow.PaymentAccountStatusProducer +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onEmpty + +internal class DefaultPaymentAccountStatusProducer @AssistedInject constructor( + @Assisted private val params: PaymentAccountStatusProducer.Params, + override val flowProducerTools: FlowProducerTools, + private val paymentAccountStatusesStore: PaymentAccountStatusesStore, + private val dispatchers: CoroutineDispatcherProvider, +) : PaymentAccountStatusProducer { + override val fallback: Option + get() = PaymentAccountStatus.Error.Unavailable(source = StatusSource.ACTUAL).some() + + override fun produce(): Flow { + return paymentAccountStatusesStore.get(userWalletId = params.userWalletId) + .onEmpty { emit(value = PaymentAccountStatus.NotCreated) } + .flowOn(dispatchers.default) + } + + @AssistedFactory + interface Factory : PaymentAccountStatusProducer.Factory { + override fun create(params: PaymentAccountStatusProducer.Params): DefaultPaymentAccountStatusProducer + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt index f479794766..508627ca37 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultCustomerOrderRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import com.tangem.datasource.api.pay.TangemPayApi +import com.tangem.datasource.api.pay.models.response.OrderResponse import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.OrderData import com.tangem.domain.pay.model.OrderStatus @@ -19,11 +20,11 @@ internal class DefaultCustomerOrderRepository @Inject constructor( tangemPayApi.getOrder(authHeader = authHeader, orderId = orderId) }.map { response -> val status = when (response.result?.status) { - null -> OrderStatus.UNKNOWN - OrderStatus.NEW.apiName -> OrderStatus.NEW - OrderStatus.PROCESSING.apiName -> OrderStatus.PROCESSING - OrderStatus.COMPLETED.apiName -> OrderStatus.COMPLETED - else -> OrderStatus.CANCELED + null -> OrderStatus.PROCESSING + OrderResponse.Result.Status.NEW -> OrderStatus.NEW + OrderResponse.Result.Status.PROCESSING -> OrderStatus.PROCESSING + OrderResponse.Result.Status.COMPLETED -> OrderStatus.COMPLETED + OrderResponse.Result.Status.CANCELED -> OrderStatus.CANCELED } OrderData( status = status, 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 2fdcfabcb6..042922e80a 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 @@ -10,6 +10,7 @@ import com.tangem.datasource.api.pay.models.response.CustomerMeResponse import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.datasource.TangemPayAuthDataSource @@ -27,9 +28,6 @@ import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject private const val VALID_STATUS = "valid" -private const val APPROVED_KYC_STATUS = "approved" -private const val IN_PROGRESS_KYC_STATUS = "in_progress" -private const val DECLINED_KYC_STATUS = "declined" private const val TAG = "TangemPay: OnboardingRepository" @Suppress("LongParameterList") @@ -145,7 +143,6 @@ internal class DefaultOnboardingRepository @Inject constructor( lastFourDigits = card.cardNumberEnd, balance = fiatBalance.availableBalance, currencyCode = fiatBalance.currency, - customerWalletAddress = paymentAccount.customerWalletAddress, depositAddress = response.depositAddress, isPinSet = response.card?.isPinSet == true, ) @@ -159,19 +156,19 @@ internal class DefaultOnboardingRepository @Inject constructor( } cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) - ProductInstance(id = instance.id, cardId = instance.cardId, cardFrozenState = cardFrozenState) + ProductInstance(id = instance.id, cardId = instance.cardId) } return CustomerInfo( customerId = response?.id, productInstance = productInstance, - kycStatus = getKycStatus(status = response?.kyc?.status), + kycStatus = KycStatus.fromString(status = response?.kyc?.status), cardInfo = cardInfo, ).also { lastFetchedCustomerInfoMap[userWalletId] = it } } - override suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either { + override suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either { val hasTangemPay = tangemPayStorage.checkCustomerWalletResult(userWalletId) if (hasTangemPay != null) { return Either.Right(hasTangemPay) @@ -228,13 +225,4 @@ internal class DefaultOnboardingRepository @Inject constructor( setHideMainOnboardingBanner(userWalletId) } } - - private fun getKycStatus(status: String?): CustomerInfo.KycStatus { - return when (status?.lowercase()) { - IN_PROGRESS_KYC_STATUS -> CustomerInfo.KycStatus.PENDING - DECLINED_KYC_STATUS -> CustomerInfo.KycStatus.REJECTED - APPROVED_KYC_STATUS -> CustomerInfo.KycStatus.APPROVED - else -> CustomerInfo.KycStatus.INIT - } - } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt index 3252c49c4e..61f222a421 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/repository/DefaultTangemPayCardDetailsRepository.kt @@ -16,10 +16,10 @@ import com.tangem.datasource.api.pay.models.request.CardDetailsRequest import com.tangem.datasource.api.pay.models.request.FreezeUnfreezeCardRequest import com.tangem.datasource.api.pay.models.request.SetPinRequest import com.tangem.datasource.api.pay.models.response.FreezeUnfreezeCardResponse +import com.tangem.datasource.api.pay.models.response.OrderResponse.Result.Status import com.tangem.datasource.local.visa.TangemPayCardFrozenStateStore import com.tangem.datasource.local.visa.TangemPayStorage import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance import com.tangem.domain.pay.model.TangemPayCardDetails @@ -279,15 +279,15 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( orderStatus.onRight { response -> val status = response.result?.status - if (status == OrderStatus.COMPLETED.apiName || status == OrderStatus.CANCELED.apiName) { + if (status == Status.COMPLETED || status == Status.CANCELED) { // Remove from jobs pollingJobs.remove(key = orderId) // Final card state val finalState = when { - status == OrderStatus.COMPLETED.apiName && isFreeze + status == Status.COMPLETED && isFreeze -> TangemPayCardFrozenState.Frozen - status == OrderStatus.COMPLETED.apiName && !isFreeze + status == Status.COMPLETED && !isFreeze -> TangemPayCardFrozenState.Unfrozen else -> return@launch } 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 021605d89a..eab58caa05 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 @@ -77,34 +77,22 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( limit: Int, ): List { cacheRegistry.invokeOnExpire( - key = getCacheKey(customerWalletAddress = config.customerWalletAddress, cursor = cursor), + key = getCacheKey(userWalletId = userWalletId, cursor = cursor), skipCache = config.shouldRefresh, - block = { - fetch( - userWalletId = userWalletId, - customerWalletAddress = config.customerWalletAddress, - cursor = cursor, - pageSize = limit, - ) - }, + block = { fetch(userWalletId = userWalletId, cursor = cursor, pageSize = limit) }, ) return txHistoryItemsStore.getSyncOrNull( - key = config.customerWalletAddress, + key = userWalletId.stringValue, cursor = cursor ?: INITIAL_CURSOR, ).orEmpty() } - private fun getCacheKey(customerWalletAddress: String, cursor: String?): String { - return "tangem_pay_tx_history_${customerWalletAddress}_${cursor ?: INITIAL_CURSOR}" + private fun getCacheKey(userWalletId: UserWalletId, cursor: String?): String { + return "tangem_pay_tx_history_${userWalletId.stringValue}_${cursor ?: INITIAL_CURSOR}" } - private suspend fun fetch( - userWalletId: UserWalletId, - customerWalletAddress: String, - cursor: String?, - pageSize: Int, - ) { + private suspend fun fetch(userWalletId: UserWalletId, cursor: String?, pageSize: Int) { requestPerformer.performRequest(userWalletId = userWalletId) { authHeader -> visaApi.getTangemPayTxHistory(authHeader = authHeader, limit = pageSize, cursor = cursor) }.onLeft { @@ -112,7 +100,7 @@ internal class DefaultTangemPayTxHistoryRepository @Inject constructor( }.onRight { response -> val result = response.result val items = txHistoryItemConverter.convertList(result.transactions).filterNotNull() - txHistoryItemsStore.store(key = customerWalletAddress, cursor = cursor ?: INITIAL_CURSOR, value = items) + txHistoryItemsStore.store(key = userWalletId.stringValue, cursor = cursor ?: INITIAL_CURSOR, value = items) } } } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt new file mode 100644 index 0000000000..c6cfc742e0 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -0,0 +1,24 @@ +package com.tangem.data.pay.store + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import javax.inject.Inject +import javax.inject.Singleton + +@Suppress("UnusedParameter", "EmptyFunctionBlock", "FunctionOnlyReturningConstant") +@Singleton +internal class PaymentAccountStatusesStore @Inject constructor() { + + fun get(userWalletId: UserWalletId): Flow { + return emptyFlow() + } + + fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { + return null + } + + fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { + } +} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt index 2a87c8b14e..a8a0e5cdbb 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/supplier/SingleAccountSupplier.kt @@ -22,4 +22,8 @@ abstract class SingleAccountSupplier( fun filterPaymentAccount(accountId: AccountId): Flow { return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance() } + + fun filterCryptoPortfolioAccount(accountId: AccountId): Flow { + return invoke(params = SingleAccountProducer.Params(accountId)).filterIsInstance() + } } \ No newline at end of file diff --git a/domain/kyc/models/.gitignore b/domain/kyc/models/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/domain/kyc/models/.gitignore @@ -0,0 +1 @@ +/build diff --git a/domain/kyc/models/build.gradle.kts b/domain/kyc/models/build.gradle.kts new file mode 100644 index 0000000000..6b18f3f83f --- /dev/null +++ b/domain/kyc/models/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +dependencies { + implementation(deps.kotlin.serialization) +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt new file mode 100644 index 0000000000..0546090329 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt @@ -0,0 +1,33 @@ +package com.tangem.domain.models.kyc + +private const val APPROVED_KYC_STATUS = "approved" +private const val IN_PROGRESS_KYC_STATUS = "in_progress" +private const val DECLINED_KYC_STATUS = "declined" + +enum class KycStatus { + /** Initial state */ + INIT, + + /** Performing the check */ + PENDING, + + /** SumSub approved */ + APPROVED, + + /** The check failed, documents rejected */ + REJECTED, + + ; + + companion object { + + fun fromString(status: String?, default: KycStatus = INIT): KycStatus { + return when (status?.lowercase()) { + IN_PROGRESS_KYC_STATUS -> PENDING + DECLINED_KYC_STATUS -> REJECTED + APPROVED_KYC_STATUS -> APPROVED + else -> default + } + } + } +} \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index c2a83d7894..53e21909a4 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { implementation(projects.domain.walletManager) implementation(projects.domain.card) implementation(projects.domain.staking) + implementation(projects.domain.visa) implementation(projects.libs.blockchainSdk) implementation(projects.domain.tokens.models) implementation(projects.domain.txhistory.models) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt index a30ede50c9..56c8bf8ef3 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/FetchingSource.kt @@ -9,4 +9,5 @@ enum class FetchingSource { NETWORK, QUOTE, STAKING, + TANGEM_PAY, } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt index 06b3f5f4ee..17b46927d6 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcher.kt @@ -2,11 +2,13 @@ package com.tangem.domain.tokens.wallet import arrow.core.Either import arrow.core.raise.either +import arrow.core.right import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher @@ -45,6 +47,7 @@ class WalletBalanceFetcher internal constructor( private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, private val stakingIdFactory: StakingIdFactory, private val dispatchers: CoroutineDispatcherProvider, ) : FlowFetcher { @@ -57,6 +60,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher: MultiNetworkStatusFetcher, multiQuoteStatusFetcher: MultiQuoteStatusFetcher, multiStakingBalanceFetcher: MultiStakingBalanceFetcher, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, stakingIdFactory: StakingIdFactory, dispatchers: CoroutineDispatcherProvider, ) : this( @@ -72,6 +76,7 @@ class WalletBalanceFetcher internal constructor( multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = dispatchers, ) @@ -91,10 +96,18 @@ class WalletBalanceFetcher internal constructor( error("UserWallet doesn't contain crypto-currencies: $userWalletId") } - fetcher.fetch(userWalletId = userWalletId, currencies = currencies) + fetcher.fetch( + userWalletId = userWalletId, + currencies = currencies, + paymentAccountRefactorEnabled = params.isPaymentAccountRefactorEnabled, + ) } - private suspend fun BaseWalletBalanceFetcher.fetch(userWalletId: UserWalletId, currencies: Set) { + private suspend fun BaseWalletBalanceFetcher.fetch( + userWalletId: UserWalletId, + currencies: Set, + paymentAccountRefactorEnabled: Boolean, + ) { coroutineScope { val results = fetchingSources.map { source -> async { @@ -102,6 +115,10 @@ class WalletBalanceFetcher internal constructor( FetchingSource.NETWORK -> fetchNetworks(userWalletId = userWalletId, currencies = currencies) FetchingSource.QUOTE -> fetchQuotes(currencies = currencies) FetchingSource.STAKING -> fetchStaking(userWalletId = userWalletId, currencies = currencies) + FetchingSource.TANGEM_PAY -> fetchPaymentAccount( + userWalletId = userWalletId, + paymentAccountRefactorEnabled = paymentAccountRefactorEnabled, + ) } source to maybeResult @@ -173,10 +190,19 @@ class WalletBalanceFetcher internal constructor( } } + private suspend fun fetchPaymentAccount( + userWalletId: UserWalletId, + paymentAccountRefactorEnabled: Boolean, + ): Either { + if (!paymentAccountRefactorEnabled) return Unit.right() + + return paymentAccountStatusFetcher.invoke(PaymentAccountStatusFetcher.Params(userWalletId)) + } + /** * Params of [WalletBalanceFetcher] * * @property userWalletId user wallet id */ - data class Params(val userWalletId: UserWalletId) + data class Params(val userWalletId: UserWalletId, val isPaymentAccountRefactorEnabled: Boolean) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt index 8bd9d8a36c..eaa142f3fb 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcher.kt @@ -27,6 +27,7 @@ internal class MultiWalletBalanceFetcher( FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING, + FetchingSource.TANGEM_PAY, ) override suspend fun getCryptoCurrencies(userWalletId: UserWalletId): Set { diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt index 2af6690bc8..937aa0d32d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/WalletBalanceFetcherTest.kt @@ -9,6 +9,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.staking.StakingID import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher +import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.staking.StakingIdFactory import com.tangem.domain.staking.model.StakingIntegrationID @@ -42,6 +43,7 @@ internal class WalletBalanceFetcherTest { private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher = mockk() private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher = mockk() private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher = mockk() + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher = mockk() private val stakingIdFactory: StakingIdFactory = mockk() private val fetcher = WalletBalanceFetcher( @@ -52,6 +54,7 @@ internal class WalletBalanceFetcherTest { multiNetworkStatusFetcher = multiNetworkStatusFetcher, multiQuoteStatusFetcher = multiQuoteStatusFetcher, multiStakingBalanceFetcher = multiStakingBalanceFetcher, + paymentAccountStatusFetcher = paymentAccountStatusFetcher, stakingIdFactory = stakingIdFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @@ -76,7 +79,12 @@ internal class WalletBalanceFetcherTest { every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } throws exception // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = exception.left() @@ -107,7 +115,12 @@ internal class WalletBalanceFetcherTest { every { currenciesRepository.getCardTypesResolver(userWalletId = userWalletId) } returns cardTypesResolver // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException("Unknown type of wallet: $userWalletId").left() @@ -139,7 +152,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } throws exception // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = exception.left() @@ -171,7 +189,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiWalletBalanceFetcher.getCryptoCurrencies(userWalletId = userWalletId) } returns emptySet() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException("UserWallet doesn't contain crypto-currencies: $userWalletId").left() @@ -213,7 +236,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiNetworkStatusFetcher(params = networkStatusFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -259,7 +287,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -311,7 +344,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -354,7 +392,12 @@ internal class WalletBalanceFetcherTest { } returns Either.Left(StakingIdFactory.Error.UnsupportedCurrency) // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -396,7 +439,12 @@ internal class WalletBalanceFetcherTest { coEvery { stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = any()) } returns stakingId // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -444,7 +492,12 @@ internal class WalletBalanceFetcherTest { } returns stellarStakingId // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert assertEitherRight(actual) @@ -506,7 +559,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns exception.left() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = IllegalStateException( @@ -572,7 +630,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiStakingBalanceFetcher(params = stakingBalanceFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() @@ -624,7 +687,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() @@ -674,7 +742,12 @@ internal class WalletBalanceFetcherTest { coEvery { multiQuoteStatusFetcher(params = quoteStatusFetcherParams) } returns Unit.right() // Act - val actual = fetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) + val actual = fetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = false + ) + ) // Assert val expected = Unit.right() diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt index 55bc9e7b8b..100c5964c1 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/wallet/implementor/MultiWalletBalanceFetcherTest.kt @@ -47,7 +47,12 @@ class MultiWalletBalanceFetcherTest { val actual = fetcher.fetchingSources // Assert - val expected = setOf(FetchingSource.NETWORK, FetchingSource.QUOTE, FetchingSource.STAKING) + val expected = setOf( + FetchingSource.NETWORK, + FetchingSource.QUOTE, + FetchingSource.STAKING, + FetchingSource.TANGEM_PAY, + ) Truth.assertThat(actual).isEqualTo(expected) } diff --git a/domain/visa/build.gradle.kts b/domain/visa/build.gradle.kts index bc0e47c44b..3c13ebe644 100644 --- a/domain/visa/build.gradle.kts +++ b/domain/visa/build.gradle.kts @@ -24,8 +24,6 @@ dependencies { implementation(projects.domain.core) implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) - implementation(projects.features.swap.domain) - /** Security */ implementation(deps.spongecastle.core) diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt new file mode 100644 index 0000000000..bdc604087f --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/PaymentAccountStatus.kt @@ -0,0 +1,66 @@ +package com.tangem.domain.pay + +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.serialization.SerializedBigDecimal +import kotlinx.serialization.Serializable + +@Serializable +sealed class PaymentAccountStatus { + + abstract val source: StatusSource + + @Serializable + data object Loading : PaymentAccountStatus() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data object NotCreated : PaymentAccountStatus() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data class UnderReview( + override val source: StatusSource, + val kycStatus: KycStatus, + ) : PaymentAccountStatus() + + @Serializable + data class IssuingCard(override val source: StatusSource) : PaymentAccountStatus() + + @Serializable + data class Locked(override val source: StatusSource) : PaymentAccountStatus() + + @Serializable + data class Loaded( + override val source: StatusSource, + val cardId: String, + val lastFourDigits: String, + val balance: SerializedBigDecimal, + val currencyCode: String, + val depositAddress: String?, + val isPinSet: Boolean, + ) : PaymentAccountStatus() + + @Serializable + sealed class Error : PaymentAccountStatus() { + @Serializable + data object ExposedDevice : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data class Unavailable(override val source: StatusSource) : Error() + + @Serializable + data object NotSynced : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + + @Serializable + data object CardIssueFailed : Error() { + override val source: StatusSource = StatusSource.ACTUAL + } + } +} \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt index 14edae3667..e2424a061a 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/pay/TangemPayDetailsConfig.kt @@ -9,7 +9,6 @@ data class TangemPayDetailsConfig( val cardId: String, val isPinSet: Boolean, val cardFrozenState: TangemPayCardFrozenState, - val customerWalletAddress: String, val cardNumberEnd: String, val chainId: Int, ) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt new file mode 100644 index 0000000000..740d9d0824 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowFetcher +import com.tangem.domain.models.wallet.UserWalletId + +interface PaymentAccountStatusFetcher : FlowFetcher { + data class Params(val userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt new file mode 100644 index 0000000000..c49a25f45d --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusProducer.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowProducer +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.PaymentAccountStatus + +interface PaymentAccountStatusProducer : FlowProducer { + data class Params(val userWalletId: UserWalletId) + + interface Factory : FlowProducer.Factory +} \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt new file mode 100644 index 0000000000..94580d26e1 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusSupplier.kt @@ -0,0 +1,10 @@ +package com.tangem.domain.pay.flow + +import com.tangem.domain.core.flow.FlowCachingSupplier +import com.tangem.domain.pay.PaymentAccountStatus + +@Suppress("UnnecessaryAbstractClass") +abstract class PaymentAccountStatusSupplier( + override val factory: PaymentAccountStatusProducer.Factory, + override val keyCreator: (PaymentAccountStatusProducer.Params) -> String, +) : FlowCachingSupplier() \ 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 501df2011b..2fd02e7a45 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 @@ -1,6 +1,6 @@ package com.tangem.domain.pay.model -import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.domain.models.kyc.KycStatus import java.math.BigDecimal sealed class MainCustomerInfoContentState { @@ -22,31 +22,15 @@ data class CustomerInfo( val cardInfo: CardInfo?, ) { - enum class KycStatus { - /** Initial state */ - INIT, - - /** Performing the check */ - PENDING, - - /** SumSub approved */ - APPROVED, - - /** The check failed, documents rejected */ - REJECTED, - } - data class ProductInstance( val id: String, val cardId: String, - val cardFrozenState: TangemPayCardFrozenState, ) data class CardInfo( val lastFourDigits: String, val balance: BigDecimal, val currencyCode: String, - val customerWalletAddress: String, val depositAddress: String?, val isPinSet: Boolean, ) 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 6ae706a0e1..327d8fd61f 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,9 +1,9 @@ package com.tangem.domain.pay.model -enum class OrderStatus(val apiName: String) { - UNKNOWN(""), - NEW("NEW"), - PROCESSING("PROCESSING"), - COMPLETED("COMPLETED"), - CANCELED("CANCELED"), +enum class OrderStatus { + UNKNOWN, // TODO remove it after TangemPay accounts refactor TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED + NEW, + PROCESSING, + COMPLETED, + CANCELED, } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt index 568cf85006..52197ec817 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/OnboardingRepository.kt @@ -23,7 +23,7 @@ interface OnboardingRepository { suspend fun getOrderId(userWalletId: UserWalletId): String? - suspend fun checkCustomerWallet(userWalletId: UserWalletId): Either + suspend fun hasTangemPayInWallet(userWalletId: UserWalletId): Either suspend fun checkCustomerEligibility(): Boolean suspend fun getCustomerEligibility(): Boolean diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt index 647e6ad412..4116e2e4f4 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/TangemPayMainScreenCustomerInfoUseCase.kt @@ -3,6 +3,7 @@ package com.tangem.domain.pay.usecase import arrow.core.Either import arrow.core.left import arrow.core.right +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.* @@ -38,7 +39,7 @@ class TangemPayMainScreenCustomerInfoUseCase( return // fast exit } - onboardingRepository.checkCustomerWallet(userWalletId) + onboardingRepository.hasTangemPayInWallet(userWalletId) .fold( ifLeft = { error -> Timber.tag(TAG).e("Failed checkCustomerWallet for $userWalletId: ${error.javaClass.simpleName}") @@ -125,7 +126,7 @@ class TangemPayMainScreenCustomerInfoUseCase( } .map { customerInfo -> Timber.tag(TAG).i("customerInfo") - if (customerInfo.cardInfo == null && customerInfo.kycStatus == CustomerInfo.KycStatus.APPROVED) { + if (customerInfo.cardInfo == null && customerInfo.kycStatus == KycStatus.APPROVED) { // If order id wasn't saved -> start order creation and get customer info onboardingRepository.createOrder(userWalletId) } @@ -151,7 +152,7 @@ class TangemPayMainScreenCustomerInfoUseCase( info = CustomerInfo( customerId = null, productInstance = null, - kycStatus = CustomerInfo.KycStatus.APPROVED, + kycStatus = KycStatus.APPROVED, cardInfo = null, ), orderStatus = orderData.status, diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt index fb61bc2965..5b68240025 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/model/TangemPayTxHistoryListConfig.kt @@ -1,3 +1,3 @@ package com.tangem.domain.tangempay.model -data class TangemPayTxHistoryListConfig(val customerWalletAddress: String, val shouldRefresh: Boolean) \ No newline at end of file +data class TangemPayTxHistoryListConfig(val shouldRefresh: Boolean) \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt index b18a7db430..2a24783240 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountCreateEditComponent.kt @@ -15,7 +15,7 @@ interface AccountCreateEditComponent : ComposableContentComponent { ) : Params data class Edit( - val account: Account, + val account: Account.CryptoPortfolio, ) : Params } } \ No newline at end of file diff --git a/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt index d1c47280ea..99450a8474 100644 --- a/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt +++ b/features/account/api/src/main/java/com/tangem/features/account/AccountDetailsComponent.kt @@ -7,5 +7,5 @@ import com.tangem.domain.models.account.Account interface AccountDetailsComponent : ComposableContentComponent { interface Factory : ComponentFactory - data class Params(val account: Account) + data class Params(val account: Account.CryptoPortfolio) } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index d100998aca..4b69fc806d 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -25,7 +25,6 @@ import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.AccountCreateEditComponent import com.tangem.features.account.analytics.AccountSettingsAnalyticEvents @@ -73,7 +72,7 @@ internal class AccountCreateEditModel @Inject constructor( when (params) { is AccountCreateEditComponent.Params.Create -> updateDerivationInfo(userWalletId = params.userWalletId) is AccountCreateEditComponent.Params.Edit -> { - val derivationIndex = params.account.derivationIndex?.value + val derivationIndex = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.AccountEditScreenOpened(derivationIndex) analyticsEventHandler.send(event) } @@ -170,7 +169,7 @@ internal class AccountCreateEditModel @Inject constructor( val icon = CryptoPortfolioIconConverter.convertBack(state.account.portfolioIcon) val isNewName = name != params.account.accountName val isNewIcon = icon != params.account.portfolioIcon - val derivationIndex = params.account.derivationIndex?.value + val derivationIndex = params.account.derivationIndex.value analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonSave(name, icon, derivationIndex)) uiState.value = uiState.value.toggleProgress(showProgress = true) @@ -182,7 +181,7 @@ internal class AccountCreateEditModel @Inject constructor( uiState.value = uiState.value.toggleProgress(showProgress = false) result - .onLeft { error -> handleEditAccountError(error, params.account.derivationIndex?.value) } + .onLeft { error -> handleEditAccountError(error, params.account.derivationIndex.value) } .onRight { showMessage(R.string.account_edit_success_message) router.pop() 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 47b06df25c..ee77d31aa2 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 @@ -14,12 +14,10 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.account.AccountDetailsComponent @@ -54,29 +52,29 @@ internal class AccountDetailsModel @Inject constructor( init { analyticsEventHandler.send(AccountSettingsAnalyticEvents.AccountSettingsScreenOpened()) - singleAccountSupplier(SingleAccountProducer.Params(accountId)) + singleAccountSupplier.filterCryptoPortfolioAccount(accountId) .onEach { account -> uiState.update { buildUI(account) } } .launchIn(modelScope) } - private fun onEditAccountClick(account: Account) { + private fun onEditAccountClick(account: Account.CryptoPortfolio) { analyticsEventHandler.send(AccountSettingsAnalyticEvents.ButtonEdit()) router.push(AppRoute.EditAccount(account)) } - private fun onManageTokensClick(account: Account) { + private fun onManageTokensClick(account: Account.CryptoPortfolio) { val route = AppRoute.ManageTokens( source = AppRoute.ManageTokens.Source.ACCOUNT, portfolioId = PortfolioId(account.accountId), ) analyticsEventHandler.send( - AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex?.value), + AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex.value), ) router.push(route) } private fun onArchiveAccountClick() { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonArchiveAccount(accountDerivation) analyticsEventHandler.send(event) confirmArchiveDialog() @@ -86,7 +84,7 @@ internal class AccountDetailsModel @Inject constructor( val secondAction = EventMessageAction( title = resourceReference(R.string.common_cancel), onClick = { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonCancelAccountArchivation(accountDerivation) analyticsEventHandler.send(event) }, @@ -107,7 +105,7 @@ internal class AccountDetailsModel @Inject constructor( } private fun archiveCryptoPortfolio() = modelScope.launch { - val accountDerivation = params.account.derivationIndex?.value + val accountDerivation = params.account.derivationIndex.value val event = AccountSettingsAnalyticEvents.ButtonArchiveAccountConfirmation(accountDerivation) analyticsEventHandler.send(event) uiState.update { it.toggleProgress(true) } @@ -128,7 +126,7 @@ internal class AccountDetailsModel @Inject constructor( val event = AccountSettingsAnalyticEvents.AccountError( source = AccountSettingsAnalyticEvents.Source.ARCHIVE, error = error.tag, - accountDerivation = params.account.derivationIndex?.value, + accountDerivation = params.account.derivationIndex.value, ) analyticsEventHandler.send(event) val titleRes: Int @@ -155,16 +153,13 @@ internal class AccountDetailsModel @Inject constructor( messageSender.send(dialogMessage) } - private fun buildUI(account: Account): AccountDetailsUM { - val archiveMode = when (account) { - is Account.CryptoPortfolio -> when (account.isMainAccount) { - true -> ArchiveMode.None - false -> ArchiveMode.Available( - onArchiveAccountClick = ::onArchiveAccountClick, - isLoading = false, - ) - } - is Account.Payment -> TODO("[REDACTED_JIRA]") + private fun buildUI(account: Account.CryptoPortfolio): AccountDetailsUM { + val archiveMode = when (account.isMainAccount) { + true -> ArchiveMode.None + false -> ArchiveMode.Available( + onArchiveAccountClick = ::onArchiveAccountClick, + isLoading = false, + ) } val isMultiCurrency = getUserWalletUseCase(account.accountId.userWalletId).getOrNull() ?.isMultiCurrency == true diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt index 40ac56570b..fadde70778 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt @@ -26,7 +26,6 @@ import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency 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.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -277,9 +276,9 @@ internal class HotCryptoModel @Inject constructor( private fun updateCryptoCurrency( cryptoCurrency: CryptoCurrency, userWallet: UserWallet, - account: AccountStatus, + account: AccountStatus.CryptoPortfolio, ): CryptoCurrency? { - val derivationIndex = account.account.derivationIndex ?: return null + val derivationIndex = account.account.derivationIndex val blockchain = cryptoCurrency.network.toBlockchain() val network = networkFactory.create( diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt new file mode 100644 index 0000000000..0f2ae969eb --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/TangemPayFeatureToggles.kt @@ -0,0 +1,5 @@ +package com.tangem.features.tangempay + +interface TangemPayFeatureToggles { + val isTangemPayAccountsRefactorEnabled: Boolean +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt new file mode 100644 index 0000000000..30897aff29 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/DefaultTangemPayFeatureToggles.kt @@ -0,0 +1,10 @@ +package com.tangem.features.tangempay + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager + +internal class DefaultTangemPayFeatureToggles( + private val featureTogglesManager: FeatureTogglesManager, +) : TangemPayFeatureToggles { + override val isTangemPayAccountsRefactorEnabled + get() = featureTogglesManager.isFeatureEnabled("TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED") +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt index 2b5736147e..58f5d9ef84 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayDetailsComponent.kt @@ -45,7 +45,6 @@ internal class TangemPayDetailsComponent( appComponentContext = child("txHistoryComponent"), params = DefaultTangemPayTxHistoryComponent.Params( userWalletId = params.userWalletId, - customerWalletAddress = params.config.customerWalletAddress, uiActions = model, ), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt index bfd618dfa5..d27e63ad51 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/txHistory/DefaultTangemPayTxHistoryComponent.kt @@ -25,7 +25,6 @@ internal class DefaultTangemPayTxHistoryComponent( data class Params( val userWalletId: UserWalletId, - val customerWalletAddress: String, val uiActions: TangemPayTxHistoryUiActions, ) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt new file mode 100644 index 0000000000..a6ea142d28 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsModule.kt @@ -0,0 +1,21 @@ +package com.tangem.features.tangempay.di + +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.features.tangempay.DefaultTangemPayFeatureToggles +import com.tangem.features.tangempay.TangemPayFeatureToggles +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object TangemPayDetailsModule { + + @Provides + @Singleton + fun provideTangemPayFeatureToggles(featureTogglesManager: FeatureTogglesManager): TangemPayFeatureToggles { + return DefaultTangemPayFeatureToggles(featureTogglesManager) + } +} \ 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 e1433f4d17..47b229ce5d 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 @@ -31,7 +31,6 @@ internal class TangemPayTxHistoryModel @Inject constructor( private val listManager = TangemPayTxHistoryListManager( repository = tangemPayTxHistoryRepository, dispatchers = dispatchers, - customerWalletAddress = params.customerWalletAddress, txHistoryUiActions = params.uiActions, ) @@ -104,7 +103,7 @@ internal class TangemPayTxHistoryModel @Inject constructor( } private fun loadMoreItems(): Boolean { - modelScope.launch { listManager.loadMore(params.customerWalletAddress) } + modelScope.launch { listManager.loadMore() } return true } 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 f1b971367c..fa798e16bc 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 @@ -22,7 +22,6 @@ private typealias TangemPayTxHistoryBatchAction = BatchAction walletBalanceFetcher( - params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId), + params = WalletBalanceFetcher.Params( + userWalletId = userWallet.walletId, + isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, + ), ) } } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt index 6c53807de1..b6b47ea825 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -145,7 +145,7 @@ internal class AccountItemsDelegate @Inject constructor( return this.sortedBy { positionByAccountId[it.id] ?: Int.MAX_VALUE } } - private fun openAccountDetails(account: Account) { + private fun openAccountDetails(account: Account.CryptoPortfolio) { router.push(AppRoute.AccountDetails(account)) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt index 5e65dfb36a..a54705233c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletTangemPayAnalyticsEventSender.kt @@ -2,7 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.analytics.utils import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.pay.model.CustomerInfo +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.pay.model.MainScreenCustomerInfo import com.tangem.domain.pay.model.OrderStatus import com.tangem.domain.tangempay.TangemPayAnalyticsEvents @@ -29,7 +29,7 @@ internal class WalletTangemPayAnalyticsEventSender @Inject constructor( // ignore cancelled state on analytics customerInfo.orderStatus == OrderStatus.CANCELED -> return // ignore kyc not approved state on analytics - customerInfo.info.kycStatus != CustomerInfo.KycStatus.APPROVED -> return + customerInfo.info.kycStatus != KycStatus.APPROVED -> return cardInfo != null && productInstance != null -> return else -> TangemPayAnalyticsEvents.IssuingBannerDisplayed() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt index 46e14c7693..10de4aeeb6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletContentFetcher.kt @@ -1,7 +1,8 @@ package com.tangem.feature.wallet.presentation.wallet.domain -import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.tokens.wallet.WalletBalanceFetcher +import com.tangem.features.tangempay.TangemPayFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveInAndJoin @@ -27,6 +28,7 @@ import javax.inject.Singleton internal class WalletContentFetcher @Inject constructor( private val walletBalanceFetcher: WalletBalanceFetcher, private val dispatchers: CoroutineDispatcherProvider, + private val tangemPayFeatureToggles: TangemPayFeatureToggles, ) { private val fetchingJobMap = ConcurrentHashMap() @@ -64,8 +66,12 @@ internal class WalletContentFetcher @Inject constructor( Timber.d("Start fetching for $userWalletId") val maybeResult = launch { - walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId)) - .onLeft(Timber::e) + walletBalanceFetcher( + params = WalletBalanceFetcher.Params( + userWalletId = userWalletId, + isPaymentAccountRefactorEnabled = tangemPayFeatureToggles.isTangemPayAccountsRefactorEnabled, + ), + ).onLeft(Timber::e) } .saveInAndJoin(jobHolder) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt index c07d25bdfc..2839523857 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TangemPayUpdateInfoStateTransformer.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.pay.model.CustomerInfo.CardInfo @@ -16,8 +17,6 @@ import com.tangem.feature.wallet.child.wallet.model.intents.TangemPayIntents 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.WalletState -import com.tangem.domain.pay.model.CustomerInfo.KycStatus.APPROVED -import com.tangem.domain.pay.model.CustomerInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import java.util.Currency @@ -55,7 +54,7 @@ internal class TangemPayUpdateInfoStateTransformer( // when statement copied to WalletTangemPayAnalyticsEventSender. Be careful when editing. return when { value.orderStatus == OrderStatus.CANCELED -> createCancelledState(customerId) - value.info.kycStatus != APPROVED && !value.info.customerId.isNullOrEmpty() -> + value.info.kycStatus != KycStatus.APPROVED && !value.info.customerId.isNullOrEmpty() -> createKycInProgressState(kycStatus = value.info.kycStatus, customerId = customerId) cardInfo != null && productInstance != null -> getCardInfoState(customerId, cardInfo, productInstance) @@ -79,7 +78,6 @@ internal class TangemPayUpdateInfoStateTransformer( cardId = productInstance.cardId, isPinSet = cardInfo.isPinSet, cardFrozenState = cardFrozenState, - customerWalletAddress = cardInfo.customerWalletAddress, cardNumberEnd = cardInfo.lastFourDigits, chainId = POLYGON_CHAIN_ID, ), @@ -94,25 +92,24 @@ internal class TangemPayUpdateInfoStateTransformer( } } - private fun createKycInProgressState(kycStatus: CustomerInfo.KycStatus, customerId: String): TangemPayState = - Progress( - title = TextReference.Res(R.string.tangempay_payment_account), - description = when (kycStatus) { - CustomerInfo.KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) - else -> TextReference.Res(R.string.tangempay_kyc_in_progress) - }, - buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), - iconRes = R.drawable.ic_promo_kyc_36, - onButtonClick = { - when (kycStatus) { - CustomerInfo.KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( - userWalletId = userWalletId, - customerId = customerId, - ) - else -> tangemPayClickIntents.onKycProgressClicked(userWalletId) - } - }, - ) + private fun createKycInProgressState(kycStatus: KycStatus, customerId: String): TangemPayState = Progress( + title = TextReference.Res(R.string.tangempay_payment_account), + description = when (kycStatus) { + KycStatus.REJECTED -> TextReference.Res(R.string.tangempay_kyc_has_failed) + else -> TextReference.Res(R.string.tangempay_kyc_in_progress) + }, + buttonText = TextReference.Res(R.string.tangempay_kyc_in_progress_notification_button), + iconRes = R.drawable.ic_promo_kyc_36, + onButtonClick = { + when (kycStatus) { + KycStatus.REJECTED -> tangemPayClickIntents.onKycRejectedClicked( + userWalletId = userWalletId, + customerId = customerId, + ) + else -> tangemPayClickIntents.onKycProgressClicked(userWalletId) + } + }, + ) private fun createIssueProgressState(): TangemPayState = Progress( title = TextReference.Res(R.string.tangempay_payment_account), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt index b8f7e9a587..fbf1117370 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/visa/TangemPayMainScreenBlock.kt @@ -8,10 +8,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import com.tangem.common.ui.R import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.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.WalletNotification.Warning.TangemPayRefreshNeeded import com.tangem.feature.wallet.presentation.wallet.ui.components.singlecurrency.TangemPayCardMainBlock @Composable @@ -36,6 +38,17 @@ private fun TangemPayMainScreenBlockPreview() { TangemThemePreview { Column(verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) { TangemPayMainScreenBlock(state = TangemPayState.Loading, isBalanceHidden = false) + TangemPayMainScreenBlock( + state = TangemPayState.RefreshNeeded( + TangemPayRefreshNeeded( + tangemIcon = R.drawable.ic_tangem_24, + buttonText = resourceReference(id = R.string.home_button_scan), + onRefreshClick = {}, + shouldShowProgress = false, + ), + ), + isBalanceHidden = false, + ) TangemPayMainScreenBlock(state = TangemPayState.ExposedDevice, isBalanceHidden = false) From 0f961af7cec24fe38da60feed7c34c3453a18744 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 12:52:01 +0500 Subject: [PATCH 37/97] Updated on 2026-08-14 --- .../tangem/core/ui/ds/badge/TangemBadge.kt | 53 +++++++--- .../core/ui/ds/row/TangemRowContainer.kt | 22 +++- .../core/ui/ds/row/token/TangemTokenRow.kt | 34 +++--- .../core/ui/ds/row/token/TangemTokenRowUM.kt | 3 +- .../internal/TangemTokenRowPreviewData.kt | 2 +- .../internal/TokenRowEndBottomContent.kt | 100 ------------------ ...EndTopContent.kt => TokenRowEndContent.kt} | 59 +++++++++-- .../row/token/internal/TokenRowPromoBanner.kt | 69 ++++++------ .../tangem/core/ui/res/TangemColorPalette.kt | 31 +++++- .../com/tangem/core/ui/res/TangemColors2.kt | 74 ++++++++++--- .../tangem/core/ui/res/TangemThemeRedesign.kt | 38 +++++-- .../main/res/drawable/shape_triangular.xml | 9 ++ 12 files changed, 294 insertions(+), 200 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt rename core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/{TokenRowEndTopContent.kt => TokenRowEndContent.kt} (64%) create mode 100644 core/ui/src/main/res/drawable/shape_triangular.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt index 7f9f95b4d3..aba734064b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt @@ -72,14 +72,14 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { */ @Composable fun TangemBadge( - text: TextReference, modifier: Modifier = Modifier, + text: TextReference? = null, @DrawableRes iconRes: Int? = null, size: TangemBadgeSize = X9, shape: TangemBadgeShape = TangemBadgeShape.Default, color: TangemBadgeColor = TangemBadgeColor.Gray, type: TangemBadgeType = TangemBadgeType.Solid, - iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start, + iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.None, onClick: (() -> Unit)? = null, ) { val iconColor = getIconColor(type = type, color = color) @@ -94,7 +94,7 @@ fun TangemBadge( .clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }), ) { AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start, + visible = iconRes != null && iconPosition != TangemBadgeIconPosition.End, modifier = Modifier.size(size = size.toContentSize()), label = "Start Icon Visibility", ) { @@ -105,13 +105,18 @@ fun TangemBadge( tint = iconColor, ) } - Text( - text = text.resolveReference(), - style = size.toTextStyle(), - maxLines = 1, - color = getTextColor(type = type, color = color), - ) - + AnimatedVisibility( + visible = text != null, + label = "Text Visibility", + ) { + val wrappedText = remember(this) { requireNotNull(text) } + Text( + text = wrappedText.resolveReference(), + style = size.toTextStyle(), + maxLines = 1, + color = getTextColor(type = type, color = color), + ) + } AnimatedVisibility( visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End, modifier = Modifier.size(size = size.toContentSize()), @@ -178,14 +183,17 @@ enum class TangemBadgeSize { X4 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 6.dp, end = 6.dp) } X6 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 12.dp, end = 12.dp) } X9 -> when (position) { TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp) TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp) + TangemBadgeIconPosition.None -> PaddingValues(start = 16.dp, end = 16.dp) } } @@ -222,6 +230,7 @@ enum class TangemBadgeSize { enum class TangemBadgeIconPosition { Start, End, + None, } /** @@ -240,6 +249,7 @@ enum class TangemBadgeColor { Blue, Red, Gray, + Green, } @ReadOnlyComposable @@ -258,6 +268,12 @@ private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when -> TangemTheme.colors2.markers.iconRed TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant } + TangemBadgeColor.Green -> when (type) { + TangemBadgeType.Outline, + TangemBadgeType.Tinted, + -> TangemTheme.colors2.markers.iconGreen + TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant + } } @ReadOnlyComposable @@ -276,8 +292,15 @@ private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when -> TangemTheme.colors2.markers.textRed TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant } + TangemBadgeColor.Green -> when (type) { + TangemBadgeType.Outline, + TangemBadgeType.Tinted, + -> TangemTheme.colors2.markers.textGreen + TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant + } } +@Suppress("CyclomaticComplexMethod") @ReadOnlyComposable @Composable private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) { @@ -286,6 +309,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundSolidGreen }, ) TangemBadgeType.Tinted -> background( @@ -293,6 +317,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.backgroundTintedGreen }, ) TangemBadgeType.Outline -> { @@ -301,6 +326,7 @@ private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadg TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed + TangemBadgeColor.Green -> TangemTheme.colors2.markers.borderTintedGreen }, shape = shape, width = 1.dp, @@ -320,16 +346,16 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl .background(TangemTheme.colors2.surface.level1) .padding(8.dp), ) { - repeat(2) { yIndex -> + repeat(3) { yIndex -> Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { repeat(TangemBadgeType.entries.size) { index -> TangemBadge( - text = stringReference("Title"), + text = stringReference("Title").takeIf { yIndex < 2 }, iconRes = R.drawable.ic_information_24, type = TangemBadgeType.entries[index], color = params, shape = TangemBadgeShape.entries[yIndex % 2], - iconPosition = TangemBadgeIconPosition.entries[yIndex % 2], + iconPosition = TangemBadgeIconPosition.entries[yIndex], ) } } @@ -344,6 +370,7 @@ private class TangemBadgePreviewProvider : PreviewParameterProvider = persistentListOf(), + val startIcons: ImmutableList = persistentListOf(), + val endIcons: ImmutableList = persistentListOf(), val priceChangeUM: PriceChangeState = PriceChangeState.Unknown, ) : EndContentUM() diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt index ce68a8771d..a18f605cca 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TangemTokenRowPreviewData.kt @@ -142,7 +142,7 @@ internal object TangemTokenRowPreviewData { ) }), ), - icons = persistentListOf( + startIcons = persistentListOf( TangemIconUM.Icon(R.drawable.ic_staking_mini_10), TangemIconUM.Icon(R.drawable.ic_attention_12), TangemIconUM.Icon(R.drawable.ic_error_sync_24), diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt deleted file mode 100644 index 376e670bda..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndBottomContent.kt +++ /dev/null @@ -1,100 +0,0 @@ -package com.tangem.core.ui.ds.row.token.internal - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.width -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.marketprice.PriceChangeState -import com.tangem.core.ui.components.text.applyBladeBrush -import com.tangem.core.ui.ds.row.token.TangemTokenRowUM -import com.tangem.core.ui.extensions.orMaskWithStars -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreviewRedesign - -@Composable -internal fun TokenRowEndBottomContent( - endContentUM: TangemTokenRowUM.EndContentUM, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - when (endContentUM) { - is TangemTokenRowUM.EndContentUM.Content -> Content( - modifier = modifier, - endContentUM = endContentUM, - isBalanceHidden = isBalanceHidden, - ) - TangemTokenRowUM.EndContentUM.Empty -> Unit - TangemTokenRowUM.EndContentUM.Loading -> TextShimmer( - style = TangemTheme.typography2.captionSemibold12, - modifier = modifier.width(TangemTheme.dimens2.x10), - radius = TangemTheme.dimens2.x25, - ) - } -} - -@Composable -private fun Content( - endContentUM: TangemTokenRowUM.EndContentUM.Content, - isBalanceHidden: Boolean, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( - isEnabled = endContentUM.isFlickering, - textColor = if (endContentUM.isAvailable) { - TangemTheme.colors2.text.neutral.secondary - } else { - TangemTheme.colors2.text.status.disabled - }, - ), - ) - - when (val priceChangeUM = endContentUM.priceChangeUM) { - is PriceChangeState.Content -> TokenRowPriceChangeContent( - priceChangeState = priceChangeUM, - isFlickering = endContentUM.isFlickering, - isAvailable = endContentUM.isAvailable, - ) - PriceChangeState.Unknown -> Unit - } - } -} - -// region Preview -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun TokenRowEndBottomContent_Preview( - @PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM, -) { - TangemThemePreviewRedesign { - TokenRowEndBottomContent( - endContentUM = params, - isBalanceHidden = false, - ) - } -} - -private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - TangemTokenRowPreviewData.bottomEndContentUM, - ) -} -// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt similarity index 64% rename from core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt rename to core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt index 131497424f..934b33aa8c 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndTopContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowEndContent.kt @@ -8,15 +8,18 @@ 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.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.TextStyle 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.util.fastForEach import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.orMaskWithStars @@ -25,9 +28,11 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable -internal fun TokenRowEndTopContent( +internal fun TokenRowEndContent( endContentUM: TangemTokenRowUM.EndContentUM, isBalanceHidden: Boolean, + textStyle: TextStyle, + textColor: Color, modifier: Modifier = Modifier, ) { when (endContentUM) { @@ -35,11 +40,13 @@ internal fun TokenRowEndTopContent( modifier = modifier, endContentUM = endContentUM, isBalanceHidden = isBalanceHidden, + textStyle = textStyle, + textColor = textColor, ) TangemTokenRowUM.EndContentUM.Empty -> Unit TangemTokenRowUM.EndContentUM.Loading -> TextShimmer( - style = TangemTheme.typography2.bodySemibold16, - modifier = modifier.width(TangemTheme.dimens2.x18), + style = textStyle, + modifier = modifier.width(TangemTheme.dimens2.x10), radius = TangemTheme.dimens2.x25, ) } @@ -48,6 +55,8 @@ internal fun TokenRowEndTopContent( @Composable private fun Content( endContentUM: TangemTokenRowUM.EndContentUM.Content, + textStyle: TextStyle, + textColor: Color, isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { @@ -56,14 +65,14 @@ private fun Content( verticalAlignment = Alignment.CenterVertically, ) { AnimatedVisibility( - visible = endContentUM.icons.isNotEmpty(), + visible = endContentUM.startIcons.isNotEmpty(), ) { Row( modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), ) { - endContentUM.icons.fastForEach { icon -> + endContentUM.startIcons.fastForEach { icon -> Icon( modifier = Modifier.size(TangemTheme.dimens2.x3), painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)), @@ -75,11 +84,11 @@ private fun Content( } Text( - modifier = Modifier, text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), maxLines = 1, overflow = TextOverflow.Ellipsis, - style = TangemTheme.typography2.bodySemibold16.applyBladeBrush( + color = textColor, + style = textStyle.applyBladeBrush( isEnabled = endContentUM.isFlickering, textColor = if (endContentUM.isAvailable) { TangemTheme.colors2.text.neutral.primary @@ -88,6 +97,34 @@ private fun Content( }, ), ) + + AnimatedVisibility( + visible = endContentUM.endIcons.isNotEmpty(), + ) { + Row( + modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + ) { + endContentUM.endIcons.fastForEach { icon -> + Icon( + modifier = Modifier.size(TangemTheme.dimens2.x3), + painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)), + tint = icon.tintReference(), + contentDescription = null, + ) + } + } + } + + when (val priceChangeUM = endContentUM.priceChangeUM) { + is PriceChangeState.Content -> TokenRowPriceChangeContent( + priceChangeState = priceChangeUM, + isFlickering = endContentUM.isFlickering, + isAvailable = endContentUM.isAvailable, + ) + PriceChangeState.Unknown -> Unit + } } } @@ -95,13 +132,15 @@ private fun Content( @Composable @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun TokenRowEndTopContent_Preview( +private fun TokenRowEndContent_Preview( @PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM, ) { TangemThemePreviewRedesign { - TokenRowEndTopContent( + TokenRowEndContent( endContentUM = params, isBalanceHidden = false, + textColor = TangemTheme.colors2.text.neutral.primary, + textStyle = TangemTheme.typography2.captionSemibold12, ) } } @@ -109,7 +148,7 @@ private fun TokenRowEndTopContent_Preview( private class TokenRowEndContentPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - TangemTokenRowPreviewData.topEndContentUM, + TangemTokenRowPreviewData.bottomEndContentUM, ) } // endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt index 93845dea18..6ab252c5b8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt @@ -3,15 +3,12 @@ package com.tangem.core.ui.ds.row.token.internal import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -19,6 +16,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.ds.badge.* import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -40,55 +38,52 @@ internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.C LaunchedEffect(promoBannerUM) { promoBannerUM.onPromoShown() } - val bgColor = TangemTheme.colors.control.default - Column(modifier = modifier) { + val bgColor = TangemTheme.colors2.markers.backgroundTintedGreen + Column( + modifier = modifier, + ) { + Icon( + painter = painterResource(id = R.drawable.shape_triangular), + contentDescription = null, + tint = bgColor, + modifier = Modifier.padding(start = TangemTheme.dimens2.x5), + ) Row( modifier = Modifier .background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4)) .clickable(onClick = promoBannerUM.onPromoBannerClick) - .padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2) - .fillMaxWidth(), + .padding( + start = TangemTheme.dimens2.x2_5, + end = TangemTheme.dimens2.x0_5, + top = TangemTheme.dimens2.x0_5, + bottom = TangemTheme.dimens2.x0_5, + ), verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), ) { Icon( imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24), contentDescription = null, - tint = TangemTheme.colors.icon.accent, + tint = TangemTheme.colors2.markers.textGreen, modifier = Modifier - .padding(end = TangemTheme.dimens2.x2) - .size(TangemTheme.dimens2.x4), + .padding(vertical = TangemTheme.dimens2.x0_5) + .size(TangemTheme.dimens2.x3), ) Text( text = promoBannerUM.title.resolveReference(), - style = TangemTheme.typography2.captionSemibold12, - color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.captionSemibold11, + color = TangemTheme.colors2.markers.textGreen, modifier = Modifier - .weight(1f) - .padding(end = TangemTheme.dimens2.x2), + .padding(vertical = TangemTheme.dimens2.x0_5), ) - Icon( - painter = painterResource(id = R.drawable.ic_close_24), - contentDescription = null, - tint = TangemTheme.colors2.text.neutral.secondary, - modifier = Modifier - .size(TangemTheme.dimens2.x4) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = ripple(bounded = false), - onClick = { promoBannerUM.onCloseClick() }, - ), - ) - } - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - Icon( - painter = painterResource(id = R.drawable.ic_rectangle_bottom), - contentDescription = null, - tint = bgColor, - modifier = Modifier - .size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2), + TangemBadge( + size = TangemBadgeSize.X4, + shape = TangemBadgeShape.Rounded, + color = TangemBadgeColor.Green, + type = TangemBadgeType.Tinted, + iconRes = R.drawable.ic_close_24, + iconPosition = TangemBadgeIconPosition.None, + onClick = promoBannerUM.onCloseClick, ) } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 150d6586d3..f724e1f8db 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -59,20 +59,45 @@ object TangemColorPalette { val DarkGreen = Color(0xFF06311F) // endregion Green - // region Blue + // region Azure val Azure = Color(0xFF0099FF) + val Azure_50 = Color(0x800099FF) + val Azure_10 = Color(0x1A0099FF) // endregion Blue - // region Red + // region Amaranth val Amaranth = Color(0xFFFF3333) + val Amaranth_50 = Color(0x80FF3333) + val Amaranth_20 = Color(0x33FF3333) + val Amaranth_10 = Color(0x1AFF3333) + // endregion Amaranth + + // region Flamingo val Flamingo = Color(0xFFFF5B5B) - // endregion Red + val Flamingo_50 = Color(0x80FF5B5B) + val Flamingo_20 = Color(0x33FF5B5B) + val Flamingo_10 = Color(0x1AFF5B5B) + // endregion Flamingo // region Yellow val Tangerine = Color(0xFFFFB71B) val Mustard = Color(0xFFFDDE55) // endregion Yellow + // region Emerald + val Emerald = Color(0xFF34DF12) + val Emerald_50 = Color(0x8034DF12) + val Emerald_20 = Color(0x3334DF12) + val Emerald_10 = Color(0x1A34DF12) + // endregion Emerald + + // region Eucalyptus + val Eucalyptus = Color(0xFF0C9F3D) + val Eucalyptus_50 = Color(0x800C9F3D) + val Eucalyptus_20 = Color(0x330C9F3D) + val Eucalyptus_10 = Color(0x1A0C9F3D) + // endregion Emerald + // region Overlay val Overlay1 = Color(0x66000000) val Overlay2 = Color(0xB2000000) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt index c3d2a23e2a..904dc3170b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt @@ -448,24 +448,36 @@ class TangemColors2 internal constructor( @Stable class Markers internal constructor( - backgroundSolidGray: Color, - backgroundDisabled: Color, - backgroundSolidBlue: Color, - textGray: Color, textDisabled: Color, - iconGray: Color, iconDisabled: Color, + backgroundDisabled: Color, + textGray: Color, + iconGray: Color, borderGray: Color, - backgroundTintedBlue: Color, + backgroundSolidGray: Color, + backgroundTintedGray: Color, textBlue: Color, + iconBlue: Color, + borderTintedBlue: Color, + backgroundSolidBlue: Color, + backgroundTintedBlue: Color, + textRed: Color, + iconRed: Color, + borderTintedRed: Color, backgroundSolidRed: Color, backgroundTintedRed: Color, - iconBlue: Color, - iconRed: Color, - textRed: Color, - backgroundTintedGray: Color, - borderTintedBlue: Color, - borderTintedRed: Color, + textGreen: Color, + iconGreen: Color, + borderTintedGreen: Color, + borderSolidColor: Color, + backgroundTintedGreen: Color, + backgroundSolidGreen: Color, + textGreenAlt: Color, + iconGreenAlt: Color, + borderTintedGreenAlt: Color, + borderSolidColorAlt: Color, + backgroundTintedGreenAlt: Color, + backgroundSolidGreenAlt: Color, ) { var backgroundSolidGray by mutableStateOf(backgroundSolidGray) private set @@ -504,6 +516,32 @@ class TangemColors2 internal constructor( var borderTintedRed by mutableStateOf(borderTintedRed) private set + var textGreen by mutableStateOf(textGreen) + private set + var iconGreen by mutableStateOf(iconGreen) + private set + var borderTintedGreen by mutableStateOf(borderTintedGreen) + private set + var borderSolidColor by mutableStateOf(borderSolidColor) + private set + var backgroundTintedGreen by mutableStateOf(backgroundTintedGreen) + private set + var backgroundSolidGreen by mutableStateOf(backgroundSolidGreen) + private set + var textGreenAlt by mutableStateOf(textGreenAlt) + private set + var iconGreenAlt by mutableStateOf(iconGreenAlt) + private set + var borderTintedGreenAlt by mutableStateOf(borderTintedGreenAlt) + private set + var borderSolidColorAlt by mutableStateOf(borderSolidColorAlt) + private set + + var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreen) + private set + var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreen) + private set + fun update(other: Markers) { backgroundSolidGray = other.backgroundSolidGray backgroundDisabled = other.backgroundDisabled @@ -523,6 +561,18 @@ class TangemColors2 internal constructor( backgroundTintedGray = other.backgroundTintedGray borderTintedBlue = other.borderTintedBlue borderTintedRed = other.borderTintedRed + textGreen = other.textGreen + iconGreen = other.iconGreen + borderTintedGreen = other.borderTintedGreen + borderSolidColor = other.borderSolidColor + backgroundTintedGreen = other.backgroundTintedGreen + backgroundSolidGreen = other.backgroundSolidGreen + textGreenAlt = other.textGreenAlt + iconGreenAlt = other.iconGreenAlt + borderTintedGreenAlt = other.borderTintedGreenAlt + borderSolidColorAlt = other.borderSolidColorAlt + backgroundTintedGreenAlt = other.backgroundTintedGreenAlt + backgroundSolidGreenAlt = other.backgroundSolidGreenAlt } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index b838ad7601..93a3647d65 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -154,16 +154,28 @@ private fun lightThemeColors2(): TangemColors2 { iconGray = TangemColorPalette.Dark1, iconDisabled = TangemColorPalette.Light2, borderGray = TangemColorPalette.Light3, - backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), + backgroundTintedBlue = TangemColorPalette.Azure_10, textBlue = text.status.accent, backgroundSolidRed = TangemColorPalette.Amaranth, - backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + backgroundTintedRed = TangemColorPalette.Amaranth_10, iconBlue = TangemColorPalette.Azure, iconRed = TangemColorPalette.Amaranth, textRed = TangemColorPalette.Amaranth, backgroundTintedGray = TangemColorPalette.Dark6.copy(alpha = 0.1f), - borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), - borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + borderTintedBlue = TangemColorPalette.Azure_10, + borderTintedRed = TangemColorPalette.Amaranth_10, + textGreen = TangemColorPalette.Emerald, + iconGreen = TangemColorPalette.Emerald, + borderTintedGreen = TangemColorPalette.Emerald_10, + borderSolidColor = TangemColorPalette.Emerald_50, + backgroundTintedGreen = TangemColorPalette.Emerald_10, + backgroundSolidGreen = TangemColorPalette.Emerald, + textGreenAlt = TangemColorPalette.Eucalyptus, + iconGreenAlt = TangemColorPalette.Eucalyptus, + borderTintedGreenAlt = TangemColorPalette.Eucalyptus_10, + borderSolidColorAlt = TangemColorPalette.Eucalyptus_50, + backgroundTintedGreenAlt = TangemColorPalette.Eucalyptus_10, + backgroundSolidGreenAlt = TangemColorPalette.Eucalyptus, ) val tabs = TangemColors2.Tabs( textPrimary = TangemColorPalette.Light2, @@ -304,7 +316,7 @@ private fun darkThemeColors2(): TangemColors2 { iconGray = TangemColorPalette.Dark2, iconDisabled = TangemColorPalette.Dark5, borderGray = TangemColorPalette.White.copy(alpha = 0.2f), - backgroundTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), + backgroundTintedBlue = TangemColorPalette.Azure_10, textBlue = text.status.accent, backgroundSolidRed = TangemColorPalette.Amaranth, backgroundTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), @@ -312,8 +324,20 @@ private fun darkThemeColors2(): TangemColors2 { iconRed = TangemColorPalette.Flamingo, textRed = TangemColorPalette.Flamingo, backgroundTintedGray = TangemColorPalette.White.copy(alpha = 0.1f), - borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f), - borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f), + borderTintedBlue = TangemColorPalette.Azure_10, + borderTintedRed = TangemColorPalette.Amaranth_10, + textGreen = TangemColorPalette.Emerald, + iconGreen = TangemColorPalette.Emerald, + borderTintedGreen = TangemColorPalette.Emerald_10, + borderSolidColor = TangemColorPalette.Emerald_50, + backgroundTintedGreen = TangemColorPalette.Emerald_10, + backgroundSolidGreen = TangemColorPalette.Emerald, + textGreenAlt = TangemColorPalette.Emerald, + iconGreenAlt = TangemColorPalette.Emerald, + borderTintedGreenAlt = TangemColorPalette.Emerald_10, + borderSolidColorAlt = TangemColorPalette.Emerald_50, + backgroundTintedGreenAlt = TangemColorPalette.Emerald_10, + backgroundSolidGreenAlt = TangemColorPalette.Emerald, ) val tabs = TangemColors2.Tabs( textPrimary = TangemColorPalette.Dark4, diff --git a/core/ui/src/main/res/drawable/shape_triangular.xml b/core/ui/src/main/res/drawable/shape_triangular.xml new file mode 100644 index 0000000000..c4baf11fb9 --- /dev/null +++ b/core/ui/src/main/res/drawable/shape_triangular.xml @@ -0,0 +1,9 @@ + + + From 8ce04b394989396e59ee20ffd87058d21e19c3d1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 20 Feb 2026 12:52:07 +0500 Subject: [PATCH 38/97] Updated on 2026-08-14 --- .../core/ui/ds/row/TangemRowContainer.kt | 2 +- .../tangem/core/ui/res/TangemColorPalette.kt | 4 +- .../com/tangem/core/ui/res/TangemColors2.kt | 4 +- .../SetTokenListErrorTransformer.kt | 17 +- .../transformers/SetTokenListTransformer.kt | 34 +- .../converter/EarnApyConverter.kt | 139 +++++ .../WalletTokensListUMTransformer.kt | 521 ++++++++++++++++++ .../YieldSupplyPromoBannerConverter.kt | 30 + .../subscribers/AccountListSubscriber.kt | 32 +- .../subscribers/BasicAccountListSubscriber.kt | 24 + .../subscribers/BasicTokenListSubscriber.kt | 1 + .../MultiCurrencyAccountContent.kt | 2 +- .../multicurrency/MultiCurrencyContent.kt | 309 ++++++++++- 13 files changed, 1092 insertions(+), 27 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt index f31b477a3b..d917b39b7f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt @@ -16,7 +16,7 @@ import kotlin.math.max /** * A custom layout composable that arranges its children in a row with specific layout IDs. */ -internal enum class TangemRowLayoutId { +enum class TangemRowLayoutId { HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP, EXTRA_BOTTOM } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index f724e1f8db..0906b9626d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -63,7 +63,7 @@ object TangemColorPalette { val Azure = Color(0xFF0099FF) val Azure_50 = Color(0x800099FF) val Azure_10 = Color(0x1A0099FF) - // endregion Blue + // endregion Azure // region Amaranth val Amaranth = Color(0xFFFF3333) @@ -96,7 +96,7 @@ object TangemColorPalette { val Eucalyptus_50 = Color(0x800C9F3D) val Eucalyptus_20 = Color(0x330C9F3D) val Eucalyptus_10 = Color(0x1A0C9F3D) - // endregion Emerald + // endregion Eucalyptus // region Overlay val Overlay1 = Color(0x66000000) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt index 904dc3170b..b9b0c54c51 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt @@ -537,9 +537,9 @@ class TangemColors2 internal constructor( var borderSolidColorAlt by mutableStateOf(borderSolidColorAlt) private set - var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreen) + var backgroundTintedGreenAlt by mutableStateOf(backgroundTintedGreenAlt) private set - var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreen) + var backgroundSolidGreenAlt by mutableStateOf(backgroundSolidGreenAlt) private set fun update(other: Markers) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 9ecb89cabb..6efe15284e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -7,10 +7,7 @@ import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -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.model.WalletUM +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.disableButtons import timber.log.Timber import java.math.BigDecimal @@ -53,7 +50,17 @@ internal class SetTokenListErrorTransformer( } override fun transform(walletUM: WalletUM): WalletUM { - return walletUM // todo redesign main + return when (walletUM) { + is WalletUM.Content -> { + walletUM.copy( + tokensListUM = WalletTokensListUM.Empty, + ) + } + is WalletUM.Locked -> { + Timber.w("Impossible to load tokens list for locked wallet") + walletUM + } + } } private fun WalletCardState.toLoadedState(): WalletCardState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 27a1171ace..d50fe9e0bb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -5,12 +5,10 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState -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.model.WalletUM +import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMTransformer import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import timber.log.Timber import java.math.BigDecimal @@ -23,6 +21,7 @@ internal class SetTokenListTransformer( private val yieldSupplyApyMap: Map = emptyMap(), private val stakingAvailabilityMap: Map = emptyMap(), private val shouldShowMainPromo: Boolean, + private val isAccountsModeEnabled: Boolean, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { @@ -47,7 +46,17 @@ internal class SetTokenListTransformer( } override fun transform(walletUM: WalletUM): WalletUM { - return walletUM // todo redesign main + return when (walletUM) { + is WalletUM.Content -> { + walletUM.copy( + tokensListUM = toLoadedState(), + ) + } + is WalletUM.Locked -> { + Timber.w("Impossible to load tokens list for locked wallet") + walletUM + } + } } private fun WalletCardState.toLoadedState(): WalletCardState { @@ -73,4 +82,19 @@ internal class SetTokenListTransformer( shouldShowMainPromo = shouldShowMainPromo, ).convert(value = this) } + + private fun toLoadedState(): WalletTokensListUM { + if (params !is TokenConverterParams.Account) return WalletTokensListUM.Empty + + return WalletTokensListUMTransformer( + selectedWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + yieldModuleApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = isAccountsModeEnabled, + expandedAccounts = params.expandedAccounts, + ).convert(value = params.accountList) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt new file mode 100644 index 0000000000..25f9f9df7b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/EarnApyConverter.kt @@ -0,0 +1,139 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.common.ui.R +import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.currency.yieldSupplyKey +import com.tangem.domain.models.staking.StakingBalance +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.domain.staking.model.StakingOption +import com.tangem.domain.staking.model.common.RewardInfo +import com.tangem.domain.staking.model.common.RewardType +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.EarnApyConverter.EarnApyInfo +import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.converter.Converter +import java.math.BigDecimal + +internal class EarnApyConverter( + val yieldModuleApyMap: Map, + val stakingApyMap: Map, +) : Converter { + + override fun convert(value: CryptoCurrencyStatus): EarnApyInfo? { + val token = value.currency as? CryptoCurrency.Token + if (token != null && yieldModuleApyMap.isNotEmpty()) { + val yieldSupplyApy = yieldModuleApyMap.entries.firstOrNull { apy -> + apy.key.equals( + other = token.yieldSupplyKey(), + ignoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId), + ) + }?.value + if (yieldSupplyApy != null) { + val isActive = value.value.yieldSupplyStatus?.isActive == false + return EarnApyInfo( + text = resourceReference( + R.string.yield_module_earn_badge, + wrappedList(yieldSupplyApy), + ), + isActive = isActive, + apy = yieldSupplyApy.toString(), + source = TokenItemStateConverter.ApySource.YIELD_SUPPLY, + ) + } + } + + if (stakingApyMap.isNotEmpty()) { + val stakingInfo = findStakingRate( + currencyStatus = value, + stakingApyMap = stakingApyMap, + ) + val rewardTypeRes = when (stakingInfo.rewardType) { + RewardType.APR -> R.string.staking_apr_earn_badge + RewardType.UNKNOWN, + RewardType.APY, + null, + -> R.string.yield_module_earn_badge + } + if (stakingInfo.rate != null) { + val apyString = stakingInfo.rate.format { percent(withPercentSign = false) } + return EarnApyInfo( + text = resourceReference( + rewardTypeRes, + wrappedList(apyString), + ), + isActive = stakingInfo.isActive, + apy = apyString, + source = TokenItemStateConverter.ApySource.STAKING, + ) + } + } + + return null + } + + private fun findStakingRate( + currencyStatus: CryptoCurrencyStatus, + stakingApyMap: Map, + ): StakingLocalInfo { + val stakingAvailability = stakingApyMap[currencyStatus.currency] as? StakingAvailability.Available + ?: return StakingLocalInfo(rate = null, isActive = false, rewardType = null) + + val stakingBalance = currencyStatus.value.stakingBalance as? StakingBalance.Data + val stakeKitBalance = stakingBalance as? StakingBalance.Data.StakeKit + val p2pEthPoolBalance = stakingBalance as? StakingBalance.Data.P2PEthPool + + val rateInfo = when (val stakingOptions = stakingAvailability.option) { + is StakingOption.P2PEthPool -> { + RewardInfo( + rate = stakingOptions.apy, + type = RewardType.APY, + ) + } + is StakingOption.StakeKit -> if (stakeKitBalance != null) { + val validatorsByAddress = stakingOptions.yield.validators.associateBy { it.address } + stakeKitBalance.balance.items + .mapNotNull { it.validatorAddress } + .firstNotNullOfOrNull { address -> + validatorsByAddress[address]?.rewardInfo + } ?: stakingOptions.yield.validators + .filter { it.preferred } + .mapNotNull { validator -> + validator.rewardInfo + } + .maxByOrNull { it.rate } + } else { + stakingOptions.yield.validators + .filter { it.preferred } + .mapNotNull { validator -> + validator.rewardInfo + } + .maxByOrNull { it.rate } + } + } + + return StakingLocalInfo( + rate = rateInfo?.rate, + isActive = stakeKitBalance != null || p2pEthPoolBalance != null, + rewardType = rateInfo?.type, + ) + } + + data class StakingLocalInfo( + val rate: BigDecimal?, + val isActive: Boolean, + val rewardType: RewardType?, + ) + + data class EarnApyInfo( + val text: TextReference?, + val isActive: Boolean, + val apy: String?, + val source: TokenItemStateConverter.ApySource, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt new file mode 100644 index 0000000000..652591e155 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt @@ -0,0 +1,521 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import androidx.compose.ui.text.SpanStyle +import com.tangem.common.getTotalCryptoAmount +import com.tangem.common.getTotalFiatAmount +import com.tangem.common.ui.account.AccountIconItemStateConverter +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM.EndContentUM +import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.format.bigdecimal.* +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountId +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.network.Network +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.addIf +import com.tangem.utils.extensions.orZero +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +@Suppress("LargeClass", "LongParameterList") +internal class WalletTokensListUMTransformer( + private val appCurrency: AppCurrency, + private val selectedWallet: UserWallet, + private val clickIntents: WalletClickIntents, + private val yieldModuleApyMap: Map, + private val isAccountsModeEnabled: Boolean, + private val expandedAccounts: Set, + stakingAvailabilityMap: Map, + shouldShowMainPromo: Boolean, +) : Converter { + + private val yieldSupplyPromoBannerConverter = YieldSupplyPromoBannerConverter( + yieldModuleApyMap, + shouldShowMainPromo, + ) + private val currencyToIconStateConverter = CryptoCurrencyToIconStateConverter() + private val earnApyConverter = EarnApyConverter( + yieldModuleApyMap = yieldModuleApyMap, + stakingApyMap = stakingAvailabilityMap, + ) + + override fun convert(value: AccountStatusList): WalletTokensListUM { + val promoCryptoCurrency = yieldSupplyPromoBannerConverter.convert2(value = value) + return if (value.accountStatuses.isEmpty()) { + WalletTokensListUM.Empty + } else { + val isCollapsable = value.accountStatuses.count { + it is AccountStatus.CryptoPortfolio && it.account.tokensCount > 0 + } > 1 + + val tokenListUM = value.accountStatuses + .filterIsInstance() + .asSequence() + .flatMap { accountStatus -> + if (isAccountsModeEnabled) { + val isExpanded = expandedAccounts.contains(accountStatus.account.accountId) + sequenceOf( + TokensListItemUM2.Portfolio( + tokenRowUM = toAccountRow(accountStatus, isExpanded), + isExpanded = isExpanded || !isCollapsable, + isCollapsable = isCollapsable, + tokenList = getTokenListItems( + accountStatus.tokenList, + promoCryptoCurrency, + ).toPersistentList(), + ), + ) + } else { + getTokenListItems(accountStatus.tokenList, promoCryptoCurrency) + } + }.toPersistentList() + + WalletTokensListUM.Content( + tokenList = tokenListUM, + organizeButtonUM = getOrganizeButtonUM(value), + ) + } + } + + private fun getTokenListItems( + tokenList: TokenList, + promoCryptoCurrency: CryptoCurrencyStatus?, + ): Sequence { + return when (tokenList) { + TokenList.Empty -> emptySequence() + is TokenList.GroupedByNetwork -> { + tokenList.groups.asSequence().flatMap { (network, currencies) -> + buildList { + add( + TokensListItemUM2.GroupTitle( + tokenRowUM = toGroupRow(network), + ), + ) + addAll( + currencies.asSequence().map { currencyStatus -> + val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id + TokensListItemUM2.Token( + tokenRowUM = toCurrencyRow( + currencyStatus = currencyStatus, + shouldShowPromo = shouldShowPromo, + ), + ) + }.toList(), + ) + } + } + } + is TokenList.Ungrouped -> { + tokenList.currencies.asSequence().map { currencyStatus -> + TokensListItemUM2.Token( + toCurrencyRow( + currencyStatus = currencyStatus, + shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id, + ), + ) + } + } + } + } + + private fun toAccountRow(accountStatus: AccountStatus.CryptoPortfolio, isExpanded: Boolean): TangemTokenRowUM { + val account = accountStatus.account + + val (topEndContent, bottomEndContent) = when (val accountBalance = accountStatus.tokenList.totalFiatBalance) { + TotalFiatBalance.Failed -> toFailedAccountRow() + is TotalFiatBalance.Loaded -> toLoadedAccountRow(accountStatus, accountBalance) + TotalFiatBalance.Loading -> EndContentUM.Loading to EndContentUM.Loading + } + + return TangemTokenRowUM.Content( + id = accountStatus.account.accountId.value, + headIconUM = TangemIconUM.Currency( + currencyIconState = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall).convert(account), + ), + titleUM = TangemTokenRowUM.TitleUM.Content( + text = account.accountName.toUM().value, + ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Content( + text = pluralReference( + R.plurals.common_tokens_count, + count = account.tokensCount, + formatArgs = wrappedList(account.tokensCount), + ), + ), + topEndContentUM = topEndContent, + bottomEndContentUM = bottomEndContent, + onItemClick = { + if (isExpanded) { + clickIntents.onAccountCollapseClick(account) + } else { + clickIntents.onAccountExpandClick(account) + } + }, + onItemLongClick = null, + ) + } + + private fun toFailedAccountRow(): Pair { + return EndContentUM.Content( + text = stringReference(StringsSigns.DASH_SIGN), + ) to EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + } + + private fun toLoadedAccountRow( + accountStatus: AccountStatus.CryptoPortfolio, + accountBalance: TotalFiatBalance.Loaded, + ): Pair { + val priceChange = accountStatus.priceChangeLce.getOrNull() + + return EndContentUM.Content( + text = accountBalance.amount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + ) to if (priceChange != null) { + val priceChangeType = PriceChangeType.fromBigDecimal(priceChange.value) + + EndContentUM.Content( + text = stringReference( + priceChange.value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + priceChangeUM = PriceChangeState.Content( + type = priceChangeType, + valueInPercent = priceChange.value.format { percent() }, + ), + ) + } else { + EndContentUM.Empty + } + } + + private fun toGroupRow(network: Network): TangemHeaderRowUM { + return TangemHeaderRowUM( + id = network.hashCode().toString(), + title = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(network.name), + ), + ) + } + + private fun toCurrencyRow(currencyStatus: CryptoCurrencyStatus, shouldShowPromo: Boolean): TangemTokenRowUM { + val earnApyInfo = earnApyConverter.convert(currencyStatus) + + return TangemTokenRowUM.Content( + id = currencyStatus.currency.id.value, + headIconUM = TangemIconUM.Currency( + currencyIconState = currencyToIconStateConverter.convert(currencyStatus), + ), + titleUM = toCurrencyRowTitle(currencyStatus, earnApyInfo), + subtitleUM = toCurrencyRowSubtitle(currencyStatus), + topEndContentUM = toCurrencyRowTopEnd(currencyStatus), + bottomEndContentUM = toCurrencyRowBottomEnd(currencyStatus), + promoBannerUM = toPromoBannerUM( + currencyStatus, + earnApyInfo.takeIf { shouldShowPromo }, + ), + onItemClick = when (currencyStatus.value) { + CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.MissedDerivation, + -> null + else -> { + { + clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) + } + } + }, + onItemLongClick = when (currencyStatus.value) { + CryptoCurrencyStatus.Loading -> null + else -> { + { + clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) + } + } + }, + ) + } + + private fun toCurrencyRowTitle( + currencyStatus: CryptoCurrencyStatus, + earnApyInfo: EarnApyConverter.EarnApyInfo?, + ): TangemTokenRowUM.TitleUM = when (val value = currencyStatus.value) { + is CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> { + TangemTokenRowUM.TitleUM.Content( + text = stringReference(currencyStatus.currency.name), + ) + } + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + TangemTokenRowUM.TitleUM.Content( + text = stringReference(currencyStatus.currency.name), + hasPending = value.hasCurrentNetworkTransactions, + badge = if (earnApyInfo != null && earnApyInfo.text != null) { + TangemBadgeUM( + type = TangemBadgeType.Solid, + color = when { + earnApyInfo.isActive -> TangemBadgeColor.Blue + else -> TangemBadgeColor.Gray + }, + shape = TangemBadgeShape.Rounded, + size = TangemBadgeSize.X4, + text = earnApyInfo.text, + onClick = if (earnApyInfo.apy != null) { + { + clickIntents.onApyLabelClick( + userWalletId = selectedWallet.walletId, + currencyStatus = currencyStatus, + apySource = earnApyInfo.source, + apy = earnApyInfo.apy, + ) + } + } else { + null + }, + ) + } else { + null + }, + ) + } + } + + private fun toCurrencyRowSubtitle(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM.SubtitleUM { + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loading -> TangemTokenRowUM.SubtitleUM.Loading + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> TangemTokenRowUM.SubtitleUM.Content( + text = stringReference( + currencyStatus.value.fiatRate.format { + fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) + }, + ), + priceChangeUM = PriceChangeState.Content( + type = PriceChangeType.fromBigDecimal(currencyStatus.value.priceChange.orZero()), + valueInPercent = currencyStatus.value.priceChange.format { percent() }, + ), + isFlickering = currencyStatus.value.isFlickering(), + ) + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.NoAmount, + -> TangemTokenRowUM.SubtitleUM.Empty + } + } + + private fun toCurrencyRowTopEnd(currencyStatus: CryptoCurrencyStatus): EndContentUM { + val yieldSupply = currencyStatus.value.yieldSupplyStatus + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> { + EndContentUM.Content( + text = currencyStatus.getTotalFiatAmount().formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + isFlickering = currencyStatus.value.isFlickering(), + startIcons = buildList { + addIf( + element = TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + condition = yieldSupply?.isActive == true && !yieldSupply.isAllowedToSpend, + ) + addIf( + element = TangemIconUM.Icon( + iconRes = R.drawable.ic_error_sync_default_24, + tintReference = { TangemTheme.colors2.graphic.neutral.tertiary }, + ), + condition = currencyStatus.value.sources.total == StatusSource.ONLY_CACHE, + ) + }.toImmutableList(), + ) + } + is CryptoCurrencyStatus.Loading -> EndContentUM.Loading + is CryptoCurrencyStatus.MissedDerivation -> EndContentUM.Content( + text = stringReference(StringsSigns.DASH_SIGN), + ) + is CryptoCurrencyStatus.Unreachable -> EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.NoAmount, + -> EndContentUM.Empty + } + } + + private fun toCurrencyRowBottomEnd(currencyStatus: CryptoCurrencyStatus): EndContentUM { + return when (currencyStatus.value) { + is CryptoCurrencyStatus.Loaded, + is CryptoCurrencyStatus.Custom, + is CryptoCurrencyStatus.NoQuote, + is CryptoCurrencyStatus.NoAccount, + -> EndContentUM.Content( + text = stringReference( + currencyStatus.getTotalCryptoAmount().format { + crypto(cryptoCurrency = currencyStatus.currency) + }, + ), + isFlickering = currencyStatus.value.isFlickering(), + ) + is CryptoCurrencyStatus.Loading -> EndContentUM.Loading + is CryptoCurrencyStatus.MissedDerivation -> EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_no_address, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.Unreachable -> EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is CryptoCurrencyStatus.NoAmount, + -> EndContentUM.Empty + } + } + + private fun toPromoBannerUM( + currencyStatus: CryptoCurrencyStatus, + earnApyInfo: EarnApyConverter.EarnApyInfo?, + ): TangemTokenRowUM.PromoBannerUM { + val currency = currencyStatus.currency + val isTokenCurrency = currency is CryptoCurrency.Token + val isCurrencyStatusLoaded = currencyStatus.value is CryptoCurrencyStatus.Loaded + val isApyInfoNotNull = earnApyInfo != null && earnApyInfo.apy != null + + if (!isTokenCurrency || !isCurrencyStatusLoaded || !isApyInfoNotNull) { + return TangemTokenRowUM.PromoBannerUM.Empty + } + + return TangemTokenRowUM.PromoBannerUM.Content( + title = resourceReference( + R.string.yield_module_main_screen_promo_banner_message, + wrappedList(earnApyInfo.apy), + ), + onPromoBannerClick = { + clickIntents.onYieldPromoClicked(currency) + clickIntents.onApyLabelClick( + userWalletId = selectedWallet.walletId, + currencyStatus = currencyStatus, + apySource = earnApyInfo.source, + apy = earnApyInfo.apy, + ) + }, + onCloseClick = clickIntents::onYieldPromoCloseClick, + onPromoShown = { + clickIntents.onYieldPromoShown(currency) + }, + ) + } + + private fun getOrganizeButtonUM(accountList: AccountStatusList): TangemButtonUM? { + return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) { + TangemButtonUM( + text = resourceReference(R.string.organize_tokens_title), + isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, + size = TangemButtonSize.X9, + shape = TangemButtonShape.Rounded, + type = TangemButtonType.PrimaryInverse, + iconRes = R.drawable.ic_filter_default_24, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + null + } + } + + private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE + + private fun isSingleCurrencyWalletWithToken(): Boolean { + return selectedWallet is UserWallet.Cold && + selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt index 26c9df46a3..a94d6b5b78 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverter.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.currency.yieldSupplyKey @@ -28,6 +29,35 @@ internal class YieldSupplyPromoBannerConverter( if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null if (yieldModuleApyMap.isEmpty()) return null + val max = cryptoCurrencyStatuses.asSequence() + .mapNotNull { status -> + val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null + val shouldIgnoreCase = BlockchainUtils.isCaseInsensitiveContractAddress(token.network.rawId) + val tokenKey = "${token.network.rawId}_${token.contractAddress}" + + val matchedKey = yieldModuleApyMap.keys.firstOrNull { mapKey -> + mapKey.equals(tokenKey, shouldIgnoreCase) + } ?: return@mapNotNull null + + status to matchedKey + } + .maxByOrNull { (status, _) -> status.value.amount ?: BigDecimal.ZERO } + + return max?.first + } + + fun convert2(value: AccountStatusList): CryptoCurrencyStatus? { + if (!shouldShowMainPromo) return null + + val currencies = value.flattenCurrencies().filter { status -> + status.value is CryptoCurrencyStatus.Loaded + } + + val cryptoCurrencyStatuses = currencies.filter { it.currency is CryptoCurrency.Token } + + if (cryptoCurrencyStatuses.any { it.value.yieldSupplyStatus?.isActive == true }) return null + if (yieldModuleApyMap.isEmpty()) return null + val max = cryptoCurrencyStatuses.asSequence() .mapNotNull { status -> val token = status.currency as? CryptoCurrency.Token ?: return@mapNotNull null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt index cf83e0d256..2d7ba38899 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AccountListSubscriber.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -37,6 +38,7 @@ internal class AccountListSubscriber @AssistedInject constructor( private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, + private val designFeatureToggles: DesignFeatureToggles, ) : BasicAccountListSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( @@ -51,15 +53,27 @@ internal class AccountListSubscriber @AssistedInject constructor( accountList, appCurrency, expandedAccounts, isAccountMode, yieldSupplyApyMap, shouldShowMainPromo, stakingAvailabilityMap, -> - updateState( - accountList = accountList, - appCurrency = appCurrency, - expandedAccounts = expandedAccounts, - isAccountMode = isAccountMode, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, - ) + if (designFeatureToggles.isRedesignEnabled) { + updateState2( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) + } else { + updateState( + accountList = accountList, + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + isAccountMode = isAccountMode, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + ) + } } private fun stakingAvailabilityFlow(): Flow> = getAccountStatusListFlow() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index f4b38a9290..5c944d4473 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -85,6 +85,29 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { } } + protected fun updateState2( + accountList: AccountStatusList, + appCurrency: AppCurrency, + expandedAccounts: Set, + isAccountMode: Boolean, + yieldSupplyApyMap: Map = emptyMap(), + stakingAvailabilityMap: Map = emptyMap(), + shouldShowMainPromo: Boolean = false, + ) { + stateController.update( + SetTokenListTransformer( + params = TokenConverterParams.Account(accountList, expandedAccounts), + userWallet = userWallet, + appCurrency = appCurrency, + clickIntents = clickIntents, + yieldSupplyApyMap = yieldSupplyApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = isAccountMode, + ), + ) + } + private fun singleAccountTransform( maybeTokenList: Lce, appCurrency: AppCurrency, @@ -141,6 +164,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = false, ), ) } 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 3fbfb3592d..929f5e07ad 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 @@ -143,6 +143,7 @@ internal abstract class BasicTokenListSubscriber( yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, + isAccountsModeEnabled = false, ), ) } 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 1ddf7dd2cf..29616c5969 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 @@ -163,7 +163,7 @@ private fun LazyListScope.portfolioItem( @Suppress("MagicNumber") @Composable -private fun SlideInItemVisibility( +internal fun SlideInItemVisibility( visible: Boolean, currentIndex: Int, lastIndex: Int, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index 7d1be99d66..c3755c12bc 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -1,5 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency +import androidx.compose.animation.* +import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding @@ -8,22 +12,37 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.lerp import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEachIndexed +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.currency.icon.CurrencyIconState 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.stringResourceSafe +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.row.header.TangemHeaderRow +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.ds.row.token.TangemTokenRow +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.ds.row.token.internal.TokenRowTitle +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.core.ui.utils.ProvideSharedTransitionScope import com.tangem.core.ui.utils.lazyListItemPosition import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM import kotlinx.collections.immutable.ImmutableList internal const val NON_CONTENT_TOKENS_LIST_KEY = "NON_CONTENT_TOKENS_LIST" @@ -59,6 +78,180 @@ internal fun LazyListScope.tokensListItems( } } +/** + * LazyList extension for [WalletTokensListState] + * + * @param walletTokensListUM state + * @param modifier modifier + * +[REDACTED_AUTHOR] + */ +internal fun LazyListScope.tokensListItems2( + walletTokensListUM: WalletTokensListUM, + modifier: Modifier = Modifier, + isBalanceHidden: Boolean, +) { + when (walletTokensListUM) { + is WalletTokensListUM.Loading, + is WalletTokensListUM.Content, + -> { + walletTokensListUM.tokenList.fastForEachIndexed { index, listItem -> + when (listItem) { + is TokensListItemUM2.GroupTitle, + is TokensListItemUM2.Token, + -> tokenItem( + listItem = listItem, + index = index, + lastIndex = walletTokensListUM.tokenList.lastIndex, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + is TokensListItemUM2.Portfolio -> portfolioItem( + listItem = listItem, + index = index, + isBalanceHidden = isBalanceHidden, + modifier = modifier, + ) + } + } + } + WalletTokensListUM.Empty -> nonContentItem(modifier = modifier) + } +} + +private fun LazyListScope.tokenItem( + listItem: TokensListItemUM2, + index: Int, + lastIndex: Int, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + item( + key = listItem.tokenRowUM.id, + contentType = listItem.tokenRowUM::class.java, + ) { + val itemModifier = modifier + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index } + .padding(top = if (index == 0) TangemTheme.dimens2.x3 else 0.dp) + .roundedShapeItemDecoration( + radius = 18.dp, + currentIndex = index, + addDefaultPadding = false, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors2.surface.level3, + ) + + when (val tokenRowUM = listItem.tokenRowUM) { + is TangemTokenRowUM -> TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + reorderableTokenListState = null, + modifier = itemModifier, + ) + is TangemHeaderRowUM -> TangemHeaderRow( + headerRowUM = tokenRowUM, + modifier = itemModifier, + ) + } + } +} + +private fun LazyListScope.portfolioItem( + listItem: TokensListItemUM2.Portfolio, + index: Int, + isBalanceHidden: Boolean, + modifier: Modifier, +) { + val lastIndex = listItem.tokenList.lastIndex + 1 + + accountItem( + listItem = listItem, + modifier = modifier, + index = index, + lastIndex = lastIndex, + isBalanceHidden = isBalanceHidden, + ) + itemsIndexed( + items = listItem.tokenList, + key = { _, item -> item.tokenRowUM.id }, + contentType = { _, item -> item::class.java }, + itemContent = { tokenIndex, item -> + SlideInItemVisibility( + currentIndex = tokenIndex + 1, + lastIndex = lastIndex, + modifier = modifier + .animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null) + .roundedShapeItemDecoration( + radius = 18.dp, + currentIndex = tokenIndex + 1, + addDefaultPadding = false, + lastIndex = lastIndex, + backgroundColor = TangemTheme.colors2.surface.level3, + ), + visible = listItem.isExpanded, + ) { + val itemModifier = Modifier + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = tokenIndex + 1 } + + when (val tokenRowUM = item.tokenRowUM) { + is TangemTokenRowUM -> TangemTokenRow( + tokenRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, + reorderableTokenListState = null, + modifier = itemModifier, + ) + is TangemHeaderRowUM -> TangemHeaderRow( + headerRowUM = tokenRowUM, + modifier = itemModifier, + ) + } + } + }, + ) +} + +private fun LazyListScope.accountItem( + listItem: TokensListItemUM2.Portfolio, + modifier: Modifier, + index: Int, + lastIndex: Int, + isBalanceHidden: Boolean, +) { + item( + key = listItem.tokenRowUM.id, + contentType = listItem.tokenRowUM::class.java, + ) { + val portfolioModifier = modifier + .padding(top = if (index != 0) TangemTheme.dimens2.x2 else TangemTheme.dimens2.x3) + .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) + .semantics { lazyListItemPosition = index } + .roundedShapeItemDecoration( + currentIndex = 0, + radius = 18.dp, + addDefaultPadding = false, + lastIndex = if (listItem.isExpanded) lastIndex else 0, + backgroundColor = TangemTheme.colors2.surface.level3, + ) + if (listItem.isCollapsable) { + PortfolioRowItem( + item = listItem, + isBalanceHidden = isBalanceHidden, + modifier = portfolioModifier, + ) + } else { + TangemHeaderRow( + title = (listItem.tokenRowUM.titleUM as? TangemTokenRowUM.TitleUM.Content)?.text.orEmpty(), + subtitle = (listItem.tokenRowUM.topEndContentUM as? TangemTokenRowUM.EndContentUM.Content) + ?.text?.orMaskWithStars(isBalanceHidden), + headTangemIconUM = listItem.tokenRowUM.headIconUM, + modifier = portfolioModifier, + ) + } + } +} + private fun LazyListScope.contentItems( items: ImmutableList, modifier: Modifier = Modifier, @@ -77,6 +270,7 @@ private fun LazyListScope.contentItems( currentIndex = index, lastIndex = items.lastIndex, backgroundColor = TangemTheme.colors.background.primary, + radius = 18.dp, ) .testTag(MainScreenTestTags.TOKEN_LIST_ITEM) .semantics { lazyListItemPosition = index }, @@ -85,6 +279,117 @@ private fun LazyListScope.contentItems( ) } +@Suppress("MagicNumber", "ReusedModifierInstance", "LongMethod") +@Composable +internal fun PortfolioRowItem( + item: TokensListItemUM2.Portfolio, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + // TangemSharedTransitionLayout { + ProvideSharedTransitionScope(modifier) { + val iconSharedContentState = rememberSharedContentState(key = "icon") + val titleSharedContentState = rememberSharedContentState(key = "title") + val boundsTransform = BoundsTransform { _, _ -> tween(250) } + + AnimatedContent( + item.isExpanded, + transitionSpec = { + fadeIn(animationSpec = tween(350, delayMillis = 90)) + .togetherWith(fadeOut(animationSpec = tween(350))) + }, + ) { isExpandedWrapped -> + val animatedContentScope = this + + val composables = remember { + SharedTokenRowComposables( + icon = { modifier -> + val size = if (isExpandedWrapped) AccountIconSize.ExtraSmall else AccountIconSize.Default + val currencyIconState = + when (val currencyIconState = item.tokenRowUM.headIconUM.currencyIconState) { + is CurrencyIconState.CryptoPortfolio.Icon -> + currencyIconState.copy(size = size) + is CurrencyIconState.CryptoPortfolio.Letter -> + currencyIconState.copy(size = size) + else -> currencyIconState + } + + TangemIcon( + tangemIconUM = item.tokenRowUM.headIconUM.copy(currencyIconState = currencyIconState), + modifier = modifier.sharedBounds( + sharedContentState = iconSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + ), + ) + }, + title = { modifier -> + val targetAnimationFraction = if (isExpandedWrapped) 0f else 1f + + val animationFraction = animateFloatAsState( + targetValue = targetAnimationFraction, + animationSpec = tween(durationMillis = 350), + ) + + val startStyle = TangemTheme.typography2.captionSemibold12 + val stopStyle = TangemTheme.typography2.bodySemibold16 + + val textStyle by remember(animationFraction.value) { + derivedStateOf { lerp(startStyle, stopStyle, animationFraction.value) } + } + + val resizedTitle = when (val titleUM = item.tokenRowUM.titleUM) { + is TangemTokenRowUM.TitleUM.Content -> titleUM.copy( + text = styledStringReference( + titleUM.text.resolveReference(), + { textStyle.toSpanStyle() }, + ), + ) + else -> titleUM + } + + TokenRowTitle( + titleUM = resizedTitle, + modifier = modifier.sharedBounds( + sharedContentState = titleSharedContentState, + animatedVisibilityScope = animatedContentScope, + boundsTransform = boundsTransform, + resizeMode = scaleToBounds(ContentScale.Fit, Alignment.CenterStart), + ), + ) + }, + ) + } + + if (isExpandedWrapped) { + TangemHeaderRow( + subtitle = (item.tokenRowUM.topEndContentUM as? TangemTokenRowUM.EndContentUM.Content) + ?.text?.orMaskWithStars(isBalanceHidden), + titleContent = composables.title, + headContent = composables.icon, + footerTangemIconRes = R.drawable.ic_minimize_24, + onItemClick = item.tokenRowUM.onItemClick, + ) + } else { + TangemTokenRow( + tokenRowUM = item.tokenRowUM, + headComponent = composables.icon, + titleComponent = composables.title, + isBalanceHidden = isBalanceHidden, + reorderableTokenListState = null, + ) + } + } + // } + } +} + +@Stable +class SharedTokenRowComposables( + val title: @Composable (Modifier) -> Unit, + val icon: @Composable (Modifier) -> Unit, +) + private fun LazyListScope.nonContentItem(modifier: Modifier = Modifier) { item( key = NON_CONTENT_TOKENS_LIST_KEY, From febd32b964cb2dfb46a1f1c68b791dfb182d9868 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 17:59:33 +0300 Subject: [PATCH 39/97] Updated on 2026-08-14 --- core/ui/build.gradle.kts | 4 +- .../components/background/ShaderBackground.kt | 70 +++++++ .../MovingColorfulBlubsBackground.kt | 169 +++++++++++++++ .../NorthernLightsBackground.kt | 151 ++++++++++++++ .../tangem/core/ui/screen/ComposeScreen.kt | 5 +- .../com/tangem/core/ui/shader/GlossyShader.kt | 30 +++ .../NorthernLightsMeshGradientShader.kt | 193 ++++++++++++++++++ .../com/tangem/core/ui/shader/TangemShader.kt | 16 ++ .../shader/runtime/FallbackRuntimeEffect.kt | 13 ++ .../core/ui/shader/runtime/RuntimeEffect.kt | 35 ++++ .../ui/shader/runtime/RuntimeShaderEffect.kt | 41 ++++ .../presentation/wallet/ui/WalletScreen2.kt | 4 + 12 files changed, 728 insertions(+), 3 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index db9bc30c60..482fb4ddd7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -61,12 +61,12 @@ dependencies { api(deps.jodatime) implementation(deps.timber) implementation(deps.markdown) - implementation(deps.haze) { + api(deps.haze) { exclude(module = "activity-compose") exclude(module = "activity") exclude(module = "activity-ktx") } - implementation(deps.haze.materials) { + api(deps.haze.materials) { exclude(module = "activity-compose") exclude(module = "activity") exclude(module = "activity-ktx") diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt new file mode 100644 index 0000000000..4b7090b987 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/ShaderBackground.kt @@ -0,0 +1,70 @@ +@file:Suppress("MagicNumber", "UnnecessaryParentheses") +package com.tangem.core.ui.components.background + +import androidx.compose.animation.core.withInfiniteAnimationFrameMillis +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import com.tangem.core.ui.shader.TangemShader +import com.tangem.core.ui.shader.runtime.buildEffect +import kotlin.math.round + +@Composable +fun Modifier.shaderBackground( + shader: TangemShader, + speed: Float = 1f, + fallback: () -> Brush = { + Brush.horizontalGradient(listOf(Color.Transparent, Color.Transparent)) + }, +): Modifier { + val runtimeEffect = remember(shader) { buildEffect(shader) } + var size: Size by remember { mutableStateOf(Size(-1f, -1f)) } + val speedModifier = shader.speedModifier + + val time by if (runtimeEffect.isSupported) { + var startMillis = remember(shader) { -1L } + produceState(0f, speedModifier) { + while (true) { + withInfiniteAnimationFrameMillis { frameTimeMillis -> + if (startMillis < 0) startMillis = frameTimeMillis + value = ((frameTimeMillis - startMillis) / 16.6f) / 10f + } + } + } + } else { + remember { mutableFloatStateOf(-1f) } + } + + return this then Modifier.onGloballyPositioned { + size = Size(it.size.width.toFloat(), it.size.height.toFloat()) + }.drawBehind { + runtimeEffect.update( + shader = shader, + time = (time * speed * speedModifier).round(3), + width = size.width, + height = size.height, + ) // set uniforms for the shaders + + if (runtimeEffect.isReady) { + drawRect(brush = runtimeEffect.build()) + } else { + drawRect(brush = fallback()) + } + } +} + +private fun Float.round(decimals: Int): Float { + var multiplier = 1.0f + repeat(decimals) { multiplier *= 10 } + return round(this * multiplier) / multiplier +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt new file mode 100644 index 0000000000..f4e9988a2b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/MovingColorfulBlubsBackground.kt @@ -0,0 +1,169 @@ +@file:Suppress("MagicNumber") +package com.tangem.core.ui.components.background.northernlights + +import androidx.compose.runtime.Composable +import android.graphics.BlurMaskFilter +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.* +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas + +@Suppress("LongMethod") +@Composable +internal fun MovingColorfulBlubsBackground(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "FluidMeshGradient") + + // ── Circle 1 (left) ────────────────────────────────────────────────────── + val color1 by transition.animateColor( + initialValue = Color(0xFF3355EE), + targetValue = Color(0xFF5577FF), + animationSpec = infiniteRepeatable( + animation = tween(4_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "color1", + ) + val x1 by transition.animateFloat( + initialValue = 0.05f, + targetValue = 0.28f, + animationSpec = infiniteRepeatable( + animation = tween(5_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "x1", + ) + val y1 by transition.animateFloat( + initialValue = 0.0f, + targetValue = 0.18f, + animationSpec = infiniteRepeatable( + animation = tween(6_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "y1", + ) + + // ── Circle 2 (right) ───────────────────────────────────────────────────── + val color2 by transition.animateColor( + initialValue = Color(0xFF7733CC), + targetValue = Color(0xFF4455EE), + animationSpec = infiniteRepeatable( + animation = tween(5_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(1_500), + ), + label = "color2", + ) + val x2 by transition.animateFloat( + initialValue = 0.68f, + targetValue = 0.92f, + animationSpec = infiniteRepeatable( + animation = tween(7_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + ), + label = "x2", + ) + val y2 by transition.animateFloat( + initialValue = 0.02f, + targetValue = 0.20f, + animationSpec = infiniteRepeatable( + animation = tween(5_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(2_000), + ), + label = "y2", + ) + + // ── Oval (center) ──────────────────────────────────────────────────────── + val ovalColor by transition.animateColor( + initialValue = Color(0xFF5533CC), + targetValue = Color(0xFF8844EE), + animationSpec = infiniteRepeatable( + animation = tween(7_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(2_500), + ), + label = "ovalColor", + ) + // ── Circle 3 (center) ──────────────────────────────────────────────────── + val color3 by transition.animateColor( + initialValue = Color(0xFF9933BB), + targetValue = Color(0xFFBB44DD), + animationSpec = infiniteRepeatable( + animation = tween(6_000, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(3_000), + ), + label = "color3", + ) + val x3 by transition.animateFloat( + initialValue = 0.35f, + targetValue = 0.58f, + animationSpec = infiniteRepeatable( + animation = tween(6_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(1_000), + ), + label = "x3", + ) + val y3 by transition.animateFloat( + initialValue = 0.0f, + targetValue = 0.15f, + animationSpec = infiniteRepeatable( + animation = tween(4_500, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse, + initialStartOffset = StartOffset(500), + ), + label = "y3", + ) + + var blurRadiusState by remember { mutableFloatStateOf(0f) } + val circlePaint1 = remember { Paint() } + val circlePaint2 = remember { Paint() } + val circlePaint3 = remember { Paint() } + val ovalPaint = remember { Paint() } + + Canvas(modifier = modifier) { + val blurRadius = (size.minDimension * 0.28f).coerceIn(60f, 300f) + val circleRadius = size.width * 0.52f + + // Update maskFilter only when blur radius changes meaningfully + if (blurRadiusState != blurRadius) { + blurRadiusState = blurRadius + val mf = BlurMaskFilter(blurRadius, BlurMaskFilter.Blur.NORMAL) + circlePaint1.asFrameworkPaint().maskFilter = mf + circlePaint2.asFrameworkPaint().maskFilter = mf + circlePaint3.asFrameworkPaint().maskFilter = mf + ovalPaint.asFrameworkPaint().maskFilter = mf + } + + circlePaint1.color = color1.copy(alpha = 0.85f) + circlePaint2.color = color2.copy(alpha = 0.85f) + circlePaint3.color = color3.copy(alpha = 0.85f) + ovalPaint.color = ovalColor.copy(alpha = 0.80f) + + drawIntoCanvas { canvas -> + canvas.drawCircle(Offset(x1 * size.width, y1 * size.height), circleRadius, circlePaint1) + canvas.drawCircle(Offset(x2 * size.width, y2 * size.height), circleRadius, circlePaint2) + canvas.drawCircle(Offset(x3 * size.width, y3 * size.height), circleRadius, circlePaint3) + + val halfW = size.width * 0.68f + val halfH = size.width * 0.24f + val ovalCx = size.width * 0.50f + val ovalCy = 0f + + canvas.drawOval( + Rect(left = ovalCx - halfW, top = ovalCy - halfH, right = ovalCx + halfW, bottom = ovalCy + halfH), + ovalPaint, + ) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt new file mode 100644 index 0000000000..a59ce0f0e6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt @@ -0,0 +1,151 @@ +@file:Suppress("MagicNumber") + +package com.tangem.core.ui.components.background.northernlights + +import android.os.Build +import androidx.compose.animation.animateColor +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.StartOffset +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.keyframes +import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.components.background.shaderBackground +import com.tangem.core.ui.res.LocalPowerSavingState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader + +/** + * Animated northern lights background. + * Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode. + */ +@Composable +fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion: Boolean = false) { + val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() + if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) { + NorthernLightsBackgroundWithShader(modifier) + } else { + MovingColorfulBlubsBackground(modifier) + } +} + +@Suppress("LongMethod") +@Composable +private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) { + val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2") + val backgroundColor = TangemTheme.colors2.surface.level1 + + // Each track cycles through 4 states (matching the screenshot frames): + // deep/dark → saturated+bright → light/pastel → vibrant/vivid → back + // 16 s total per track, staggered so no two tracks peak simultaneously. + + // ── Color 1 – indigo → bright blue → lavender → hot violet ────────────── + val color1 by transition.animateColor( + initialValue = Color(0xFF2A1480), + targetValue = Color(0xFF2A1480), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF2A1480) at 0 using FastOutSlowInEasing + Color(0xFF4477EE) at 4_000 using FastOutSlowInEasing + Color(0xFFBBAAEE) at 8_000 using FastOutSlowInEasing + Color(0xFF8833EE) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + ), + label = "color1", + ) + + // ── Color 2 – dark blue → cyan-blue → sky → teal ───────────────────────── + val color2 by transition.animateColor( + initialValue = Color(0xFF1444AA), + targetValue = Color(0xFF1444AA), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF1444AA) at 0 using FastOutSlowInEasing + Color(0xFF22AADD) at 4_000 using FastOutSlowInEasing + Color(0xFF99BBDD) at 8_000 using FastOutSlowInEasing + Color(0xFF44DDCC) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(4_000), + ), + label = "color2", + ) + + // ── Color 3 – dark purple → medium purple → rose pink → magenta ────────── + val color3 by transition.animateColor( + initialValue = Color(0xFF4422BB), + targetValue = Color(0xFF4422BB), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF4422BB) at 0 using FastOutSlowInEasing + Color(0xFF7733CC) at 4_000 using FastOutSlowInEasing + Color(0xFFDD88BB) at 8_000 using FastOutSlowInEasing + Color(0xFFEE44AA) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(8_000), + ), + label = "color3", + ) + + // ── Color 4 – dark violet → medium violet → light pink → hot pink ──────── + val color4 by transition.animateColor( + initialValue = Color(0xFF331199), + targetValue = Color(0xFF331199), + animationSpec = infiniteRepeatable( + animation = keyframes { + durationMillis = 16_000 + Color(0xFF331199) at 0 using FastOutSlowInEasing + Color(0xFF6644CC) at 4_000 using FastOutSlowInEasing + Color(0xFFCC77DD) at 8_000 using FastOutSlowInEasing + Color(0xFFFF66CC) at 12_000 using FastOutSlowInEasing + }, + repeatMode = RepeatMode.Restart, + initialStartOffset = StartOffset(2_000), + ), + label = "color4", + ) + + // Keep a stable shader instance so the RuntimeShader is never recreated. + // Colors are pushed each recomposition via updateColors(). + val shader = remember { + NorthernLightsMeshGradientShader( + colors = arrayOf( + Color(0xFF2A1480), + Color(0xFF1444AA), + Color(0xFF4422BB), + Color(0xFF331199), + backgroundColor, + ), + speed = 0.5f, + scale = 4f, + ) + } + val colorsArray = remember { Array(5) { Color.Unspecified } } + colorsArray[0] = color1 + colorsArray[1] = color2 + colorsArray[2] = color3 + colorsArray[3] = color4 + colorsArray[4] = backgroundColor + shader.updateColors(colorsArray) + + Box( + modifier = modifier + .background(backgroundColor) + .fillMaxSize() + .shaderBackground(shader), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt index d1c1e10b32..cff7ef913b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/screen/ComposeScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemeRedesign /** * Interface representing a Compose screen with common theming and content composition properties. @@ -61,7 +62,9 @@ internal fun ComposeScreen.createComposeView( uiDependencies = uiDependencies, overrideSystemBarColors = overrideSystemBarColors, ) { - ScreenContent(modifier = screenModifier) + TangemThemeRedesign { + ScreenContent(modifier = screenModifier) + } } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt new file mode 100644 index 0000000000..600ad9af39 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/GlossyShader.kt @@ -0,0 +1,30 @@ +package com.tangem.core.ui.shader + +class GlossyShader : TangemShader { + override val sksl: String = + """ +// The MIT License + +// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +uniform float uTime; +uniform vec3 uResolution; + +vec4 main( vec2 fragCoord ) +{ + float mr = min(uResolution.x, uResolution.y); + vec2 uv = (fragCoord * 2.0 - uResolution.xy) / mr; + + float d = -uTime * 0.5; + float a = 0.0; + for (float i = 0.0; i < 8.0; ++i) { + a += cos(i - d - a * uv.x); + d += sin(uv.y * i + a); + } + d += uTime * 0.5; + vec3 col = vec3(cos(uv * vec2(d, a)) * 0.6 + 0.4, cos(a + d) * 0.5 + 0.5); + col = cos(col * cos(vec3(d, a, 2.5)) * 0.5 + 0.5); + return vec4(col,1.0); +} + """ +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt new file mode 100644 index 0000000000..05ade74d32 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/NorthernLightsMeshGradientShader.kt @@ -0,0 +1,193 @@ +@file:Suppress("MagicNumber") +package com.tangem.core.ui.shader + +import androidx.compose.ui.graphics.Color +import com.tangem.core.ui.shader.runtime.RuntimeEffect + +/** + * A shader that creates a colorful, flowing "northern lights" effect. + * @param colors The colors to display. The last provided color acts like a "background" + * @param speed Adjust the speed of the movement + * @param scale Adjusts the scale of the board. Higher number -> larger billboard -> smaller color blobs + * +[REDACTED_AUTHOR] + */ +class NorthernLightsMeshGradientShader( + colors: Array, + speed: Float = 1f, + scale: Float = 2f, +) : TangemShader { + + private val colorCount = colors.size + private val colorUniforms = colors.flatMap { + listOf(it.red, it.green, it.blue) + }.toTypedArray().toFloatArray() + private val ambientUniform = FloatArray(3) + + init { + recomputeAmbient() + } + + override val sksl = """ +uniform float uTime; +uniform vec3 uResolution; +uniform vec3 uAmbient; + +const int MAX_COLORS = $colorCount; +uniform vec3 uColor[MAX_COLORS]; + +// Simplex 3D Noise +// by Ian McEwan, Ashima Arts +// https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83 +// +vec4 permute(vec4 x) { + return mod(((x * 34.0) + 1.0) * x, 289.0); +} +vec4 taylorInvSqrt(vec4 r) { + return 1.79284291400159 - 0.85373472095314 * r; +} + +float snoise(vec3 v) { + const vec2 C = vec2(1.0 / 6.0, 1.0 / 3.0); + const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); + + // First corner + vec3 i = floor(v + dot(v, C.yyy)); + vec3 x0 = v - i + dot(i, C.xxx); + + // Other corners + vec3 g = step(x0.yzx, x0.xyz); + vec3 l = 1.0 - g; + vec3 i1 = min(g.xyz, l.zxy); + vec3 i2 = max(g.xyz, l.zxy); + + // x0 = x0 - 0. + 0.0 * C + vec3 x1 = x0 - i1 + 1.0 * C.xxx; + vec3 x2 = x0 - i2 + 2.0 * C.xxx; + vec3 x3 = x0 - 1. + 3.0 * C.xxx; + + // Permutations + i = mod(i, 289.0); + vec4 p = permute(permute(permute(i.z + vec4(0.0, i1.z, i2.z, 1.0)) + i.y + vec4(0.0, i1.y, i2.y, 1.0)) + i.x + vec4(0.0, i1.x, i2.x, 1.0)); + + // Gradients + // ( N*N points uniformly over a square, mapped onto an octahedron.) + float n_ = 1.0 / 7.0; // N=7 + vec3 ns = n_ * D.wyz - D.xzx; + + vec4 j = p - 49.0 * floor(p * ns.z * ns.z); // mod(p,N*N) + + vec4 x_ = floor(j * ns.z); + vec4 y_ = floor(j - 7.0 * x_); // mod(j,N) + + vec4 x = x_ * ns.x + ns.yyyy; + vec4 y = y_ * ns.x + ns.yyyy; + vec4 h = 1.0 - abs(x) - abs(y); + + vec4 b0 = vec4(x.xy, y.xy); + vec4 b1 = vec4(x.zw, y.zw); + + vec4 s0 = floor(b0) * 2.0 + 1.0; + vec4 s1 = floor(b1) * 2.0 + 1.0; + vec4 sh = -step(h, vec4(0.0)); + + vec4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; + vec4 a1 = b1.xzyw + s1.xzyw * sh.zzww; + + vec3 p0 = vec3(a0.xy, h.x); + vec3 p1 = vec3(a0.zw, h.y); + vec3 p2 = vec3(a1.xy, h.z); + vec3 p3 = vec3(a1.zw, h.w); + + //Normalise gradients + vec4 norm = taylorInvSqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3))); + p0 *= norm.x; + p1 *= norm.y; + p2 *= norm.z; + p3 *= norm.w; + + // Mix final noise value + vec4 m = max(0.6 - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); + m = m * m; + return 42.0 * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3))); +} + +vec4 main( vec2 fragCoord ) { + float mr = min(uResolution.x, uResolution.y); + vec2 uv = (fragCoord * $scale - uResolution.xy) / mr; + + vec2 base = uv / 2; + + vec3 vColor = uColor[MAX_COLORS - 1]; + + const vec2 frequency = vec2(0.7, 0.3); + const float noiseFloor = 0.00001; + float t = uTime * 0.005; + + for(int i = 0; i < MAX_COLORS - 1; i++) { + float fi = float(i); + float flow = 5. + fi * 0.3; + float speed = 6. * $speed + fi * 0.3; + float seed = 1. + fi * 4.; + float noiseCeil = 0.6 + fi * 0.07; + + float noise = smoothstep(noiseFloor, noiseCeil, snoise(vec3(base.x * frequency.x, base.y * frequency.y - t * flow, t * speed + seed))); + + vColor = mix(vColor, uColor[i], noise); + } + + vColor = max(vColor, uAmbient); + + // Elliptical falloff centred at the very top of the screen. + // Using fragCoord directly (pixels) and uResolution for screen size. + // Horizontal radius ~ 80 % of screen width → wide enough to cover corners. + // Vertical radius ~ 45 % of screen height → controls how far down the glow reaches. + vec2 topCenter = vec2(uResolution.x * 0.5, 0.0); + vec2 delta = fragCoord - topCenter; + vec2 radii = vec2(uResolution.x * 0.9, uResolution.y * 0.65); + float normDist = length(delta / radii); + float alpha = pow(1.0 - smoothstep(0.0, 1.0, normDist), 1.5); + + // Pre-multiplied alpha so the shader composites correctly over the dark background. + return vec4(vColor * alpha, alpha); +} + """ + + /** Updates the animated colors in-place without recreating the shader. */ + fun updateColors(colors: Array) { + colors.forEachIndexed { i, color -> + colorUniforms[i * 3 + 0] = color.red + colorUniforms[i * 3 + 1] = color.green + colorUniforms[i * 3 + 2] = color.blue + } + recomputeAmbient() + } + + private fun recomputeAmbient() { + val count = colorCount - 1 + var r = 0f + var g = 0f + var b = 0f + for (i in 0 until count) { + r += colorUniforms[i * 3] + g += colorUniforms[i * 3 + 1] + b += colorUniforms[i * 3 + 2] + } + val scale = 0.5f / count + ambientUniform[0] = r * scale + ambientUniform[1] = g * scale + ambientUniform[2] = b * scale + } + + override fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) { + super.applyUniforms(runtimeEffect = runtimeEffect, time = time, width = width, height = height) + + runtimeEffect.setFloatUniform(name = "uColor", values = colorUniforms) + runtimeEffect.setFloatUniform( + name = "uAmbient", + value1 = ambientUniform[0], + value2 = ambientUniform[1], + value3 = ambientUniform[2], + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt new file mode 100644 index 0000000000..388914a11b --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/TangemShader.kt @@ -0,0 +1,16 @@ +package com.tangem.core.ui.shader + +import com.tangem.core.ui.shader.runtime.RuntimeEffect + +interface TangemShader { + val speedModifier: Float + get() = 0.5f + + val sksl: String + + /** Applies the uniforms required for this shader to the effect */ + fun applyUniforms(runtimeEffect: RuntimeEffect, time: Float, width: Float, height: Float) { + runtimeEffect.setFloatUniform(name = "uResolution", value1 = width, value2 = height, value3 = width / height) + runtimeEffect.setFloatUniform(name = "uTime", value1 = time) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt new file mode 100644 index 0000000000..899e3c8a78 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/FallbackRuntimeEffect.kt @@ -0,0 +1,13 @@ +package com.tangem.core.ui.shader.runtime + +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color + +internal class FallbackRuntimeEffect : RuntimeEffect { + override val isSupported: Boolean = false + override val isReady: Boolean = false + + override fun build(): Brush { + return Brush.horizontalGradient(listOf(Color.White, Color.White)) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt new file mode 100644 index 0000000000..035495aad8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeEffect.kt @@ -0,0 +1,35 @@ +package com.tangem.core.ui.shader.runtime + +import android.os.Build +import androidx.compose.ui.graphics.Brush +import com.tangem.core.ui.shader.TangemShader + +interface RuntimeEffect { + + val isSupported: Boolean + val isReady: Boolean + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float, value2: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) {} + + /** Sets a float array uniform for this shader */ + fun setFloatUniform(name: String, values: FloatArray) {} + + fun update(shader: TangemShader, time: Float, width: Float, height: Float) {} + + fun build(): Brush +} + +internal fun buildEffect(shader: TangemShader): RuntimeEffect { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + RuntimeShaderEffect(shader) + } else { + FallbackRuntimeEffect() + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt new file mode 100644 index 0000000000..78c50e2e7c --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/shader/runtime/RuntimeShaderEffect.kt @@ -0,0 +1,41 @@ +package com.tangem.core.ui.shader.runtime + +import android.graphics.RuntimeShader +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.ShaderBrush +import com.tangem.core.ui.shader.TangemShader + +@RequiresApi(Build.VERSION_CODES.TIRAMISU) +internal class RuntimeShaderEffect(tangemShader: TangemShader) : RuntimeEffect { + private val compositeRuntimeEffect = RuntimeShader(tangemShader.sksl) + + override val isSupported: Boolean = true + override var isReady: Boolean = false + + override fun setFloatUniform(name: String, value1: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1) + } + + override fun setFloatUniform(name: String, value1: Float, value2: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1, value2) + } + + override fun setFloatUniform(name: String, value1: Float, value2: Float, value3: Float) { + compositeRuntimeEffect.setFloatUniform(name, value1, value2, value3) + } + + override fun setFloatUniform(name: String, values: FloatArray) { + compositeRuntimeEffect.setFloatUniform(name, values) + } + + override fun update(shader: TangemShader, time: Float, width: Float, height: Float) { + shader.applyUniforms(runtimeEffect = this, time = time, width = width, height = height) + isReady = width > 0 && height > 0 + } + + override fun build(): Brush { + return ShaderBrush(compositeRuntimeEffect) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index 8a878d9111..cd10d61f4d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -35,6 +35,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ExperimentalDecomposeApi import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight +import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.components.haze.hazeSourceTangem @@ -118,6 +119,9 @@ private fun WalletContent2( val partialCollapsedHeight = 64.dp + statusBarHeight val scaffoldContent: @Composable (PaddingValues?) -> Unit = { _ -> + Box(Modifier.fillMaxSize()) { + NorthernLightsBackground(Modifier.matchParentSize()) + } val pagerState = rememberPagerState( initialPage = state.selectedWalletIndex, From ff918c04b49cdd017fa3e8f1f9b1431ed5edd615 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 13:08:09 +0500 Subject: [PATCH 40/97] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 14 -- .../status/di/AccountStatusUseCaseModule.kt | 12 +- .../status/model/AccountCryptoCurrency.kt | 2 +- ...eV2.kt => ApplyTokenListSortingUseCase.kt} | 6 +- ...2.kt => ToggleTokenListGroupingUseCase.kt} | 2 +- ...V2.kt => ToggleTokenListSortingUseCase.kt} | 2 +- .../ApplyTokenListSortingUseCaseTest.kt | 2 +- ... => ToggleTokenListGroupingUseCaseTest.kt} | 4 +- ...t => ToggleTokenListSortingUseCaseTest.kt} | 4 +- .../tokens/ToggleTokenListGroupingUseCase.kt | 51 ---- .../tokens/ToggleTokenListSortingUseCase.kt | 38 --- .../tokens/ToggleTokenListGroupingTest.kt | 103 -------- .../ToggleTokenListSortingUseCaseTest.kt | 103 -------- .../organizetokens/OrganizeTokensComponent.kt | 2 +- .../PortfolioOrganizeTokensAnalyticsEvent.kt | 2 +- .../organizetokens/entity}/DraggableItem.kt | 2 +- .../entity/OrganizeTokensListUM.kt} | 20 +- .../entity}/OrganizeTokensState.kt | 3 +- .../model/CryptoCurrenciesIdsResolver.kt | 35 +++ .../organizetokens/model}/Intents.kt | 4 +- .../model/OrganizeTokensModel.kt | 233 +++++------------- .../model/OrganizeTokensStateHolder.kt | 111 +++++++++ .../model}/common/DraggableItemOperations.kt | 4 +- .../model}/common/DraggableItemsOperations.kt | 55 +---- .../model}/common/IdsOperations.kt | 2 +- .../OrganiseTokensListStateOperations.kt | 18 ++ .../model}/common/TokenListOperations.kt | 2 +- .../converter/InProgressStateConverter.kt | 4 +- .../converter/TokenListToStateConverter.kt} | 20 +- .../error/TokenListErrorConverter.kt | 6 +- .../error/TokenListSortingErrorConverter.kt | 6 +- ...CryptoCurrencyToDraggableItemConverter.kt} | 10 +- .../NetworkGroupToDraggableItemsConverter.kt} | 16 +- .../items/OrganizedTokenListConverter.kt | 8 +- .../model/dnd/DragAndDropAdapter.kt} | 24 +- .../model}/dnd/DraggableGroupsOperations.kt | 47 +--- .../ui}/OrganizeTokensScreen.kt | 23 +- .../ui/preview/OrganizeTokensPreview.kt | 127 ++++++++++ .../presentation/common/WalletPreviewData.kt | 130 ---------- .../OrganizeTokensStateHolder.kt | 166 ------------- .../utils/CryptoCurrenciesIdsResolver.kt | 60 ----- .../OrganiseTokensListStateOperations.kt | 31 --- .../converter/TokenListToStateConverter.kt | 32 --- .../CryptoCurrencyToDraggableItemConverter.kt | 74 ------ .../NetworkGroupToDraggableItemsConverter.kt | 41 --- .../items/TokenListToListStateConverter.kt | 45 ---- .../utils/dnd/DragAndDropAdapter.kt | 185 -------------- 47 files changed, 440 insertions(+), 1451 deletions(-) rename domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/{ApplyTokenListSortingUseCaseV2.kt => ApplyTokenListSortingUseCase.kt} (98%) rename domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/{ToggleTokenListGroupingUseCaseV2.kt => ToggleTokenListGroupingUseCase.kt} (98%) rename domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/{ToggleTokenListSortingUseCaseV2.kt => ToggleTokenListSortingUseCase.kt} (98%) rename domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/{ToggleTokenListGroupingUseCaseV2Test.kt => ToggleTokenListGroupingUseCaseTest.kt} (98%) rename domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/{ToggleTokenListSortingUseCaseV2Test.kt => ToggleTokenListSortingUseCaseTest.kt} (97%) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt delete mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt delete mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation => child}/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt (92%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/model => child/organizetokens/entity}/DraggableItem.kt (98%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/model/OrganizeTokensListState.kt => child/organizetokens/entity/OrganizeTokensListUM.kt} (54%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/model => child/organizetokens/entity}/OrganizeTokensState.kt (91%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens => child/organizetokens/model}/Intents.kt (76%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/common/DraggableItemOperations.kt (59%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/common/DraggableItemsOperations.kt (63%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/common/IdsOperations.kt (78%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/common/TokenListOperations.kt (83%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/converter/InProgressStateConverter.kt (82%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt => child/organizetokens/model/converter/TokenListToStateConverter.kt} (83%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/converter/error/TokenListErrorConverter.kt (64%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/converter/error/TokenListSortingErrorConverter.kt (65%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt => child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt} (87%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt => child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt} (78%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/converter/items/OrganizedTokenListConverter.kt (85%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt => child/organizetokens/model/dnd/DragAndDropAdapter.kt} (87%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens/utils => child/organizetokens/model}/dnd/DraggableGroupsOperations.kt (59%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/{presentation/organizetokens => child/organizetokens/ui}/OrganizeTokensScreen.kt (95%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index ef95af2a32..ecba7042ef 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -177,20 +177,6 @@ internal object TokensDomainModule { return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier) } - @Provides - @Singleton - fun provideToggleTokenListGroupingUseCase( - dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListGroupingUseCase { - return ToggleTokenListGroupingUseCase(dispatchers) - } - - @Provides - @Singleton - fun provideToggleTokenListSortingUseCase(dispatchers: CoroutineDispatcherProvider): ToggleTokenListSortingUseCase { - return ToggleTokenListSortingUseCase(dispatchers) - } - @Provides @Singleton fun provideApplyTokenListSortingUseCase( 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 a228aceb2e..1f7bea813c 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 @@ -67,8 +67,8 @@ internal object AccountStatusUseCaseModule { fun provideApplyTokenListSortingUseCaseV2( accountsCRUDRepository: AccountsCRUDRepository, dispatchers: CoroutineDispatcherProvider, - ): ApplyTokenListSortingUseCaseV2 { - return ApplyTokenListSortingUseCaseV2( + ): ApplyTokenListSortingUseCase { + return ApplyTokenListSortingUseCase( accountsCRUDRepository = accountsCRUDRepository, dispatchers = dispatchers, ) @@ -141,8 +141,8 @@ internal object AccountStatusUseCaseModule { @Singleton fun provideToggleTokenListSortingUseCaseV2( dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListSortingUseCaseV2 { - return ToggleTokenListSortingUseCaseV2( + ): ToggleTokenListSortingUseCase { + return ToggleTokenListSortingUseCase( dispatchers = dispatchers, ) } @@ -151,8 +151,8 @@ internal object AccountStatusUseCaseModule { @Singleton fun provideToggleTokenListGroupingUseCaseV2( dispatchers: CoroutineDispatcherProvider, - ): ToggleTokenListGroupingUseCaseV2 { - return ToggleTokenListGroupingUseCaseV2( + ): ToggleTokenListGroupingUseCase { + return ToggleTokenListGroupingUseCase( dispatchers = dispatchers, ) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt index 5acaea2b98..152ab3ca77 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/model/AccountCryptoCurrency.kt @@ -4,7 +4,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import kotlinx.serialization.Serializable -typealias AccountCryptoCurrencies = Map> +typealias AccountCryptoCurrencies = Map> /** * Combines an [Account] with its corresponding [CryptoCurrency]. diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt index 6dc53cca47..55e21d936c 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCase.kt @@ -25,7 +25,7 @@ private typealias SortingErrorByAccountId = MutableMap + errors[account.accountId] = error return@map account } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCase.kt index 32e1b28366..648daca038 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCase.kt @@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider * * @property dispatchers Provides coroutine dispatchers for executing tasks. */ -class ToggleTokenListGroupingUseCaseV2( +class ToggleTokenListGroupingUseCase( private val dispatchers: CoroutineDispatcherProvider, ) { diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt similarity index 98% rename from domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt rename to domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt index ccf2416368..18bfbc35a1 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCase.kt @@ -21,7 +21,7 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider * * @property dispatchers Provides coroutine dispatchers for executing tasks. */ -class ToggleTokenListSortingUseCaseV2( +class ToggleTokenListSortingUseCase( private val dispatchers: CoroutineDispatcherProvider, ) { diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt index af71ec2c02..bd340dc150 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ApplyTokenListSortingUseCaseTest.kt @@ -30,7 +30,7 @@ internal class ApplyTokenListSortingUseCaseTest { private val accountsCRUDRepository = mockk(relaxUnitFun = true) - private val useCase = ApplyTokenListSortingUseCaseV2( + private val useCase = ApplyTokenListSortingUseCase( accountsCRUDRepository = accountsCRUDRepository, dispatchers = TestingCoroutineDispatcherProvider(), ) diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt similarity index 98% rename from domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt index 9dadb85e4f..9e3b423a62 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListGroupingUseCaseTest.kt @@ -28,9 +28,9 @@ import java.math.BigDecimal [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ToggleTokenListGroupingUseCaseV2Test { +class ToggleTokenListGroupingUseCaseTest { - private val useCase = ToggleTokenListGroupingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider()) + private val useCase = ToggleTokenListGroupingUseCase(dispatchers = TestingCoroutineDispatcherProvider()) private val userWalletId = UserWalletId("011") private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt similarity index 97% rename from domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt rename to domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt index 344dc8641e..c7808be2d4 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseV2Test.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ToggleTokenListSortingUseCaseTest.kt @@ -28,9 +28,9 @@ import java.math.BigDecimal [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class ToggleTokenListSortingUseCaseV2Test { +class ToggleTokenListSortingUseCaseTest { - private val useCase = ToggleTokenListSortingUseCaseV2(dispatchers = TestingCoroutineDispatcherProvider()) + private val useCase = ToggleTokenListSortingUseCase(dispatchers = TestingCoroutineDispatcherProvider()) private val userWalletId = UserWalletId("011") private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt deleted file mode 100644 index 11bdea2a34..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingUseCase.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.Raise -import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.operations.TokenListFactory -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class ToggleTokenListGroupingUseCase( - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke(tokenList: TokenList): Either { - return withContext(dispatchers.default) { - either { - when (tokenList) { - is TokenList.GroupedByNetwork -> ungroupTokens(tokenList) - is TokenList.Ungrouped -> groupTokens(tokenList) - is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty) - } - } - } - } - - private fun Raise.groupTokens(tokenList: TokenList.Ungrouped): TokenList.GroupedByNetwork { - validate(tokenList) - - return TokenListFactory.createGroupedByNetwork(tokenList) - } - - private fun Raise.ungroupTokens(tokenList: TokenList.GroupedByNetwork): TokenList.Ungrouped { - validate(tokenList) - - return TokenListFactory.createUngrouped(tokenList) - } - - private fun Raise.validate(tokenList: TokenList) { - ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { - TokenListSortingError.TokenListIsLoading - } - - ensure(tokenList.flattenCurrencies().isNotEmpty()) { - TokenListSortingError.TokenListIsEmpty - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt deleted file mode 100644 index 2bc37f5bb1..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCase.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.either -import arrow.core.raise.ensure -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.operations.TokenListFactory -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext - -class ToggleTokenListSortingUseCase( - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke(tokenList: TokenList): Either { - return withContext(dispatchers.default) { - either { - ensure(tokenList.totalFiatBalance !is TotalFiatBalance.Loading) { - TokenListSortingError.TokenListIsLoading - } - - TokenListFactory.create( - statuses = tokenList.flattenCurrencies(), - groupType = when (tokenList) { - is TokenList.GroupedByNetwork -> TokensGroupType.NETWORK - is TokenList.Ungrouped -> TokensGroupType.NONE - is TokenList.Empty -> raise(TokenListSortingError.TokenListIsEmpty) - }, - sortType = TokensSortType.BALANCE, - ) - } - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt deleted file mode 100644 index 8630063505..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListGroupingTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.left -import arrow.core.right -import com.google.common.truth.Truth -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class ToggleTokenListGroupingTest { - - private val useCase = ToggleTokenListGroupingUseCase( - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `when list is empty then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsEmpty.left() - - // When - val actual = useCase(MockTokenLists.emptyUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and sorted then sorted grouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.sortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and unsorted then unsorted grouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.unsortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and sorted then sorted ungrouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.sortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and unsorted then unsorted ungrouped list should be received`() = runTest { - // Given - val expected = MockTokenLists.unsortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt deleted file mode 100644 index 787d55393c..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ToggleTokenListSortingUseCaseTest.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.left -import arrow.core.right -import com.google.common.truth.Truth -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockTokenLists -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class ToggleTokenListSortingUseCaseTest { - - private val useCase = ToggleTokenListSortingUseCase( - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @Test - fun `when list is empty then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsEmpty.left() - - // When - val actual = useCase(MockTokenLists.emptyTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and loading then error should be received`() = runTest { - // Given - val expected = TokenListSortingError.TokenListIsLoading.left() - - // When - val actual = useCase(MockTokenLists.loadingUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and unsorted then grouped and sorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and unsorted then ungrouped and sorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is grouped and sorted then grouped and unsorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedGroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedGroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } - - @Test - fun `when list is ungrouped and sorted then ungrouped and unsorted list should be received`() = runTest { - // Given - val expected = MockTokenLists.sortedUngroupedTokenList.right() - - // When - val actual = useCase(MockTokenLists.unsortedUngroupedTokenList) - - // Then - Truth.assertThat(actual).isEqualTo(expected) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt index b54ed3161b..e8a18c88ec 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/OrganizeTokensComponent.kt @@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.organizetokens.model.OrganizeTokensModel -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensScreen +import com.tangem.feature.wallet.child.organizetokens.ui.OrganizeTokensScreen import kotlinx.coroutines.launch internal class OrganizeTokensComponent( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt similarity index 92% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt index a0c96c2974..5d4948ab02 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/analytics/PortfolioOrganizeTokensAnalyticsEvent.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.analytics +package com.tangem.feature.wallet.child.organizetokens.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt similarity index 98% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt index da7e0746a2..4d0560b517 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/DraggableItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/DraggableItem.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.token.state.TokenItemState diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt similarity index 54% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt index 720edbb802..38144081c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensListUM.kt @@ -1,27 +1,9 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf -@Deprecated("Use OrganizeTokensListUM instead, will be removed in future releases") -@Immutable -internal sealed class OrganizeTokensListState { - abstract val items: PersistentList - - data class GroupedByNetwork( - override val items: PersistentList, - ) : OrganizeTokensListState() - - data class Ungrouped( - override val items: PersistentList, - ) : OrganizeTokensListState() - - data object Empty : OrganizeTokensListState() { - override val items: PersistentList = persistentListOf() - } -} - @Immutable internal sealed interface OrganizeTokensListUM { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt similarity index 91% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt index 556f2e081d..234ab0e793 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/model/OrganizeTokensState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/entity/OrganizeTokensState.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.model +package com.tangem.feature.wallet.child.organizetokens.entity import androidx.compose.runtime.Immutable import com.tangem.core.ui.event.StateEvent @@ -7,7 +7,6 @@ import org.burnoutcrew.reorderable.ItemPosition @Immutable internal data class OrganizeTokensState( val onBackClick: () -> Unit, - val itemsState: OrganizeTokensListState, val tokenListUM: OrganizeTokensListUM, val header: HeaderConfig, val actions: ActionsConfig, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt new file mode 100644 index 0000000000..cb1a1b0db7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/CryptoCurrenciesIdsResolver.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.child.organizetokens.model + +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.account.status.model.AccountCryptoCurrencies +import com.tangem.domain.models.account.filterCryptoPortfolio +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM + +internal class CryptoCurrenciesIdsResolver { + + fun resolve(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies { + if (accountStatusList == null) return emptyMap() + + val draggableTokens = when (tokensListUM) { + OrganizeTokensListUM.EmptyList -> return emptyMap() + is OrganizeTokensListUM.AccountList, + is OrganizeTokensListUM.TokensList, + -> tokensListUM.items.filterIsInstance() + } + + return accountStatusList.accountStatuses + .filterCryptoPortfolio() + .filter { it.tokenList != TokenList.Empty } + .associate { accountStatus -> + val currenciesById = accountStatus.flattenCurrencies().associateBy { it.currency.id.value } + + accountStatus.account to draggableTokens + .asSequence() + .filter { it.accountId == accountStatus.account.accountId.value } + .mapNotNull { token -> currenciesById[token.id]?.currency } + .toList() + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt similarity index 76% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt index e0f4868347..5123361a66 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/Intents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/Intents.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.wallet.presentation.organizetokens +package com.tangem.feature.wallet.child.organizetokens.model -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem import org.burnoutcrew.reorderable.ItemPosition internal interface OrganizeTokensIntents { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt index fb310ec8be..bd4e6ee9be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensModel.kt @@ -7,35 +7,21 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.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.status.usecase.ApplyTokenListSortingUseCaseV2 -import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCaseV2 -import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCaseV2 +import com.tangem.domain.account.status.usecase.ApplyTokenListSortingUseCase +import com.tangem.domain.account.status.usecase.ToggleTokenListGroupingUseCase +import com.tangem.domain.account.status.usecase.ToggleTokenListSortingUseCase 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 -import com.tangem.domain.core.lce.Lce import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.ToggleTokenListGroupingUseCase -import com.tangem.domain.tokens.ToggleTokenListSortingUseCase -import com.tangem.domain.tokens.error.TokenListError import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensIntents -import com.tangem.feature.wallet.presentation.organizetokens.OrganizeTokensStateHolder -import com.tangem.feature.wallet.presentation.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.CryptoCurrenciesIdsResolver -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.disableSortingByBalance -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapter -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2 +import com.tangem.feature.wallet.child.organizetokens.analytics.PortfolioOrganizeTokensAnalyticsEvent +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapter import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* @@ -49,18 +35,13 @@ internal class OrganizeTokensModel @Inject constructor( paramsContainer: ParamsContainer, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, override val dispatchers: CoroutineDispatcherProvider, - private val getTokenListUseCase: GetTokenListUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val analyticsEventsHandler: AnalyticsEventHandler, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val toggleTokenListGroupingUseCase: ToggleTokenListGroupingUseCase, private val toggleTokenListSortingUseCase: ToggleTokenListSortingUseCase, private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val analyticsEventsHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val toggleTokenListGroupingUseCaseV2: ToggleTokenListGroupingUseCaseV2, - private val toggleTokenListSortingUseCaseV2: ToggleTokenListSortingUseCaseV2, - private val applyTokenListSortingUseCaseV2: ApplyTokenListSortingUseCaseV2, ) : Model(), OrganizeTokensIntents { private val selectedAppCurrencyFlow = createSelectedAppCurrencyFlow() @@ -68,24 +49,17 @@ internal class OrganizeTokensModel @Inject constructor( private var isBalanceHidden = true private val dragAndDropAdapter = DragAndDropAdapter( - listStateProvider = Provider { uiState.value.itemsState }, - ) - - private val dragAndDropAdapterV2 = DragAndDropAdapterV2( tokenListUMProvider = Provider { uiState.value.tokenListUM }, ) private val stateHolder = OrganizeTokensStateHolder( intents = this, - dragAndDropIntents = dragAndDropAdapter, - dragAndDropAdapterV2 = dragAndDropAdapterV2, + dragAndDropAdapter = dragAndDropAdapter, appCurrencyProvider = Provider(selectedAppCurrencyFlow::value), - accountsFeatureToggles = accountsFeatureToggles, ) private val userWalletId = paramsContainer.require().userWalletId - private var cachedTokenList: TokenList? = null private var cachedAccountStatusList: AccountStatusList? = null private var isAccountsModeEnabled: Boolean = false @@ -113,68 +87,35 @@ internal class OrganizeTokensModel @Inject constructor( } override fun onSortClick() { - if (accountsFeatureToggles.isFeatureEnabled) { - val list = cachedAccountStatusList ?: return - if (list.sortType == TokensSortType.BALANCE) return + val list = cachedAccountStatusList ?: return + if (list.sortType == TokensSortType.BALANCE) return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) - modelScope.launch { - toggleTokenListSortingUseCaseV2(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) - cachedAccountStatusList = it - }, - ) - } - } else { - val list = cachedTokenList ?: return - if (list.sortedBy == TokensSortType.BALANCE) return - - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.ByBalance()) - - modelScope.launch { - toggleTokenListSortingUseCase(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSorting(it) - cachedTokenList = it - }, - ) - } + modelScope.launch { + toggleTokenListSortingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { accountStatusList -> + stateHolder.updateStateAfterTokenListSorting(accountStatusList, isAccountsModeEnabled) + cachedAccountStatusList = accountStatusList + }, + ) } } override fun onGroupClick() { - if (accountsFeatureToggles.isFeatureEnabled) { - val list = cachedAccountStatusList ?: return + val list = cachedAccountStatusList ?: return - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) + analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) - modelScope.launch { - toggleTokenListGroupingUseCaseV2(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) - cachedAccountStatusList = it - }, - ) - } - } else { - val list = cachedTokenList ?: return - - analyticsEventsHandler.send(PortfolioOrganizeTokensAnalyticsEvent.Group()) - - modelScope.launch { - toggleTokenListGroupingUseCase(list).fold( - ifLeft = stateHolder::updateStateWithError, - ifRight = { - stateHolder.updateStateAfterTokenListSorting(it) - cachedTokenList = it - }, - ) - } + modelScope.launch { + toggleTokenListGroupingUseCase(list).fold( + ifLeft = stateHolder::updateStateWithError, + ifRight = { accountStatusList -> + stateHolder.updateStateAfterTokenListSorting(accountStatusList, isAccountsModeEnabled) + cachedAccountStatusList = accountStatusList + }, + ) } } @@ -183,39 +124,20 @@ internal class OrganizeTokensModel @Inject constructor( stateHolder.updateStateToDisplayProgress() val resolver = CryptoCurrenciesIdsResolver() val isSortedByBalance = uiState.value.header.isSortedByBalance + val tokensListUM = uiState.value.tokenListUM - val result = if (accountsFeatureToggles.isFeatureEnabled) { - val tokensListUM = uiState.value.tokenListUM + val isGroupedByNetwork = tokensListUM.isGrouped - val isGroupedByNetwork = tokensListUM.isGrouped + sendAnalyticsEvent( + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) - sendAnalyticsEvent( - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - - applyTokenListSortingUseCaseV2( - sortedTokensIdsByAccount = resolver.resolveV2(tokensListUM, cachedAccountStatusList), - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - } else { - val listState = uiState.value.itemsState - - val isGroupedByNetwork = listState is OrganizeTokensListState.GroupedByNetwork - - sendAnalyticsEvent( - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - - applyTokenListSortingUseCase( - userWalletId = userWalletId, - sortedTokensIds = resolver.resolve(listState, cachedTokenList), - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - } + val result = applyTokenListSortingUseCase( + sortedTokensIdsByAccount = resolver.resolve(tokensListUM, cachedAccountStatusList), + isGroupedByNetwork = isGroupedByNetwork, + isSortedByBalance = isSortedByBalance, + ) result.fold( ifLeft = stateHolder::updateStateWithError, @@ -235,72 +157,35 @@ internal class OrganizeTokensModel @Inject constructor( private fun bootstrapTokenList() { modelScope.launch { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountList = singleAccountStatusListSupplier.getSyncOrNull( - SingleAccountStatusListProducer.Params(userWalletId), - ) ?: return@launch + val accountList = singleAccountStatusListSupplier.getSyncOrNull( + SingleAccountStatusListProducer.Params(userWalletId), + ) ?: return@launch - isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() + isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() - stateHolder.updateStateWithAccountList( - accountStatusList = accountList, - isAccountsModeEnabled = isAccountsModeEnabled, - ) + stateHolder.updateStateWithAccountList( + accountStatusList = accountList, + isAccountsModeEnabled = isAccountsModeEnabled, + ) - cachedAccountStatusList = accountList - } else { - val tokenList = getTokenList() ?: return@launch - stateHolder.updateStateWithTokenList(tokenList) - cachedTokenList = tokenList - } + cachedAccountStatusList = accountList } } - private suspend fun getTokenList(): TokenList? { - val maybeTokenList = getTokenListUseCase.launch(userWalletId) - .filterNot(Lce::isLoading) - .firstOrNull() - ?: return null - - return maybeTokenList - .onError(stateHolder::updateStateWithError) - .getOrNull(isPartialContentAccepted = false) - } - private fun bootstrapDragAndDropUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - dragAndDropAdapterV2.dragAndDropUpdates - .distinctUntilChanged() - .onEach { (type, updatedListState) -> - disableSortingByBalanceIfListChangedV2(type) + dragAndDropAdapter.dragAndDropUpdates + .distinctUntilChanged() + .onEach { (type, updatedListState) -> + disableSortingByBalanceIfListChanged(type) - stateHolder.updateStateWithManualSortingV2(updatedListState) - } - .launchIn(modelScope) - } else { - dragAndDropAdapter.dragAndDropUpdates - .distinctUntilChanged() - .onEach { (type, updatedListState) -> - disableSortingByBalanceIfListChanged(type) - - stateHolder.updateStateWithManualSorting(updatedListState) - } - .launchIn(modelScope) - } + stateHolder.updateStateWithManualSorting(updatedListState) + } + .launchIn(modelScope) } private fun disableSortingByBalanceIfListChanged(dragOperationType: DragAndDropAdapter.DragOperation.Type) { if (dragOperationType !is DragAndDropAdapter.DragOperation.Type.End) return - if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { - cachedTokenList = cachedTokenList?.disableSortingByBalance() - stateHolder.disableSortingByBalance() - } - } - - private fun disableSortingByBalanceIfListChangedV2(dragOperationType: DragAndDropAdapterV2.DragOperation.Type) { - if (dragOperationType !is DragAndDropAdapterV2.DragOperation.Type.End) return - if (uiState.value.header.isSortedByBalance && dragOperationType.isItemsOrderChanged) { cachedAccountStatusList = cachedAccountStatusList?.copy(sortType = TokensSortType.NONE) stateHolder.disableSortingByBalance() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt new file mode 100644 index 0000000000..638cfa480b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/OrganizeTokensStateHolder.kt @@ -0,0 +1,111 @@ +package com.tangem.feature.wallet.child.organizetokens.model + +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.event.triggeredEvent +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.tokens.error.TokenListSortingError +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter +import com.tangem.feature.wallet.child.organizetokens.model.converter.TokenListToStateConverter +import com.tangem.feature.wallet.child.organizetokens.model.converter.error.TokenListSortingErrorConverter +import com.tangem.feature.wallet.child.organizetokens.model.dnd.DragAndDropAdapter +import com.tangem.utils.Provider +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +internal class OrganizeTokensStateHolder( + private val intents: OrganizeTokensIntents, + private val dragAndDropAdapter: DragAndDropAdapter, + private val appCurrencyProvider: Provider, +) { + + private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) + + private val inProgressStateConverter by lazy { InProgressStateConverter() } + + private val tokenListSortingErrorConverter by lazy { + TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) + } + + val stateFlow: StateFlow = stateFlowInternal + + fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { + updateState { + TokenListToStateConverter( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = appCurrencyProvider(), + ).transform(this) + } + } + + fun updateStateAfterTokenListSorting(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { + updateState { + TokenListToStateConverter( + accountStatusList = accountStatusList, + isAccountsMode = isAccountsModeEnabled, + appCurrency = appCurrencyProvider(), + ).transform(this).copy( + scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), + ) + } + } + + fun updateStateToDisplayProgress() { + updateState { inProgressStateConverter.convert(value = this) } + } + + fun updateStateToHideProgress() { + updateState { inProgressStateConverter.convertBack(value = this) } + } + + fun updateStateWithManualSorting(tokenListUM: OrganizeTokensListUM) { + updateState { copy(tokenListUM = tokenListUM) } + } + + fun disableSortingByBalance() { + updateState { copy(header = header.copy(isSortedByBalance = false)) } + } + + fun updateHiddenState(isBalanceHidden: Boolean) { + updateState { copy(isBalanceHidden = isBalanceHidden) } + } + + fun updateStateWithError(error: TokenListSortingError) { + updateState { tokenListSortingErrorConverter.convert(error) } + } + + private fun getInitialState(): OrganizeTokensState { + return OrganizeTokensState( + onBackClick = intents::onBackClick, + tokenListUM = OrganizeTokensListUM.EmptyList, + header = OrganizeTokensState.HeaderConfig( + onSortClick = intents::onSortClick, + onGroupClick = intents::onGroupClick, + ), + actions = OrganizeTokensState.ActionsConfig( + onApplyClick = intents::onApplyClick, + onCancelClick = intents::onCancelClick, + ), + dndConfig = OrganizeTokensState.DragAndDropConfig( + onItemDragged = dragAndDropAdapter::onItemDragged, + onItemDragStart = dragAndDropAdapter::onItemDraggingStart, + onItemDragEnd = dragAndDropAdapter::onItemDraggingEnd, + canDragItemOver = dragAndDropAdapter::canDragItemOver, + ), + scrollListToTop = consumedEvent(), + isBalanceHidden = true, + ) + } + + private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { + stateFlowInternal.update(block) + } + + private fun consumeScrollListToTopEvent() { + updateState { copy(scrollListToTop = consumedEvent()) } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt similarity index 59% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt index 80ea9a25b1..1133e2b1b9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemOperations.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem internal fun getGroupPlaceholder(index: Int, accountId: String = ""): DraggableItem.Placeholder { return DraggableItem.Placeholder( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt similarity index 63% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt index 2c10ffaf82..f668be3be6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/DraggableItemsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/DraggableItemsOperations.kt @@ -1,36 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem -internal fun List.uniteItems(): List { - val items = prepareItems() - val lastItemIndex = items.lastIndex - - return prepareItems().mapIndexed { index, item -> - val mode = when (index) { - // 1 index is used because the first item is always a placeholder, check `prepareItems()` function - 1 -> DraggableItem.RoundingMode.Top() - lastItemIndex -> DraggableItem.RoundingMode.Bottom() - else -> when (item) { - is DraggableItem.Portfolio, - is DraggableItem.Placeholder, - -> DraggableItem.RoundingMode.None - is DraggableItem.GroupHeader -> DraggableItem.RoundingMode.Top(showGap = true) - is DraggableItem.Token -> if (items[index + 1] is DraggableItem.Placeholder) { - DraggableItem.RoundingMode.Bottom(showGap = true) - } else { - DraggableItem.RoundingMode.None - } - } - } - - item - .updateRoundingMode(mode) - .updateShadowVisibility(show = false) - } -} - -internal fun List.uniteItemsV2(isAccountsMode: Boolean): List { +internal fun List.uniteItems(isAccountsMode: Boolean): List { val items = this val lastItemIndex = items.lastIndex @@ -92,27 +64,6 @@ internal fun List.divideMovingItem(movingItem: DraggableItem): Li return mutableList } -/** - * !!! Workaround !!! - * - * We need to add a [DraggableItem.Placeholder] (since it's not draggable) as the first item of the list, because the - * [DND library](https://github.com/aclassen/ComposeReorderable) glitches when a user tries to drag the first item. - * - * @since 07.09.2023 - * */ -private fun List.prepareItems(): List { - val firstPlaceholderId = "initial_placeholder" - val items = this - - return mutableListOf().apply { - add(DraggableItem.Placeholder(firstPlaceholderId)) - - val itemsWithoutFirstPlaceholder = items.filterNot { it.id == firstPlaceholderId } - - addAll(itemsWithoutFirstPlaceholder) - } -} - /** * Applying rounding to tokens * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt similarity index 78% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt index 0225b0d6ca..b4d70c5fb0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/IdsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/IdsOperations.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt new file mode 100644 index 0000000000..2935a14855 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/OrganiseTokensListStateOperations.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.wallet.child.organizetokens.model.common + +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList + +internal inline fun OrganizeTokensListUM.updateItems( + update: (PersistentList) -> List, +): OrganizeTokensListUM { + val updatedItems = update(items).toPersistentList() + + return when (this) { + is OrganizeTokensListUM.AccountList -> copy(items = updatedItems) + is OrganizeTokensListUM.TokensList -> copy(items = updatedItems) + OrganizeTokensListUM.EmptyList -> this + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/TokenListOperations.kt similarity index 83% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/TokenListOperations.kt index 8600d8beaf..b298e1121c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/TokenListOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/common/TokenListOperations.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common +package com.tangem.feature.wallet.child.organizetokens.model.common import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.tokenlist.TokenList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt similarity index 82% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt index 936385e8ea..7da5b51e59 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/InProgressStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/InProgressStateConverter.kt @@ -1,6 +1,6 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter +package com.tangem.feature.wallet.child.organizetokens.model.converter -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState import com.tangem.utils.converter.TwoWayConverter internal class InProgressStateConverter : TwoWayConverter { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt similarity index 83% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt index 279d59f8ed..61bb28d854 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/TokenListToStateConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter +package com.tangem.feature.wallet.child.organizetokens.model.converter import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter import com.tangem.domain.account.models.AccountStatusList @@ -8,17 +8,17 @@ import com.tangem.domain.models.TokensSortType import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.OrganizedTokenListConverter +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder +import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems +import com.tangem.feature.wallet.child.organizetokens.model.converter.items.OrganizedTokenListConverter import com.tangem.utils.converter.Converter import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.toPersistentList -internal class TokenListToStateConverterV2( +internal class TokenListToStateConverter( private val accountStatusList: AccountStatusList, private val isAccountsMode: Boolean, private val appCurrency: AppCurrency, @@ -82,7 +82,7 @@ internal class AccountTokenItemConverter( emptyList() } }.toList() - .uniteItemsV2(true).toPersistentList(), + .uniteItems(true).toPersistentList(), ) } else { OrganizeTokensListUM.TokensList( @@ -92,7 +92,7 @@ internal class AccountTokenItemConverter( add(getGroupPlaceholder(accountId = value.mainAccount.accountId.value, index = -1)) } addAll(organizedTokenListConverter.convert(value.mainAccount)) - }.uniteItemsV2(false) + }.uniteItems(false) .toPersistentList(), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListErrorConverter.kt similarity index 64% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListErrorConverter.kt index e2117bfc5f..506816829f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListErrorConverter.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error +package com.tangem.feature.wallet.child.organizetokens.model.converter.error import com.tangem.domain.tokens.error.TokenListError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt similarity index 65% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt index 7d7bcd16c5..f86c07468e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/error/TokenListSortingErrorConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/error/TokenListSortingErrorConverter.kt @@ -1,8 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error +package com.tangem.feature.wallet.child.organizetokens.model.converter.error import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.model.converter.InProgressStateConverter import com.tangem.utils.Provider import com.tangem.utils.converter.Converter diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt similarity index 87% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt index 478351d5dc..cd6c995427 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/CryptoCurrencyToDraggableItemConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState @@ -11,14 +11,14 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance import com.tangem.common.getTotalWithRewardsStakingBalance -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupHeaderId +import com.tangem.feature.wallet.child.organizetokens.model.common.getTokenItemId import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.orZero import java.math.BigDecimal -internal class CryptoCurrencyToDraggableItemConverterV2( +internal class CryptoCurrencyToDraggableItemConverter( private val appCurrency: AppCurrency, ) : Converter { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt similarity index 78% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt index 13206c8a13..9a68fca15d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/NetworkGroupToDraggableItemsConverter.kt @@ -1,16 +1,16 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupHeaderId +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder import com.tangem.utils.converter.Converter -internal class NetworkGroupToDraggableItemsConverterV2( - private val itemConverter: CryptoCurrencyToDraggableItemConverterV2, +internal class NetworkGroupToDraggableItemsConverter( + private val itemConverter: CryptoCurrencyToDraggableItemConverter, ) : Converter, List> { override fun convert(value: Pair): List { @@ -42,10 +42,10 @@ internal class NetworkGroupToDraggableItemsConverterV2( private fun createTokens(account: Account.CryptoPortfolio, group: NetworkGroup): List { return itemConverter.convertList( - group.currencies.map { + group.currencies.map { currencyStatus -> AccountCryptoCurrencyStatus( account = account, - status = it, + status = currencyStatus, ) }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt similarity index 85% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt index c0938d2748..c1f5ce4fef 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/OrganizedTokenListConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/converter/items/OrganizedTokenListConverter.kt @@ -1,10 +1,10 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items +package com.tangem.feature.wallet.child.organizetokens.model.converter.items import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -14,9 +14,9 @@ internal class OrganizedTokenListConverter( private val appCurrency: AppCurrency, ) : Converter> { - private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverterV2(appCurrency) } + private val tokensConverter by lazy { CryptoCurrencyToDraggableItemConverter(appCurrency) } private val groupsConverter by lazy { - NetworkGroupToDraggableItemsConverterV2(tokensConverter) + NetworkGroupToDraggableItemsConverter(tokensConverter) } override fun convert(value: AccountStatus.CryptoPortfolio): PersistentList { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt similarity index 87% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt index 6c0a7531b8..7e4e3b77fd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapterV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DragAndDropAdapter.kt @@ -1,11 +1,11 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd +package com.tangem.feature.wallet.child.organizetokens.model.dnd -import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItemsV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.model.DragAndDropIntents +import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem +import com.tangem.feature.wallet.child.organizetokens.model.common.uniteItems +import com.tangem.feature.wallet.child.organizetokens.model.common.updateItems import com.tangem.utils.Provider import kotlinx.collections.immutable.mutate import kotlinx.coroutines.flow.Flow @@ -13,7 +13,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull import org.burnoutcrew.reorderable.ItemPosition -internal class DragAndDropAdapterV2( +internal class DragAndDropAdapter( private val tokenListUMProvider: Provider, ) : DragAndDropIntents { @@ -81,7 +81,7 @@ internal class DragAndDropAdapterV2( is DraggableItem.Placeholder, is DraggableItem.Portfolio, -> items - is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroupV2(items, item) + is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) .divideMovingItem(item) is DraggableItem.Token -> items.divideMovingItem(item) } @@ -96,11 +96,11 @@ internal class DragAndDropAdapterV2( updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) { when (draggingItem) { is DraggableItem.GroupHeader -> { - draggableGroupsOperations.expandGroupsV2(items) - .uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList) + draggableGroupsOperations.expandGroups(items) + .uniteItems(tokenListUM is OrganizeTokensListUM.AccountList) } is DraggableItem.Token -> { - items.uniteItemsV2(tokenListUM is OrganizeTokensListUM.AccountList) + items.uniteItems(tokenListUM is OrganizeTokensListUM.AccountList) } is DraggableItem.Placeholder, is DraggableItem.Portfolio, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt similarity index 59% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt index e92d772829..7ce66f0a2e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DraggableGroupsOperations.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/model/dnd/DraggableGroupsOperations.kt @@ -1,9 +1,8 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd +package com.tangem.feature.wallet.child.organizetokens.model.dnd -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.model.common.divideMovingItem +import com.tangem.feature.wallet.child.organizetokens.model.common.getGroupPlaceholder internal class DraggableGroupsOperations { @@ -24,47 +23,9 @@ internal class DraggableGroupsOperations { return itemsWithoutGroupTokens.divideMovingItem(movingGroup) } - fun collapseGroupV2(items: List, movingGroup: DraggableItem.GroupHeader): List { - if (!groupIdToTokens.isNullOrEmpty()) return items - - groupIdToTokens = items - .asSequence() - .filterIsInstance() - .groupBy { it.groupId } - - val itemsWithoutGroupTokens = items.filterNot { - it is DraggableItem.Token && it.groupId == movingGroup.id - } - - return itemsWithoutGroupTokens.divideMovingItem(movingGroup) - } - fun expandGroups(items: List): List { if (groupIdToTokens.isNullOrEmpty()) return items - val currentGroups = items.filterIsInstance() - val lastGroupIndex = currentGroups.lastIndex - - val expandedGroups = currentGroups - .flatMapIndexed { index, group -> - buildList { - add(group) - addAll(groupIdToTokens?.get(group.id).orEmpty()) - if (index != lastGroupIndex) { - add(getGroupPlaceholder(index)) - } - } - } - .uniteItems() - - groupIdToTokens = null - - return expandedGroups - } - - fun expandGroupsV2(items: List): List { - if (groupIdToTokens.isNullOrEmpty()) return items - val accountList = items.filterIsInstance() val currentGroups = items.filterIsInstance() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt similarity index 95% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt index 2996d749be..1c23378393 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensScreen.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.wallet.presentation.organizetokens +package com.tangem.feature.wallet.child.organizetokens.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler @@ -44,12 +44,11 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.OrganizeTokensScreenTestTags import com.tangem.core.ui.utils.WindowInsetsZero import com.tangem.core.ui.utils.lazyListItemPosition +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.child.organizetokens.ui.preview.OrganizeTokensPreview import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import org.burnoutcrew.reorderable.ReorderableLazyListState import org.burnoutcrew.reorderable.rememberReorderableLazyListState import org.burnoutcrew.reorderable.reorderable @@ -76,7 +75,6 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier .fillMaxSize(), listState = tokensListState, tokensListUM = state.tokenListUM, - state = state.itemsState, dndConfig = state.dndConfig, isBalanceHidden = state.isBalanceHidden, ) @@ -98,18 +96,13 @@ internal fun OrganizeTokensScreen(state: OrganizeTokensState, modifier: Modifier @Composable private fun TokenList( listState: LazyListState, - state: OrganizeTokensListState, tokensListUM: OrganizeTokensListUM, dndConfig: OrganizeTokensState.DragAndDropConfig, isBalanceHidden: Boolean, modifier: Modifier = Modifier, ) { val hapticFeedback = LocalHapticFeedback.current - val tokenList = if (tokensListUM !is OrganizeTokensListUM.EmptyList) { - tokensListUM.items - } else { - state.items - } + val tokenList = tokensListUM.items Box(modifier = modifier) { val onDragEnd: (Int, Int) -> Unit = remember { { _, _ -> @@ -423,8 +416,8 @@ private fun OrganizeTokensScreenPreview( private class OrganizeTokensStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.organizeTokensState, - WalletPreviewData.groupedOrganizeTokensState, + OrganizeTokensPreview.stateAccounts, + OrganizeTokensPreview.state, ), ) // endregion Preview \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt new file mode 100644 index 0000000000..47869d9b69 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/preview/OrganizeTokensPreview.kt @@ -0,0 +1,127 @@ +package com.tangem.feature.wallet.child.organizetokens.ui.preview + +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemColorPalette +import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensListUM +import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeTokensState +import com.tangem.feature.wallet.impl.R +import kotlinx.collections.immutable.PersistentList +import kotlinx.collections.immutable.toPersistentList +import java.util.UUID + +internal object OrganizeTokensPreview { + + private const val networksSize = 10 + private const val tokensSize = 3 + + private val tokenItemDragState by lazy { + TokenItemState.Draggable( + id = UUID.randomUUID().toString(), + iconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = R.drawable.img_polygon_22, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"), + ) + } + + private val draggableItems: PersistentList by lazy { + List(networksSize) { it } + .flatMap { index -> + val lastNetworkIndex = networksSize - 1 + val lastTokenIndex = tokensSize - 1 + val networkNumber = index + 1 + + val group = DraggableItem.GroupHeader( + id = networkNumber, + networkName = "$networkNumber", + + roundingMode = when (index) { + 0 -> DraggableItem.RoundingMode.Top() + lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() + else -> DraggableItem.RoundingMode.None + }, + accountId = "account_$networkNumber", + ) + + val tokens: MutableList = mutableListOf() + repeat(times = tokensSize) { i -> + val tokenNumber = i + 1 + tokens.add( + DraggableItem.Token( + tokenItemState = tokenItemDragState.copy( + id = "${group.id}_token_$tokenNumber", + titleState = TokenItemState.TitleState.Content( + text = stringReference(value = "Token $tokenNumber from $networkNumber network"), + ), + ), + groupId = group.id, + accountId = "account_$networkNumber", + roundingMode = when { + i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() + else -> DraggableItem.RoundingMode.None + }, + ), + ) + } + + val divider = DraggableItem.Placeholder( + id = "divider_$networkNumber", + accountId = "account_$networkNumber", + ) + + buildList { + add(group) + addAll(tokens) + if (index != lastNetworkIndex) { + add(divider) + } + } + } + .toPersistentList() + } + + val stateAccounts by lazy { + OrganizeTokensState( + onBackClick = {}, + tokenListUM = OrganizeTokensListUM.AccountList( + items = draggableItems, + isGrouped = true, + ), + header = OrganizeTokensState.HeaderConfig( + onSortClick = {}, + onGroupClick = {}, + ), + dndConfig = OrganizeTokensState.DragAndDropConfig( + onItemDragged = { _, _ -> }, + onItemDragStart = {}, + canDragItemOver = { _, _ -> false }, + onItemDragEnd = {}, + ), + actions = OrganizeTokensState.ActionsConfig( + onApplyClick = {}, + onCancelClick = {}, + ), + scrollListToTop = consumedEvent(), + isBalanceHidden = true, + ) + } + + val state by lazy { + stateAccounts.copy( + tokenListUM = OrganizeTokensListUM.TokensList( + items = draggableItems, + isGrouped = true, + ), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt index be88fcbccd..1c48f40af5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt @@ -1,23 +1,11 @@ package com.tangem.feature.wallet.presentation.common -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.event.consumedEvent import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.TangemColorPalette import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState import com.tangem.feature.wallet.presentation.wallet.state.model.* -import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList -import java.util.UUID @Suppress("LargeClass") internal object WalletPreviewData { @@ -67,124 +55,6 @@ internal object WalletPreviewData { ) } - private val tokenItemDragState by lazy { - TokenItemState.Draggable( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_polygon_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "3 172,14 $"), - ) - } - - private const val networksSize = 10 - private const val tokensSize = 3 - private val draggableItems: PersistentList by lazy { - List(networksSize) { it } - .flatMap { index -> - val lastNetworkIndex = networksSize - 1 - val lastTokenIndex = tokensSize - 1 - val networkNumber = index + 1 - - val group = DraggableItem.GroupHeader( - id = networkNumber, - networkName = "$networkNumber", - - roundingMode = when (index) { - 0 -> DraggableItem.RoundingMode.Top() - lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None - }, - accountId = "account_$networkNumber", - ) - - val tokens: MutableList = mutableListOf() - repeat(times = tokensSize) { i -> - val tokenNumber = i + 1 - tokens.add( - DraggableItem.Token( - tokenItemState = tokenItemDragState.copy( - id = "${group.id}_token_$tokenNumber", - titleState = TokenItemState.TitleState.Content( - text = stringReference(value = "Token $tokenNumber from $networkNumber network"), - ), - ), - groupId = group.id, - accountId = "account_$networkNumber", - roundingMode = when { - i == lastTokenIndex && index == lastNetworkIndex -> DraggableItem.RoundingMode.Bottom() - else -> DraggableItem.RoundingMode.None - }, - ), - ) - } - - val divider = DraggableItem.Placeholder( - id = "divider_$networkNumber", - accountId = "account_$networkNumber", - ) - - buildList { - add(group) - addAll(tokens) - if (index != lastNetworkIndex) { - add(divider) - } - } - } - .toPersistentList() - } - - private val draggableTokens by lazy { - draggableItems - .filterIsInstance() - .toMutableList() - .also { - it[0] = it[0].copy(roundingMode = DraggableItem.RoundingMode.Top()) - } - .toPersistentList() - } - - val groupedOrganizeTokensState by lazy { - OrganizeTokensState( - onBackClick = {}, - itemsState = OrganizeTokensListState.GroupedByNetwork( - items = draggableItems, - ), - tokenListUM = OrganizeTokensListUM.EmptyList, - header = OrganizeTokensState.HeaderConfig( - onSortClick = {}, - onGroupClick = {}, - ), - dndConfig = OrganizeTokensState.DragAndDropConfig( - onItemDragged = { _, _ -> }, - onItemDragStart = {}, - canDragItemOver = { _, _ -> false }, - onItemDragEnd = {}, - ), - actions = OrganizeTokensState.ActionsConfig( - onApplyClick = {}, - onCancelClick = {}, - ), - scrollListToTop = consumedEvent(), - isBalanceHidden = true, - ) - } - - val organizeTokensState by lazy { - groupedOrganizeTokensState.copy( - itemsState = OrganizeTokensListState.Ungrouped( - items = draggableTokens, - ), - ) - } - val actionsBottomSheet = ActionsBottomSheetConfig( actions = listOf( TokenActionButtonConfig( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt deleted file mode 100644 index f1eb7915cf..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/OrganizeTokensStateHolder.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens - -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.InProgressStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.TokenListToStateConverterV2 -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListErrorConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.error.TokenListSortingErrorConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.CryptoCurrencyToDraggableItemConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.NetworkGroupToDraggableItemsConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter -import com.tangem.feature.wallet.presentation.organizetokens.utils.dnd.DragAndDropAdapterV2 -import com.tangem.utils.Provider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update - -internal class OrganizeTokensStateHolder( - private val intents: OrganizeTokensIntents, - private val dragAndDropIntents: DragAndDropIntents, - private val dragAndDropAdapterV2: DragAndDropAdapterV2, - private val appCurrencyProvider: Provider, - private val accountsFeatureToggles: AccountsFeatureToggles, -) { - - private val stateFlowInternal: MutableStateFlow = MutableStateFlow(getInitialState()) - - private val tokenListConverter by lazy { - val tokensConverter = CryptoCurrencyToDraggableItemConverter(appCurrencyProvider) - val itemsConverter = TokenListToListStateConverter( - tokensConverter = tokensConverter, - groupsConverter = NetworkGroupToDraggableItemsConverter(tokensConverter), - ) - - TokenListToStateConverter(Provider(stateFlowInternal::value), itemsConverter) - } - - private val inProgressStateConverter by lazy { InProgressStateConverter() } - - private val tokenListErrorConverter by lazy { - TokenListErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) - } - - private val tokenListSortingErrorConverter by lazy { - TokenListSortingErrorConverter(Provider(stateFlowInternal::value), inProgressStateConverter) - } - - val stateFlow: StateFlow = stateFlowInternal - - fun updateStateWithTokenList(tokenList: TokenList) { - updateState { tokenListConverter.convert(tokenList) } - } - - fun updateStateWithAccountList(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { - updateState { - TokenListToStateConverterV2( - accountStatusList = accountStatusList, - isAccountsMode = isAccountsModeEnabled, - appCurrency = appCurrencyProvider(), - ).transform(this) - } - } - - fun updateStateAfterTokenListSorting(tokenList: TokenList) { - updateState { - tokenListConverter.convert(tokenList).copy( - scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), - ) - } - } - - fun updateStateAfterTokenListSortingV2(accountStatusList: AccountStatusList, isAccountsModeEnabled: Boolean) { - updateState { - TokenListToStateConverterV2( - accountStatusList = accountStatusList, - isAccountsMode = isAccountsModeEnabled, - appCurrency = appCurrencyProvider(), - ).transform(this).copy( - scrollListToTop = triggeredEvent(Unit, ::consumeScrollListToTopEvent), - ) - } - } - - fun updateStateToDisplayProgress() { - updateState { inProgressStateConverter.convert(value = this) } - } - - fun updateStateToHideProgress() { - updateState { inProgressStateConverter.convertBack(value = this) } - } - - fun updateStateWithManualSortingV2(tokenListUM: OrganizeTokensListUM) { - updateState { copy(tokenListUM = tokenListUM) } - } - - fun updateStateWithManualSorting(itemsState: OrganizeTokensListState) { - updateState { copy(itemsState = itemsState) } - } - - fun disableSortingByBalance() { - updateState { copy(header = header.copy(isSortedByBalance = false)) } - } - - fun updateHiddenState(isBalanceHidden: Boolean) { - updateState { copy(isBalanceHidden = isBalanceHidden) } - } - - fun updateStateWithError(error: TokenListError) { - updateState { tokenListErrorConverter.convert(error) } - } - - fun updateStateWithError(error: TokenListSortingError) { - updateState { tokenListSortingErrorConverter.convert(error) } - } - - private fun getInitialState(): OrganizeTokensState { - return OrganizeTokensState( - onBackClick = intents::onBackClick, - itemsState = OrganizeTokensListState.Empty, - tokenListUM = OrganizeTokensListUM.EmptyList, - header = OrganizeTokensState.HeaderConfig( - onSortClick = intents::onSortClick, - onGroupClick = intents::onGroupClick, - ), - actions = OrganizeTokensState.ActionsConfig( - onApplyClick = intents::onApplyClick, - onCancelClick = intents::onCancelClick, - ), - dndConfig = if (accountsFeatureToggles.isFeatureEnabled) { - OrganizeTokensState.DragAndDropConfig( - onItemDragged = dragAndDropAdapterV2::onItemDragged, - onItemDragStart = dragAndDropAdapterV2::onItemDraggingStart, - onItemDragEnd = dragAndDropAdapterV2::onItemDraggingEnd, - canDragItemOver = dragAndDropAdapterV2::canDragItemOver, - ) - } else { - OrganizeTokensState.DragAndDropConfig( - onItemDragged = dragAndDropIntents::onItemDragged, - onItemDragStart = dragAndDropIntents::onItemDraggingStart, - onItemDragEnd = dragAndDropIntents::onItemDraggingEnd, - canDragItemOver = dragAndDropIntents::canDragItemOver, - ) - }, - scrollListToTop = consumedEvent(), - isBalanceHidden = true, - ) - } - - private inline fun updateState(block: OrganizeTokensState.() -> OrganizeTokensState) { - stateFlowInternal.update(block) - } - - private fun consumeScrollListToTopEvent() { - updateState { copy(scrollListToTop = consumedEvent()) } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt deleted file mode 100644 index 770ada3985..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/CryptoCurrenciesIdsResolver.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils - -import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.status.model.AccountCryptoCurrencies -import com.tangem.domain.models.account.filterCryptoPortfolio -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM - -internal class CryptoCurrenciesIdsResolver { - - fun resolve(listState: OrganizeTokensListState, tokenList: TokenList?): List { - val draggableTokens = when (listState) { - is OrganizeTokensListState.Empty -> return emptyList() - is OrganizeTokensListState.GroupedByNetwork -> listState.items.filterIsInstance() - is OrganizeTokensListState.Ungrouped -> listState.items.filterIsInstance() - } - val currenciesStatuses = when (tokenList) { - is TokenList.GroupedByNetwork -> tokenList.groups.flatMap { it.currencies } - is TokenList.Ungrouped -> tokenList.currencies - is TokenList.Empty, - null, - -> return emptyList() - } - - return draggableTokens.mapNotNull { draggableToken -> - val currencyStatus = currenciesStatuses.firstOrNull { - it.currency.id.value == draggableToken.id - } - - currencyStatus?.currency?.id - } - } - - @Suppress("UseOrEmpty") - fun resolveV2(tokensListUM: OrganizeTokensListUM, accountStatusList: AccountStatusList?): AccountCryptoCurrencies { - val draggableTokens = when (tokensListUM) { - OrganizeTokensListUM.EmptyList -> return emptyMap() - is OrganizeTokensListUM.AccountList, - is OrganizeTokensListUM.TokensList, - -> tokensListUM.items.filterIsInstance() - } - - return accountStatusList?.accountStatuses - ?.filterCryptoPortfolio() - ?.filter { it.tokenList != TokenList.Empty } - ?.associate { accountStatus -> - val currencies = accountStatus.flattenCurrencies() - accountStatus.account to draggableTokens - .asSequence() - .filter { it.accountId == accountStatus.account.accountId.value } - .mapNotNull { sortedToken -> - currencies.firstOrNull { it.currency.id.value == sortedToken.id }?.currency - } - .toList() - } ?: emptyMap() - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt deleted file mode 100644 index d67847aa0a..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/common/OrganiseTokensListStateOperations.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.common - -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListUM -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList - -internal inline fun OrganizeTokensListState.updateItems( - update: (PersistentList) -> List, -): OrganizeTokensListState { - val updatedItems = update(items).toPersistentList() - - return when (this) { - is OrganizeTokensListState.GroupedByNetwork -> copy(items = updatedItems) - is OrganizeTokensListState.Ungrouped -> copy(items = updatedItems) - is OrganizeTokensListState.Empty -> this - } -} - -internal inline fun OrganizeTokensListUM.updateItems( - update: (PersistentList) -> List, -): OrganizeTokensListUM { - val updatedItems = update(items).toPersistentList() - - return when (this) { - is OrganizeTokensListUM.AccountList -> copy(items = updatedItems) - is OrganizeTokensListUM.TokensList -> copy(items = updatedItems) - OrganizeTokensListUM.EmptyList -> this - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt deleted file mode 100644 index d561e3b262..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/TokenListToStateConverter.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter - -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensState -import com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items.TokenListToListStateConverter -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -internal class TokenListToStateConverter( - private val currentState: Provider, - private val itemsConverter: TokenListToListStateConverter, -) : Converter { - - override fun convert(value: TokenList): OrganizeTokensState { - val state = currentState() - val itemsState = itemsConverter.convert(value) - - return state.copy( - itemsState = itemsState, - header = state.header.copy( - isEnabled = itemsState !is OrganizeTokensListState.Empty, - isSortedByBalance = value.sortedBy == TokensSortType.BALANCE, - isGrouped = value is TokenList.GroupedByNetwork, - ), - actions = state.actions.copy( - canApply = itemsState !is OrganizeTokensListState.Empty, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt deleted file mode 100644 index 79ea585f1d..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/CryptoCurrencyToDraggableItemConverter.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items - -import com.tangem.common.getTotalWithRewardsStakingBalance -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.stringReference -import com.tangem.core.ui.format.bigdecimal.BigDecimalFormatConstants -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.domain.models.staking.StakingBalance -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getTokenItemId -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.orZero -import java.math.BigDecimal - -internal class CryptoCurrencyToDraggableItemConverter( - private val appCurrencyProvider: Provider, -) : Converter { - - private val iconStateConverter = CryptoCurrencyToIconStateConverter() - - override fun convert(value: CryptoCurrencyStatus): DraggableItem.Token { - return createDraggableToken(value, appCurrencyProvider()) - } - - override fun convertList(input: Collection): List { - val appCurrency = appCurrencyProvider() - - return input.map { createDraggableToken(it, appCurrency) } - } - - private fun createDraggableToken( - currencyStatus: CryptoCurrencyStatus, - appCurrency: AppCurrency, - ): DraggableItem.Token { - return DraggableItem.Token( - tokenItemState = createTokenItemState(currencyStatus, appCurrency), - groupId = getGroupHeaderId(currencyStatus.currency.network), - ) - } - - private fun createTokenItemState( - currencyStatus: CryptoCurrencyStatus, - appCurrency: AppCurrency, - ): TokenItemState.Draggable { - val currency = currencyStatus.currency - - return TokenItemState.Draggable( - id = getTokenItemId(currency.id), - iconState = iconStateConverter.convert(currencyStatus), - titleState = TokenItemState.TitleState.Content(text = stringReference(currency.name)), - subtitle2State = if (currencyStatus.value.isError) { - TokenItemState.Subtitle2State.Unreachable - } else { - TokenItemState.Subtitle2State.TextContent(text = getFormattedFiatAmount(currencyStatus, appCurrency)) - }, - ) - } - - private fun getFormattedFiatAmount(currency: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - val stakingBalance = currency.value.stakingBalance as? StakingBalance.Data - val fiatRate = currency.value.fiatRate ?: BigDecimal.ZERO - val fiatStakingBalance = stakingBalance?.getTotalWithRewardsStakingBalance(currency.currency.network.rawId) - ?.multiply(fiatRate).orZero() - - val fiatAmount = currency.value.fiatAmount ?: return BigDecimalFormatConstants.EMPTY_BALANCE_SIGN - return (fiatAmount + fiatStakingBalance).format { fiat(appCurrency.code, appCurrency.symbol) } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt deleted file mode 100644 index 73ecfe3f79..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/NetworkGroupToDraggableItemsConverter.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items - -import com.tangem.domain.models.tokenlist.TokenList.GroupedByNetwork.NetworkGroup -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupHeaderId -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.getGroupPlaceholder -import com.tangem.utils.converter.Converter - -internal class NetworkGroupToDraggableItemsConverter( - private val itemConverter: CryptoCurrencyToDraggableItemConverter, -) : Converter> { - - override fun convert(value: NetworkGroup): List { - return buildList { - add(createGroupHeader(value)) - addAll(createTokens(value)) - } - } - - override fun convertList(input: Collection): List> { - val lastItemIndex = input.size - 1 - - return input.mapIndexed { index, networkGroup -> - convert(networkGroup).toMutableList() - .also { mutableGroup -> - if (index != lastItemIndex) { - mutableGroup.add(getGroupPlaceholder(index)) - } - } - } - } - - private fun createGroupHeader(group: NetworkGroup) = DraggableItem.GroupHeader( - id = getGroupHeaderId(group.network), - networkName = group.network.name, - ) - - private fun createTokens(group: NetworkGroup): List { - return itemConverter.convertList(group.currencies) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt deleted file mode 100644 index 770d63c0f4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/converter/items/TokenListToListStateConverter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.converter.items - -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.toPersistentList - -internal class TokenListToListStateConverter( - private val groupsConverter: NetworkGroupToDraggableItemsConverter, - private val tokensConverter: CryptoCurrencyToDraggableItemConverter, -) : Converter { - - override fun convert(value: TokenList): OrganizeTokensListState { - return when (value) { - is TokenList.GroupedByNetwork -> createListState(value) - is TokenList.Ungrouped -> createListState(value) - is TokenList.Empty -> createEmptyListState() - } - } - - private fun createListState(tokenList: TokenList.GroupedByNetwork): OrganizeTokensListState.GroupedByNetwork { - return OrganizeTokensListState.GroupedByNetwork( - items = groupsConverter.convertList(tokenList.groups) - .flatten() - .uniteItems() - .toPersistentList(), - ) - } - - @Suppress("UNCHECKED_CAST") // Erased type - private fun createListState(tokenList: TokenList.Ungrouped): OrganizeTokensListState.Ungrouped { - return OrganizeTokensListState.Ungrouped( - items = tokensConverter.convertList(tokenList.currencies) - .uniteItems() - .toPersistentList() as PersistentList, - ) - } - - private fun createEmptyListState(): OrganizeTokensListState.Empty { - return OrganizeTokensListState.Empty - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt deleted file mode 100644 index 7158aa1260..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/organizetokens/utils/dnd/DragAndDropAdapter.kt +++ /dev/null @@ -1,185 +0,0 @@ -package com.tangem.feature.wallet.presentation.organizetokens.utils.dnd - -import com.tangem.feature.wallet.presentation.organizetokens.DragAndDropIntents -import com.tangem.feature.wallet.presentation.organizetokens.model.DraggableItem -import com.tangem.feature.wallet.presentation.organizetokens.model.OrganizeTokensListState -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.divideMovingItem -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.uniteItems -import com.tangem.feature.wallet.presentation.organizetokens.utils.common.updateItems -import com.tangem.utils.Provider -import kotlinx.collections.immutable.mutate -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.filterNotNull -import org.burnoutcrew.reorderable.ItemPosition - -internal class DragAndDropAdapter( - private val listStateProvider: Provider, -) : DragAndDropIntents { - - private val draggableGroupsOperations = DraggableGroupsOperations() - - private val externalListState: OrganizeTokensListState - get() = listStateProvider.invoke() - - private val dragAndDropUpdatesInternal: MutableStateFlow = MutableStateFlow(value = null) - - private var draggingItem: DraggableItem? = null - private var draggingListState: OrganizeTokensListState? = null - - val dragAndDropUpdates: Flow - get() = dragAndDropUpdatesInternal.filterNotNull() - - override fun canDragItemOver(dragOver: ItemPosition, dragging: ItemPosition): Boolean { - val items = when (val listState = externalListState) { - is OrganizeTokensListState.GroupedByNetwork -> listState.items - is OrganizeTokensListState.Empty, - is OrganizeTokensListState.Ungrouped, - -> return true // If ungrouped then item can be moved anywhere - } - - val (dragOverItem, draggingItem) = findItemsToMove( - items = items, - moveOverItemKey = dragOver.key, - movedItemKey = dragging.key, - ) - - if (dragOverItem == null || draggingItem == null) { - return false - } - - return when (draggingItem) { - is DraggableItem.GroupHeader -> checkCanMoveHeaderOver(dragOver, dragOverItem, items.lastIndex) - is DraggableItem.Token -> checkCanMoveTokenOver(draggingItem, dragOverItem) - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> false - } - } - - override fun onItemDraggingStart(item: DraggableItem) { - if (draggingItem != null) return - draggingItem = item - - updateListState(DragOperation.Type.Start) { - when (item) { - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> items - is DraggableItem.GroupHeader -> draggableGroupsOperations.collapseGroup(items, item) - is DraggableItem.Token -> when (this) { - is OrganizeTokensListState.GroupedByNetwork -> items.divideMovingItem(item) - is OrganizeTokensListState.Ungrouped -> items.divideMovingItem(item) - is OrganizeTokensListState.Empty -> items - } - } - } - - draggingListState = externalListState - } - - override fun onItemDraggingEnd() { - val draggingItem = draggingItem ?: return - - updateListState(DragOperation.Type.End(isItemsOrderChanged = checkIsItemsOrderChanged())) { - when (draggingItem) { - is DraggableItem.GroupHeader -> draggableGroupsOperations.expandGroups(items) - is DraggableItem.Token -> items.uniteItems() - is DraggableItem.Placeholder, - is DraggableItem.Portfolio, - -> items - } - } - - this.draggingItem = null - } - - override fun onItemDragged(from: ItemPosition, to: ItemPosition) { - updateListState(DragOperation.Type.Dragged) { - items.mutate { - it.add(to.index, it.removeAt(from.index)) - } - } - } - - private fun updateListState(type: DragOperation.Type, block: OrganizeTokensListState.() -> List) { - val updatedState = externalListState.updateItems { block(externalListState) } - - dragAndDropUpdatesInternal.value = DragOperation(type, updatedState) - } - - private fun findItemsToMove( - items: List, - moveOverItemKey: Any?, - movedItemKey: Any?, - ): Pair { - var moveOverItem: DraggableItem? = null - var movedItem: DraggableItem? = null - - for (item in items) { - if (item.id == moveOverItemKey) { - moveOverItem = item - } - if (item.id == movedItemKey) { - movedItem = item - } - if (moveOverItem != null && movedItem != null) { - break - } - } - - return Pair(moveOverItem, movedItem) - } - - private fun checkCanMoveHeaderOver( - moveOverItemPosition: ItemPosition, - moveOverItem: DraggableItem, - lastItemIndex: Int, - ): Boolean { - // Group item can be moved only to group divider or to ages of the items list - return when { - moveOverItemPosition.index == 0 -> true - moveOverItemPosition.index == lastItemIndex -> true - moveOverItem is DraggableItem.Placeholder -> true - else -> false - } - } - - private fun checkCanMoveTokenOver(item: DraggableItem.Token, moveOverItem: DraggableItem): Boolean { - // Token item can be moved only in its group - return when (moveOverItem) { - is DraggableItem.GroupHeader -> false // Token item can not be moved to group item - is DraggableItem.Token -> item.groupId == moveOverItem.groupId // Token item can not be moved over its group - is DraggableItem.Portfolio, - is DraggableItem.Placeholder, - -> false - } - } - - private fun checkIsItemsOrderChanged(): Boolean { - fun OrganizeTokensListState?.getItemsIds(): List? = this?.items?.mapNotNull { item -> - if (item is DraggableItem.Placeholder) { - null - } else { - item.id - } - } - - return externalListState.getItemsIds() != draggingListState.getItemsIds() - } - - data class DragOperation( - val type: Type, - val listState: OrganizeTokensListState, - ) { - - sealed class Type { - - data object Start : Type() - - data object Dragged : Type() - - data class End(val isItemsOrderChanged: Boolean) : Type() - } - } -} \ No newline at end of file From 973109c974771ab15f7c81c834d25e7c6223c986 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 19:38:10 +0300 Subject: [PATCH 41/97] Updated on 2026-08-14 --- .../tester/presentation/TesterActivity.kt | 13 +++ .../presentation/menu/state/TesterMenuUM.kt | 1 + .../presentation/navigation/TesterScreen.kt | 1 + .../storybook/entity/StoryBookPage.kt | 15 ++++ .../storybook/entity/StoryBookUM.kt | 7 ++ .../storybook/entity/StoryPageFactory.kt | 5 ++ .../storybook/page/background/Build.kt | 19 ++++ .../page/background/NorthernLightsStory.kt | 89 +++++++++++++++++++ .../storybook/ui/StoryBookListScreen.kt | 53 +++++++++++ .../storybook/ui/StoryBookScreen.kt | 25 ++++++ .../storybook/viewmodel/StateUpdater.kt | 21 +++++ .../storybook/viewmodel/StoryBookViewModel.kt | 49 ++++++++++ .../impl/src/main/res/values/strings.xml | 1 + 13 files changed, 299 insertions(+) create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index 28d6fe9135..a5dfc60808 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -35,6 +35,8 @@ import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.feature.tester.presentation.navigation.TesterScreen import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersScreen +import com.tangem.feature.tester.presentation.storybook.ui.StoryBookScreen +import com.tangem.feature.tester.presentation.storybook.viewmodel.StoryBookViewModel import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel @@ -86,6 +88,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TEST_PUSHES, ButtonUM.ACCOUNTS, ButtonUM.ADDRESSES_INFO, + ButtonUM.STORY_BOOK, ), onButtonClick = { buttonUM -> val route = when (buttonUM) { @@ -97,6 +100,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.TEST_PUSHES -> TesterScreen.TEST_PUSHES ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS ButtonUM.ADDRESSES_INFO -> TesterScreen.ADDRESSES_INFO + ButtonUM.STORY_BOOK -> TesterScreen.STORY_BOOK } innerTesterRouter.open(route) @@ -179,6 +183,15 @@ internal class TesterActivity : ComposeActivity() { AddressesInfoScreen(state) } + + composable(route = TesterScreen.STORY_BOOK.name) { + val viewModel = hiltViewModel().apply { + setupNavigation(innerTesterRouter) + } + val state by viewModel.uiState.collectAsStateWithLifecycle() + + StoryBookScreen(state) + } } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index 2d2f610f44..e3318e0cab 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -26,5 +26,6 @@ data class TesterMenuUM( TEST_PUSHES(R.string.test_push), ACCOUNTS(R.string.accounts), ADDRESSES_INFO(R.string.addresses_info), + STORY_BOOK(R.string.story_book), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index ccf4b0acd0..ca2ae6a0e9 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -15,4 +15,5 @@ internal enum class TesterScreen { TEST_PUSHES, ACCOUNTS, ADDRESSES_INFO, + STORY_BOOK, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt new file mode 100644 index 0000000000..1c730da1d5 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -0,0 +1,15 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal sealed interface StoryBookPage + +internal data object StoryList : StoryBookPage + +internal data class NorthernLightsStory( + val variant: Variant, + val onVariantChange: (Variant) -> Unit, +) : StoryBookPage { + enum class Variant { + Shader, + Simple, + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt new file mode 100644 index 0000000000..39d646a85a --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookUM.kt @@ -0,0 +1,7 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal data class StoryBookUM( + val currentPage: StoryBookPage = StoryList, + val onBackClick: () -> Unit, + val onStoryClick: (StoryPageFactory) -> Unit, +) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt new file mode 100644 index 0000000000..acea04d55b --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryPageFactory.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.tester.presentation.storybook.entity + +internal fun interface StoryPageFactory { + fun create(updatePage: ((StoryBookPage) -> StoryBookPage) -> Unit): StoryBookPage +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt new file mode 100644 index 0000000000..522c3d4ae1 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt @@ -0,0 +1,19 @@ +package com.tangem.feature.tester.presentation.storybook.page.background + +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): NorthernLightsStory { + return NorthernLightsStory( + variant = NorthernLightsStory.Variant.Shader, + onVariantChange = { newVariant -> + updateStory { currentState -> + currentState.copy(variant = newVariant) + } + }, + ) +} + +internal val northernLightsStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt new file mode 100644 index 0000000000..a5764e2be5 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt @@ -0,0 +1,89 @@ +@file:Suppress("MagicNumber") +package com.tangem.feature.tester.presentation.storybook.page.background + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory + +@Composable +internal fun NorthernLightsStory(state: NorthernLightsStory, modifier: Modifier = Modifier) { + Box(modifier = modifier.fillMaxSize()) { + NorthernLightsBackground( + modifier = Modifier.fillMaxSize(), + forceSimpleVersion = state.variant == NorthernLightsStory.Variant.Simple, + ) + + NorthernLightsVariantToggle( + selected = state.variant, + onSelect = state.onVariantChange, + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .padding(bottom = 24.dp) + .padding(horizontal = 24.dp), + ) + } +} + +@Composable +private fun NorthernLightsVariantToggle( + selected: NorthernLightsStory.Variant, + onSelect: (NorthernLightsStory.Variant) -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(shape) + .background(Color.Black.copy(alpha = 0.35f)) + .border(width = 1.dp, color = Color.White.copy(alpha = 0.15f), shape = shape) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + NorthernLightsStory.Variant.entries.forEach { variant -> + VariantChip( + label = variant.label, + selected = variant == selected, + onClick = { onSelect(variant) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun VariantChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background(if (selected) Color.White.copy(alpha = 0.2f) else Color.Transparent) + .clickable(onClick = onClick) + .padding(vertical = 10.dp, horizontal = 16.dp), + ) { + Text( + text = label, + color = Color.White, + fontSize = 14.sp, + ) + } +} + +private val NorthernLightsStory.Variant.label: String + get() = when (this) { + NorthernLightsStory.Variant.Shader -> "Shader" + NorthernLightsStory.Variant.Simple -> "Simple" + } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt new file mode 100644 index 0000000000..ac74e45a74 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt @@ -0,0 +1,53 @@ +package com.tangem.feature.tester.presentation.storybook.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import com.tangem.feature.tester.presentation.storybook.page.background.northernLightsStoryFactory + +private data class StoryItem(val title: String, val factory: StoryPageFactory) + +private fun buildStories() = listOf( + StoryItem(title = "Northern Lights Background", factory = northernLightsStoryFactory), +) + +@Composable +internal fun StoryBookListScreen(state: StoryBookUM, modifier: Modifier = Modifier) { + val stories = remember { buildStories() } + + LazyColumn( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary), + ) { + stickyHeader { + AppBarWithBackButton( + onBackClick = state.onBackClick, + text = "Storybook", + containerColor = TangemTheme.colors.background.primary, + ) + } + + items(items = stories, key = { it.title }) { item -> + PrimaryButton( + text = item.title, + onClick = { state.onStoryClick(item.factory) }, + 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/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt new file mode 100644 index 0000000000..73e643e0ac --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -0,0 +1,25 @@ +package com.tangem.feature.tester.presentation.storybook.ui + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryList +import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory + +@Composable +internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) { + BackHandler(onBack = state.onBackClick) + + AnimatedContent( + targetState = state.currentPage, + modifier = modifier, + ) { storyState -> + when (storyState) { + StoryList -> StoryBookListScreen(state = state) + is NorthernLightsStory -> NorthernLightsStory(state = storyState) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt new file mode 100644 index 0000000000..851570c925 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StateUpdater.kt @@ -0,0 +1,21 @@ +package com.tangem.feature.tester.presentation.storybook.viewmodel + +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookPage +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal interface StateUpdater { + fun updateStory(update: (T) -> T) +} + +internal inline fun storyPageFactory( + crossinline build: StateUpdater.() -> T, +): StoryPageFactory = StoryPageFactory { updatePage -> + val updater = object : StateUpdater { + override fun updateStory(update: (T) -> T) { + updatePage { current -> + if (current is T) update(current) else current + } + } + } + updater.build() +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt new file mode 100644 index 0000000000..e08ab8c0d7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/viewmodel/StoryBookViewModel.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.tester.presentation.storybook.viewmodel + +import androidx.lifecycle.ViewModel +import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter +import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM +import com.tangem.feature.tester.presentation.storybook.entity.StoryList +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +internal class StoryBookViewModel @Inject constructor() : ViewModel() { + + private var router: InnerTesterRouter? = null + + private val _uiState = MutableStateFlow( + StoryBookUM( + onBackClick = ::onBackClick, + onStoryClick = ::onStoryClick, + ), + ) + val uiState: StateFlow = _uiState.asStateFlow() + + fun setupNavigation(router: InnerTesterRouter) { + this.router = router + } + + private fun onBackClick() { + if (_uiState.value.currentPage !is StoryList) { + _uiState.update { it.copy(currentPage = StoryList) } + } else { + router?.back() + } + } + + private fun onStoryClick(factory: StoryPageFactory) { + _uiState.update { state -> + state.copy( + currentPage = factory.create { update -> + _uiState.update { s -> s.copy(currentPage = update(s.currentPage)) } + }, + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 9aadd20368..19d8c518ee 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -22,4 +22,5 @@ News details News details (Bottom Sheet) Addresses info + Story book From 7d009163a5a7eb11669fb6b7c9f1a1c86f6ee22b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 24 Feb 2026 18:39:53 +0200 Subject: [PATCH 42/97] Updated on 2026-08-14 --- .../tap/domain/tokens/DefaultTokensFeatureToggles.kt | 8 ++++++-- .../src/main/assets/configs/feature_toggles_config.json | 4 ++++ .../com/tangem/domain/tokens/TokensFeatureToggles.kt | 4 +++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt index 444b1f4032..4e27097a9d 100644 --- a/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt +++ b/app/src/main/java/com/tangem/tap/domain/tokens/DefaultTokensFeatureToggles.kt @@ -4,5 +4,9 @@ import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.domain.tokens.TokensFeatureToggles internal class DefaultTokensFeatureToggles( - @Suppress("UnusedPrivateMember") private val featureTogglesManager: FeatureTogglesManager, -) : TokensFeatureToggles \ No newline at end of file + private val featureTogglesManager: FeatureTogglesManager, +) : TokensFeatureToggles { + + override val isMultiAddressUtxoEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled("MULTI_ADDRESS_UTXO_ENABLED") +} \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 97d0aecaab..21f4ed8abd 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -63,5 +63,9 @@ { "name": "TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED", "version": "undefined" + }, + { + "name": "MULTI_ADDRESS_UTXO_ENABLED", + "version": "undefined" } ] diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt index a2eeb1e0a6..c281c5edd4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/TokensFeatureToggles.kt @@ -5,4 +5,6 @@ package com.tangem.domain.tokens * [REDACTED_AUTHOR] */ -interface TokensFeatureToggles \ No newline at end of file +interface TokensFeatureToggles { + val isMultiAddressUtxoEnabled: Boolean +} \ No newline at end of file From 4f23a66742f4082466d06b39f5b152f5b08194ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 08:08:42 +0100 Subject: [PATCH 43/97] Updated on 2026-08-14 --- .../ui/ds/opportunities/OpportunitiesBG.kt | 4 +- .../drawable/ic_wrapped_circle_star_16.xml | 9 + .../ShortArticleToArticleConfigUMConverter.kt | 2 +- .../statemanager/NewsListBatchFlowManager.kt | 2 +- .../list/statemanager/NewsListStateManager.kt | 2 +- .../ui/feed/components/FeedListLoading.kt | 4 +- .../feed/ui/feed/components/NewsBlock.kt | 104 +++-- .../feed/ui/feed/components/NewsSlider.kt | 15 + .../feed/ui/feed/components/NewsSliderV1.kt | 67 +++ .../feed/ui/feed/components/NewsSliderV2.kt | 112 +++++ .../feed/components/articles/ArticleCard.kt | 32 ++ .../feed/components/articles/ArticleCardV1.kt | 6 +- .../feed/components/articles/ArticleCardV2.kt | 382 ++++++++++++++++++ .../components/articles}/ArticleConfigUM.kt | 4 +- .../components/articles}/ArticleHeader.kt | 2 +- .../feed/components/articles}/ArticleInfo.kt | 2 +- .../articles}/ArticleLoadingCard.kt | 2 +- .../articles/ShowMoreArticlesCard.kt | 15 + .../feed/ui/feed/components/articles}/Tags.kt | 2 +- .../preview/FeedListPreviewDataProvider.kt | 2 +- .../features/feed/ui/feed/state/FeedListUM.kt | 2 +- .../feed/ui/feed/state/NewsSliderConfig.kt | 20 + .../components/TokenMarketDetailsBody.kt | 60 +-- .../detailed/state/MarketsTokenDetailsUM.kt | 2 +- .../ui/news/details/NewsDetailsContent.kt | 2 +- .../feed/ui/news/list/NewsListContent.kt | 2 +- .../list/components/NewsListLazyColumn.kt | 6 +- .../feed/ui/news/list/state/NewsListUM.kt | 2 +- .../presentation/wallet/ui/WalletScreen2.kt | 2 +- 29 files changed, 740 insertions(+), 128 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt rename common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt => features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt (97%) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt rename {common/ui/src/main/java/com/tangem/common/ui/news => features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles}/ArticleConfigUM.kt (76%) rename {common/ui/src/main/java/com/tangem/common/ui/news => features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles}/ArticleHeader.kt (96%) rename {common/ui/src/main/java/com/tangem/common/ui/news => features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles}/ArticleInfo.kt (97%) rename {common/ui/src/main/java/com/tangem/common/ui/news => features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles}/ArticleLoadingCard.kt (98%) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt rename {common/ui/src/main/java/com/tangem/common/ui/news => features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles}/Tags.kt (98%) create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt index f0059fff8b..368874114b 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt @@ -217,8 +217,8 @@ private const val SCALE_FACTOR = 1.5f private const val INNER_SHADOW_COLOR_START = 0x00000000 private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF -private const val BORDER_COLOR = 0xF0F0F0 -private const val OVERLAY_DARK = 0x141414 +private const val BORDER_COLOR = 0xFFF0F0F0 +private const val OVERLAY_DARK = 0xFF141414 // region Previews diff --git a/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml b/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml new file mode 100644 index 0000000000..69f15ed1a6 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_wrapped_circle_star_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt index 245f6b84c7..6a6b8580cc 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/ShortArticleToArticleConfigUMConverter.kt @@ -1,6 +1,6 @@ package com.tangem.features.feed.model.converter -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt index 6b9886c03f..eb6341a0c7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListBatchFlowManager.kt @@ -1,6 +1,6 @@ package com.tangem.features.feed.model.news.list.statemanager -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.domain.models.news.ShortArticle import com.tangem.domain.news.model.NewsListBatchingContext import com.tangem.domain.news.model.NewsListConfig diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt index e16a20e9f2..9c244106fa 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/news/list/statemanager/NewsListStateManager.kt @@ -1,6 +1,6 @@ package com.tangem.features.feed.model.news.list.statemanager -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.features.feed.model.news.list.analytics.NewsListAnalyticsEvent diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt index 1c2798cbf8..73294fe9f8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/FeedListLoading.kt @@ -9,8 +9,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.common.ui.markets.MarketsListItemPlaceholder -import com.tangem.common.ui.news.DefaultLoadingArticle -import com.tangem.common.ui.news.TrendingLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.TrendingLoadingArticle import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt index ff2921118b..4601283941 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsBlock.kt @@ -2,27 +2,26 @@ package com.tangem.features.feed.ui.feed.components import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleCard -import com.tangem.common.ui.news.ArticleConfigUM -import com.tangem.common.ui.news.ShowMoreArticlesCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW @@ -30,16 +29,18 @@ import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.feed.ui.feed.state.FeedListCallbacks -import com.tangem.features.feed.ui.feed.state.NewsUM -import com.tangem.features.feed.ui.feed.state.NewsUMState +import com.tangem.features.feed.ui.feed.state.* -private const val FOURTH_ITEM_INDEX = 3 +internal const val FOURTH_ITEM_INDEX = 3 private const val GRADIENT_START = 0f private const val GRADIENT_END = 0.5f -private val LinearGradientFirstPart = Color(0xFF635EEC) -private val LinearGradientSecondPart = Color(0xFFE05AED) +private const val LINEAR_GRADIENT_FIRST_PART_V2 = 0xFFA3A0FF +private const val LINEAR_GRADIENT_SECOND_PART_V2 = 0xFFF79DFF + +private const val LINEAR_GRADIENT_FIRST_PART_V1 = 0xFF635EEC +private const val LINEAR_GRADIENT_SECOND_PART_V1 = 0xFFE05AED @Composable internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { @@ -63,7 +64,23 @@ internal fun NewsBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trend @Suppress("LongMethod") @Composable private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, trendingArticle: ArticleConfigUM?) { - val listState = rememberLazyListState() + val isRedesignEnabled = LocalRedesignEnabled.current + val gradientStart = remember(isRedesignEnabled) { + if (isRedesignEnabled) { + Color(LINEAR_GRADIENT_FIRST_PART_V2) + } else { + Color(LINEAR_GRADIENT_FIRST_PART_V1) + } + } + + val gradientEnd = remember(isRedesignEnabled) { + if (isRedesignEnabled) { + Color(LINEAR_GRADIENT_SECOND_PART_V2) + } else { + Color(LINEAR_GRADIENT_SECOND_PART_V1) + } + } + Column { Header( title = { @@ -88,8 +105,8 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, withStyle( SpanStyle().copy( brush = Brush.linearGradient( - GRADIENT_START to LinearGradientFirstPart, - GRADIENT_END to LinearGradientSecondPart, + GRADIENT_START to gradientStart, + GRADIENT_END to gradientEnd, ), ), ) { @@ -122,48 +139,19 @@ private fun NewsContentBlock(feedListCallbacks: FeedListCallbacks, news: NewsUM, } } - LazyRow( - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - state = listState, - ) { - itemsIndexed( - items = news.content, - key = { index, _ -> index }, - contentType = { _, _ -> "article" }, - ) { index, article -> - val articleModifier = if (index == FOURTH_ITEM_INDEX) { - Modifier.onFirstVisible( - minFractionVisible = 0.5f, - callback = feedListCallbacks.onSliderScroll, - ) - } else { - Modifier - } - ArticleCard( - articleConfigUM = article, - onArticleClick = { feedListCallbacks.onArticleClick(article.id) }, - modifier = articleModifier - .heightIn(min = 164.dp) - .width(216.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) - } + NewsSlider( + NewsSliderConfig( + callbacks = NewsSliderCallbacks( + onOpenAllNews = { feedListCallbacks.onOpenAllNews(true) }, + onSliderScroll = feedListCallbacks.onSliderScroll, + onSliderEndReached = feedListCallbacks.onSliderEndReached, + onArticleClick = feedListCallbacks.onArticleClick, + ), + content = news.content, + shouldShowSeeAllNewsItem = true, + ), + ) - item(contentType = "show_more") { - ShowMoreArticlesCard( - modifier = Modifier - .width(216.dp) - .heightIn(min = 164.dp) - .onFirstVisible( - minFractionVisible = 0.5f, - callback = feedListCallbacks.onSliderEndReached, - ), - onClick = { feedListCallbacks.onOpenAllNews(true) }, - ) - } - } SpacerH(32.dp) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt new file mode 100644 index 0000000000..74d1b42fd9 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig + +@Composable +internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) { + val isRedesignEnabled = LocalRedesignEnabled.current + if (isRedesignEnabled) { + NewsSliderV2(newsSliderConfig) + } else { + NewsSliderV1(newsSliderConfig) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt new file mode 100644 index 0000000000..e035dcc7a4 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt @@ -0,0 +1,67 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.unit.dp +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig + +@Composable +internal fun NewsSliderV1(newsSliderConfig: NewsSliderConfig) { + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + state = rememberLazyListState(), + ) { + itemsIndexed( + items = newsSliderConfig.content, + key = { index, _ -> index }, + contentType = { _, _ -> "article" }, + ) { index, article -> + val articleModifier = if (index == FOURTH_ITEM_INDEX) { + Modifier.onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderScroll, + ) + } else { + Modifier + } + ArticleCard( + articleConfigUM = article, + onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) }, + modifier = articleModifier + .heightIn(min = 164.dp) + .width(216.dp), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) + } + + if (newsSliderConfig.shouldShowSeeAllNewsItem) { + item(contentType = "show_more") { + ShowMoreArticlesCard( + modifier = Modifier + .width(216.dp) + .heightIn(min = 164.dp) + .onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderEndReached, + ), + onClick = newsSliderConfig.callbacks.onOpenAllNews, + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt new file mode 100644 index 0000000000..f27b0a8b1c --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt @@ -0,0 +1,112 @@ +package com.tangem.features.feed.ui.feed.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.dp +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard +import com.tangem.core.ui.extensions.conditional +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig + +private val dividerSpacerWidth = 20.dp + +@Suppress("MagicNumber", "LongMethod") +@Composable +internal fun NewsSliderV2(newsSliderConfig: NewsSliderConfig) { + val density = LocalDensity.current + val dividerWidthPx = with(density) { 1.dp.roundToPx() } + val spacerWidthPx = with(density) { dividerSpacerWidth.roundToPx() } + + LazyRow( + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp), + state = rememberLazyListState(), + ) { + itemsIndexed( + items = newsSliderConfig.content, + key = { index, _ -> index }, + contentType = { _, _ -> "article" }, + ) { index, article -> + val articleModifier = Modifier.conditional( + condition = index == FOURTH_ITEM_INDEX, + modifier = { + onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderScroll, + ) + }, + ) + + val shouldShowDivider = newsSliderConfig.shouldShowSeeAllNewsItem || + index < newsSliderConfig.content.size - 1 + + // have to use layout cause LazyRow has not fixed height and divider can not be measured + Layout( + modifier = Modifier, + content = { + ArticleCard( + articleConfigUM = article, + onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) }, + modifier = articleModifier + .fillMaxHeight() + .width(220.dp), + ) + Spacer(modifier = Modifier.width(dividerSpacerWidth)) + Box( + modifier = Modifier + .width(1.dp) + .background(TangemTheme.colors2.border.neutral.secondary), + ) + Spacer(modifier = Modifier.width(dividerSpacerWidth)) + }, + ) { measurables, constraints -> + val cardPlaceable = measurables[0].measure(constraints) + val height = cardPlaceable.height + + if (shouldShowDivider) { + val leftSpacer = measurables[1].measure(Constraints.fixed(spacerWidthPx, height)) + val divider = measurables[2].measure(Constraints.fixed(dividerWidthPx, height)) + val rightSpacer = measurables[3].measure(Constraints.fixed(spacerWidthPx, height)) + val totalWidth = cardPlaceable.width + leftSpacer.width + divider.width + rightSpacer.width + + layout(totalWidth, height) { + cardPlaceable.place(0, 0) + leftSpacer.place(cardPlaceable.width, 0) + divider.place(cardPlaceable.width + leftSpacer.width, 0) + rightSpacer.place(cardPlaceable.width + leftSpacer.width + divider.width, 0) + } + } else { + layout(cardPlaceable.width, height) { + cardPlaceable.place(0, 0) + } + } + } + } + + if (newsSliderConfig.shouldShowSeeAllNewsItem) { + item(contentType = "show_more") { + ShowMoreArticlesCard( + modifier = Modifier + .fillMaxHeight() + .width(216.dp) + .onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderEndReached, + ), + onClick = newsSliderConfig.callbacks.onOpenAllNews, + ) + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt new file mode 100644 index 0000000000..53e0457e5a --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt @@ -0,0 +1,32 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import androidx.compose.material3.CardColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.LocalRedesignEnabled + +@Composable +fun ArticleCard( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, + colors: CardColors = TangemBlockCardColors, +) { + val isRedesignEnabled = LocalRedesignEnabled.current + + if (isRedesignEnabled) { + ArticleCardV2( + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + modifier = modifier, + ) + } else { + ArticleCardV1( + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + modifier = modifier, + colors = colors, + ) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt similarity index 97% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt index a6a7a2a078..97ede1fbdf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV1.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import android.content.res.Configuration import androidx.compose.foundation.* @@ -37,7 +37,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet @Composable -fun ArticleCard( +internal fun ArticleCardV1( articleConfigUM: ArticleConfigUM, onArticleClick: () -> Unit, modifier: Modifier = Modifier, @@ -126,7 +126,7 @@ private fun TrendingArticle( } @Composable -fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) { +internal fun ShowMoreArticlesCardV1(modifier: Modifier = Modifier, onClick: () -> Unit) { BlockCard( modifier = modifier, onClick = onClick, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt new file mode 100644 index 0000000000..a47843225f --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt @@ -0,0 +1,382 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import android.content.res.Configuration +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_NO +import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toImmutableSet + +@Composable +internal fun ArticleCardV2( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + if (articleConfigUM.isTrending) { + TrendingArticle( + modifier = modifier, + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + ) + } else { + DefaultArticle( + modifier = modifier, + articleConfigUM = articleConfigUM, + onArticleClick = onArticleClick, + ) + } +} + +@Composable +private fun TrendingArticle( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + TrendingArticleBackground( + modifier = modifier, + onClick = onArticleClick, + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.Start, + ) { + DayAndRatingInfo(rating = stringReference("${articleConfigUM.score}")) + + SpacerH(8.dp) + + Text( + text = articleConfigUM.title, + color = if (articleConfigUM.isViewed) { + TangemTheme.colors2.text.neutral.tertiary + } else { + TangemTheme.colors2.text.neutral.primary + }, + style = TangemTheme.typography2.headingSemibold20, + textAlign = TextAlign.Start, + ) + + SpacerH(18.dp) + + Text( + text = articleConfigUM.createdAt.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + SpacerH(18.dp) + + Tags(tags = articleConfigUM.tags.toImmutableList()) + } + } +} + +@Composable +internal fun ShowMoreArticlesCardV2(modifier: Modifier = Modifier, onClick: () -> Unit) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxSize() + .clip(RoundedCornerShape(20.dp)) + .background(color = TangemTheme.colors2.surface.level3) + .clickable(onClick = onClick) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.primary, + shape = RoundedCornerShape(20.dp), + ) + .padding(vertical = 41.dp, horizontal = 16.dp), + ) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48), + contentDescription = stringResourceSafe(R.string.common_show_more), + ) + + SpacerH(10.dp) + + Text( + text = stringResourceSafe(R.string.news_all_news), + style = TangemTheme.typography2.bodyRegular16, + color = TangemTheme.colors2.text.neutral.primary, + ) + + Text( + text = stringResourceSafe(R.string.news_stay_in_the_loop), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + ) + } +} + +@Composable +private fun DefaultArticle( + articleConfigUM: ArticleConfigUM, + onArticleClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background(TangemTheme.colors2.surface.level2) + .clickable { onArticleClick() } + .padding(vertical = 16.dp, horizontal = 10.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + RatingInfo(rating = stringReference("${articleConfigUM.score}")) + } + + SpacerH(8.dp) + + Text( + text = articleConfigUM.title, + color = if (articleConfigUM.isViewed) { + TangemTheme.colors2.text.neutral.tertiary + } else { + TangemTheme.colors2.text.neutral.primary + }, + style = TangemTheme.typography2.bodyRegular16, + minLines = 3, + maxLines = 3, + overflow = TextOverflow.Ellipsis, + ) + + Text( + modifier = Modifier.padding(vertical = 20.dp), + text = articleConfigUM.createdAt.resolveReference(), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.secondary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + + Tags(tags = articleConfigUM.tags.toImmutableList()) + } +} + +@Suppress("MagicNumber") +@Composable +private fun TrendingArticleBackground( + onClick: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable BoxScope.() -> Unit, +) { + val isDarkTheme = LocalIsInDarkTheme.current + + val bgColor = remember(isDarkTheme) { + if (isDarkTheme) { + Color(TRENDING_NIGHT_BG) + } else { + Color(TRENDING_LIGHT_BG) + } + } + + Box( + modifier = modifier + .clip(RoundedCornerShape(20.dp)) + .drawBehind { + drawRect(bgColor) + + val w = size.width + val h = size.height + val radiusScale = (w + h) / 2f + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFF7C16F1).copy(alpha = .8f), + Color.Transparent, + ), + center = Offset(w / 2f, 2.4f * h), + radius = radiusScale * 1.57f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFF3360FF).copy(alpha = .7f), + Color.Transparent, + ), + center = Offset(w / 2f, 2.95f * h), + radius = radiusScale * 1.9f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFFFF9408).copy(alpha = .45f), + Color.Transparent, + ), + center = Offset(-0.38f * w, 2.1f * h), + radius = radiusScale * 1.41f, + ), + ) + + drawRect( + brush = Brush.radialGradient( + colors = listOf( + Color(0xFFFC2424).copy(alpha = .5f), + Color.Transparent, + ), + center = Offset(1.188f * w, 2.37f * h), + radius = radiusScale * 1.41f, + ), + ) + } + .border(width = 1.dp, color = bgColor.copy(.1f)) + .clickable(onClick = onClick), + content = content, + ) +} + +@Composable +private fun DayAndRatingInfo(rating: TextReference, modifier: Modifier = Modifier) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + ) { + RatingInfo(rating) + + SpacerW(8.dp) + + Text( + text = stringResourceSafe(R.string.feed_trending_now), + style = TangemTheme.typography2.captionSemibold12, + color = TangemTheme.colors2.text.neutral.primary, + ) + } +} + +@Composable +private fun RatingInfo(rating: TextReference) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16), + tint = TangemTheme.colors2.fill.status.attention, + contentDescription = null, + ) + + SpacerW(2.dp) + + Text( + text = rating.resolveReference(), + color = TangemTheme.colors2.text.status.attention, + style = TangemTheme.typography2.captionSemibold12, + ) +} + +private const val TRENDING_NIGHT_BG = 0xFF1F1F1F +private const val TRENDING_LIGHT_BG = 0xFFFFFFFF + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TagsPreview() { + TangemThemePreviewRedesign { + Tags( + tags = persistentListOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Best rate")), + LabelUM(TextReference.Str("Breaking news")), + ), + ) + } +} + +@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_YES) +@Preview(widthDp = 360, uiMode = UI_MODE_NIGHT_NO) +@Composable +private fun ArticleCardsPreview() { + val tags = listOf( + LabelUM(TextReference.Str("Hype")), + LabelUM(TextReference.Str("BTC")), + LabelUM(TextReference.Str("Supply")), + LabelUM(TextReference.Str("Demand")), + LabelUM(TextReference.Str("Breaking news")), + ).toImmutableSet() + + val config = ArticleConfigUM( + id = 1, + title = "Bitcoin ETFs log 4th straight day of inflows (+\$550M)", + score = 9.5f, + createdAt = TextReference.Str("1h ago"), + isTrending = true, + tags = tags, + isViewed = false, + ) + + TangemThemePreviewRedesign { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .verticalScroll(rememberScrollState()), + ) { + ArticleCardV2( + articleConfigUM = config, + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isViewed = true), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isTrending = false), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ArticleCardV2( + articleConfigUM = config.copy(isTrending = false, isViewed = true), + onArticleClick = {}, + ) + + SpacerH(20.dp) + + ShowMoreArticlesCardV2(onClick = {}) + } + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt similarity index 76% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt index 46714a0ba8..fde217b1de 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleConfigUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleConfigUM.kt @@ -1,9 +1,11 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.label.entity.LabelUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableSet +@Immutable data class ArticleConfigUM( val id: Int, val title: String, diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt similarity index 96% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt index 15bac87bdc..fcb608cf65 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleHeader.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleHeader.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt similarity index 97% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt index a2834e2afb..656b128605 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleInfo.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleInfo.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt similarity index 98% rename from common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt index 300d41f104..1d5a646dda 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/ArticleLoadingCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt new file mode 100644 index 0000000000..fba589144d --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ShowMoreArticlesCard.kt @@ -0,0 +1,15 @@ +package com.tangem.features.feed.ui.feed.components.articles + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.tangem.core.ui.res.LocalRedesignEnabled + +@Composable +fun ShowMoreArticlesCard(modifier: Modifier = Modifier, onClick: () -> Unit) { + val isRedesignEnabled: Boolean = LocalRedesignEnabled.current + if (isRedesignEnabled) { + ShowMoreArticlesCardV2(modifier = modifier, onClick = onClick) + } else { + ShowMoreArticlesCardV1(modifier = modifier, onClick = onClick) + } +} \ No newline at end of file diff --git a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt similarity index 98% rename from common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt index a163c8ee15..8508ef89a9 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/news/Tags.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt @@ -1,4 +1,4 @@ -package com.tangem.common.ui.news +package com.tangem.features.feed.ui.feed.components.articles import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt index ceaf16a400..59635e4fe6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/preview/FeedListPreviewDataProvider.kt @@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.preview import com.tangem.common.ui.charts.state.MarketChartRawData import com.tangem.common.ui.markets.models.MarketsListItemUM -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.R import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index 8d8a014175..956cfde84f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.feed.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.markets.models.MarketsListItemUM -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.extensions.TextReference import com.tangem.features.feed.model.market.list.state.SortByTypeUM import com.tangem.features.feed.ui.earn.state.EarnListUM diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt new file mode 100644 index 0000000000..f61adb2d56 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/NewsSliderConfig.kt @@ -0,0 +1,20 @@ +package com.tangem.features.feed.ui.feed.state + +import androidx.compose.runtime.Immutable +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM +import kotlinx.collections.immutable.ImmutableList + +@Immutable +internal data class NewsSliderConfig( + val shouldShowSeeAllNewsItem: Boolean, + val content: ImmutableList, + val callbacks: NewsSliderCallbacks, +) + +@Immutable +internal data class NewsSliderCallbacks( + val onOpenAllNews: () -> Unit, + val onSliderScroll: () -> Unit, + val onSliderEndReached: () -> Unit, + val onArticleClick: (id: Int) -> Unit, +) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt index ff3047cac8..01d5c21f9d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt @@ -2,30 +2,24 @@ package com.tangem.features.feed.ui.market.detailed.components import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState 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 import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onFirstVisible import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleCard import com.tangem.core.ui.components.UnableToLoadData -import com.tangem.core.ui.components.block.TangemBlockCardColors import com.tangem.core.ui.components.items.DescriptionItem import com.tangem.core.ui.components.items.DescriptionPlaceholder import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.feed.components.NewsSlider +import com.tangem.features.feed.ui.feed.state.NewsSliderCallbacks +import com.tangem.features.feed.ui.feed.state.NewsSliderConfig import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews -private const val FOURTH_ITEM_INDEX = 3 - @Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA] internal fun LazyListScope.tokenMarketDetailsBody( state: MarketsTokenDetailsUM.Body, @@ -207,13 +201,6 @@ private fun LazyListScope.loadingInfoBlocks() { private fun LazyListScope.relatedNews(relatedNews: RelatedNews) { item("related-news") { - val listState = rememberLazyListState() - val articlesReadStatus = remember(relatedNews.articles) { - relatedNews.articles.map { it.isViewed } - } - LaunchedEffect(articlesReadStatus) { - listState.requestScrollToItem(0) - } Column( modifier = Modifier .fillMaxWidth() @@ -231,35 +218,18 @@ private fun LazyListScope.relatedNews(relatedNews: RelatedNews) { color = TangemTheme.colors.text.primary1, ) - LazyRow( - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - state = listState, - ) { - itemsIndexed( - items = relatedNews.articles, - key = { index, article -> article.id }, - ) { index, article -> - val articleModifier = if (index == FOURTH_ITEM_INDEX) { - Modifier.onFirstVisible( - minFractionVisible = 0.5f, - callback = relatedNews.onScroll, - ) - } else { - Modifier - } - - ArticleCard( - articleConfigUM = article, - onArticleClick = { relatedNews.onArticledClicked(article.id) }, - modifier = articleModifier - .heightIn(min = 164.dp) - .width(216.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) - } - } + NewsSlider( + NewsSliderConfig( + callbacks = NewsSliderCallbacks( + onOpenAllNews = {}, // not applicable here + onSliderScroll = relatedNews.onScroll, + onSliderEndReached = {}, // not applicable here + onArticleClick = relatedNews.onArticledClicked, + ), + content = relatedNews.articles, + shouldShowSeeAllNewsItem = false, + ), + ) } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt index d0c4775d82..5f21370028 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/state/MarketsTokenDetailsUM.kt @@ -2,7 +2,7 @@ package com.tangem.features.feed.ui.market.detailed.state import androidx.compose.runtime.Immutable import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.event.StateEvent diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt index 41d152a94b..fd67175055 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/NewsDetailsContent.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.unit.dp import coil.compose.SubcomposeAsyncImage import coil.request.CachePolicy import coil.request.ImageRequest -import com.tangem.common.ui.news.ArticleHeader +import com.tangem.features.feed.ui.feed.components.articles.ArticleHeader import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.components.buttons.common.TangemButtonSize diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt index 5dd1413007..61ae51398f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt @@ -12,7 +12,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.chip.Chip import com.tangem.core.ui.components.chip.entity.ChipUM diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt index 465c76d0f9..8d41f60ea3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt @@ -16,9 +16,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.tangem.common.ui.news.ArticleCard -import com.tangem.common.ui.news.ArticleConfigUM -import com.tangem.common.ui.news.DefaultLoadingArticle +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.DefaultLoadingArticle import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.UnableToLoadData import com.tangem.core.ui.components.block.TangemBlockCardColors diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt index 12fe9861f3..9d8c0e32b1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/state/NewsListUM.kt @@ -1,7 +1,7 @@ package com.tangem.features.feed.ui.news.list.state import androidx.compose.runtime.Immutable -import com.tangem.common.ui.news.ArticleConfigUM +import com.tangem.features.feed.ui.feed.components.articles.ArticleConfigUM import com.tangem.core.ui.components.chip.entity.ChipUM import kotlinx.collections.immutable.ImmutableList diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index cd10d61f4d..01a5c16c67 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -177,7 +177,7 @@ private inline fun BaseScaffoldWithMarkets( val coroutineScope = rememberCoroutineScope() val background = if (state.isNewMarketEnabled) { - TangemTheme.colors.background.tertiary + TangemTheme.colors2.surface.level2 } else { TangemTheme.colors.background.primary } From acb3ced60de90b599920e25127ebf8d979d74b37 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 10:58:09 +0300 Subject: [PATCH 44/97] Updated on 2026-08-14 --- .../core/ui/components/text/BladeAnimation.kt | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt index 2c05dbfe7d..1d0dbc825f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/text/BladeAnimation.kt @@ -45,18 +45,32 @@ fun TextStyle.applyBladeBrush(isEnabled: Boolean, textColor: Color): TextStyle { override fun createShader(size: Size): Shader { val center = Offset(size.width / 2f, size.height / 2f) val diagonal = sqrt(size.width * size.width + size.height * size.height) - val direction = Offset(x = 1f, y = 0.5f) - val halfDist = diagonal / 2f - val baseStart = center - direction * halfDist - val baseEnd = center + direction * halfDist - val shift = direction * offset * diagonal + // Subtle diagonal angle, similar to iOS shimmer + val direction = Offset(x = 1f, y = 0.3f) + // Half-width of the blob (80% of diagonal total — wide, soft sweep) + val bandHalf = diagonal * 0.40f + + // Sweep the highlight center from left-of-element to right-of-element. + // offset 0..1 maps to a full pass including off-screen padding on both sides. + val shift = direction * ((offset - 0.5f) * diagonal * 1.5f) + val highlightCenter = center + shift + + // Full color text with a wide, gradual low-alpha dip sweeping left → right return LinearGradientShader( - colors = listOf(textColor.copy(alpha = 0.2f), textColor), - from = baseStart + shift, - to = baseEnd + shift, - colorStops = listOf(0.0f, 0.15f), - tileMode = TileMode.Mirror, + colors = listOf( + textColor, + textColor.copy(alpha = 0.75f), + textColor.copy(alpha = 0.45f), + textColor.copy(alpha = 0.3f), + textColor.copy(alpha = 0.45f), + textColor.copy(alpha = 0.75f), + textColor, + ), + from = highlightCenter - direction * bandHalf, + to = highlightCenter + direction * bandHalf, + colorStops = listOf(0f, 0.15f, 0.35f, 0.5f, 0.65f, 0.85f, 1f), + tileMode = TileMode.Clamp, ) } } From 36c2a24e15a1bc57acb52aff04dc1e670acf9d1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 10:01:43 +0100 Subject: [PATCH 45/97] Updated on 2026-08-14 --- .../kotlin/com/tangem/features/feed/model/earn/EarnModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt index c9385e437b..8622c63f4b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/EarnModel.kt @@ -129,8 +129,8 @@ internal class EarnModel @Inject constructor( onClearFiltersClick = ::onClearFiltersClick, ) to error }.onEach { (bestOpportunitiesState, error) -> - stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) error?.let(::handleBestOpportunitiesErrorAnalytics) + stateController.update(UpdateBestOpportunitiesStateTransformer(bestOpportunitiesState)) }.launchIn(modelScope) } From d27a59ab75eac9e8d38519a504d5f63d071bd093 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 11:52:13 +0100 Subject: [PATCH 46/97] Updated on 2026-08-14 --- .../features/feed/model/earn/analytics/EarnAnalyticsEvent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt index c608bc31c7..3ff24b4467 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/analytics/EarnAnalyticsEvent.kt @@ -26,7 +26,7 @@ internal sealed class EarnAnalyticsEvent( event = "Best Opportunities Filter Network Applied", params = mapOf( "Network Filter Type" to filterType.value, - "NetworkId" to networkId.orEmpty(), + "Network Id" to networkId.orEmpty(), ), ) From a0ba5ab9d965e59cac480b3c0a8059f6a86f4896 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 13:59:19 +0400 Subject: [PATCH 47/97] Updated on 2026-08-14 --- .../wallet/impl/detekt-baseline-debug.xml | 61 ------- .../wallet/child/wallet/model/WalletModel.kt | 76 +++----- .../model/intents/WalletClickIntents.kt | 31 ++-- .../WalletCurrencyActionsClickIntents.kt | 29 ++- .../account/AccountDependencies.kt | 2 - .../domain/GetMultiWalletWarningsFactory.kt | 36 ++-- .../domain/GetSingleWalletWarningsFactory.kt | 36 ++-- .../domain/MultiWalletTokenListStore.kt | 62 ------- .../loaders/WalletContentLoaderFactory.kt | 41 ++--- .../wallet/loaders/WalletLoaderStorage.kt | 4 +- .../loaders/WalletScreenContentLoader.kt | 26 +-- .../implementors/MultiWalletContentLoader.kt | 101 +++-------- .../MultiWalletContentLoaderFactory.kt | 71 -------- .../MultiWalletContentLoaderV2.kt | 54 ------ .../implementors/SingleWalletContentLoader.kt | 97 +++------- .../SingleWalletContentLoaderFactory.kt | 57 ------ .../SingleWalletContentLoaderV2.kt | 96 ---------- .../SingleWalletWithTokenContentLoader.kt | 79 ++------- ...ngleWalletWithTokenContentLoaderFactory.kt | 57 ------ .../SingleWalletWithTokenContentLoaderV2.kt | 46 ----- .../subscribers/BasicTokenListSubscriber.kt | 165 ------------------ .../MultiWalletActionButtonsSubscriber.kt | 19 +- .../MultiWalletTokenListSubscriber.kt | 83 --------- .../MultiWalletWarningsSubscriber.kt | 20 ++- .../subscribers/PrimaryCurrencySubscriber.kt | 62 +++---- .../PrimaryCurrencySubscriberV2.kt | 84 --------- .../SingleWalletButtonsSubscriber.kt | 54 +++--- .../SingleWalletButtonsSubscriberV2.kt | 46 ----- .../SingleWalletExpressStatusesSubscriber.kt | 87 +++++---- ...SingleWalletExpressStatusesSubscriberV2.kt | 84 --------- .../SingleWalletNotificationsSubscriber.kt | 18 +- .../SingleWalletWithTokenListSubscriber.kt | 51 ------ .../wallet/subscribers/TxHistorySubscriber.kt | 58 +++--- .../subscribers/TxHistorySubscriberV2.kt | 114 ------------ .../subscribers/WalletNFTListSubscriber.kt | 58 ------ .../wallet/utils/DefaultUserWalletsFetcher.kt | 10 +- 36 files changed, 327 insertions(+), 1748 deletions(-) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index 4d05f69171..44d956c114 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -5,11 +5,8 @@ BooleanPropertyNaming:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher$@Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean BooleanPropertyNaming:DraggableItem.kt$DraggableItem$abstract val showShadow: Boolean BooleanPropertyNaming:DraggableItem.kt$DraggableItem.RoundingMode$abstract val showGap: Boolean - BooleanPropertyNaming:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$private var readyForRateAppNotification = false - BooleanPropertyNaming:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory$val userHasWalletOrWallet2 = userWallets.filterIsInstance<UserWallet.Cold>().any { val typesResolver = it.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } BooleanPropertyNaming:OrganizeTokensState.kt$OrganizeTokensState.ActionsConfig$val showApplyProgress: Boolean = false BooleanPropertyNaming:ScrollToWalletTransformer.kt$ScrollToWalletTransformer$private val withScrollAnimation: Boolean = true - BooleanPropertyNaming:SetWalletCardDropDownItemsTransformer.kt$SetWalletCardDropDownItemsTransformer$private val dropdownEnabled: Boolean BooleanPropertyNaming:TangemPayState.kt$TangemPayState.Progress$val showProgress: Boolean = false BooleanPropertyNaming:TokenActionButtonConfig.kt$TokenActionButtonConfig$val enabled: Boolean = true BooleanPropertyNaming:UpdateMultiWalletActionButtonBadgeTransformer.kt$UpdateMultiWalletActionButtonBadgeTransformer$private val showSwapBadge: Boolean @@ -18,127 +15,69 @@ BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Is click enabled */ abstract val enabled: Boolean BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton$/** Whether to dim content */ abstract val dimContent: Boolean BooleanPropertyNaming:WalletManageButton.kt$WalletManageButton.Swap$val showBadge: Boolean = false - BooleanPropertyNaming:WalletModel.kt$WalletModel$private var needToRefreshWallet = false - BooleanPropertyNaming:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase$private val useNewListRepository: Boolean - BooleanPropertyNaming:WalletScreen.kt$val portfolioContent = state is WalletState.MultiCurrency.Content && state.tokensListState is WalletTokensListState.ContentState.PortfolioContent BooleanPropertyNaming:WalletScreen.kt$val showMarketsHint by remember { derivedStateOf { // Show hint only when there are items in the list // and when there a no items to scroll listState.layoutInfo.totalItemsCount > 0 && !listState.canScrollBackward && !listState.canScrollForward || listState.canScrollBackward && !listState.canScrollForward } } BooleanPropertyNaming:WalletScreen.kt$var visible by remember { mutableStateOf(value = false) } BooleanPropertyNaming:WalletScreenState.kt$WalletScreenState$val showMarketsOnboarding: Boolean BooleanPropertyNaming:WalletWithFundsChecker.kt$WalletWithFundsChecker$val prevStatus = statusByWalletId.get(userWalletId) - IgnoredReturnValue:MultiWalletTokenListStore.kt$MultiWalletTokenListStore$remove(userWalletId) MaxChainedCallsOnSameLine:HasSingleWalletSignedHashesUseCase.kt$HasSingleWalletSignedHashesUseCase$userWallet.scanResponse.card.wallets.firstOrNull()?.totalSignedHashes - MultilineLambdaItParameter:BasicTokenListSubscriber.kt$BasicTokenListSubscriber${ it.getOrElse { e -> Timber.e("Failed to load app currency: $e") AppCurrency.Default } } MultilineLambdaItParameter:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler${ Timber.tag(LOG_TAG).e("Error on getting user wallet: $it") showAlert(Failed) } MultilineLambdaItParameter:DefaultUserWalletImageFetcher.kt$DefaultUserWalletImageFetcher${ it.fold( ifLeft = { emit(UserWalletItemUM.ImageState.Loading) }, ifRight = { wallet -> emitAll(walletImage(wallet, size)) }, ) } - MultilineLambdaItParameter:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory${ hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it) .conflate() .distinctUntilChanged() .firstOrNull() } - MultilineLambdaItParameter:GetSingleWalletWarningsFactory.kt$GetSingleWalletWarningsFactory${ val typesResolver = it.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } - MultilineLambdaItParameter:NetworkGroupToDraggableItemsConverterV2.kt$NetworkGroupToDraggableItemsConverterV2${ AccountCryptoCurrencyStatus( account = account, status = it, ) } MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ isBalanceHidden = it.isBalanceHidden stateHolder.updateHiddenState(isBalanceHidden) } - MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ stateHolder.updateStateAfterTokenListSorting(it) cachedTokenList = it } - MultilineLambdaItParameter:OrganizeTokensModel.kt$OrganizeTokensModel${ stateHolder.updateStateAfterTokenListSortingV2(it, isAccountsModeEnabled) cachedAccountStatusList = it } - MultilineLambdaItParameter:OrganizedTokenListConverter.kt$OrganizedTokenListConverter${ AccountCryptoCurrencyStatus( account = cryptoAccount, status = it, ) } - MultilineLambdaItParameter:PrimaryCurrencySubscriber.kt$PrimaryCurrencySubscriber${ // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( balance = it, tokensCount = null, ), ) } - MultilineLambdaItParameter:PrimaryCurrencySubscriber.kt$PrimaryCurrencySubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach } - MultilineLambdaItParameter:PrimaryCurrencySubscriberV2.kt$PrimaryCurrencySubscriberV2${ // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( balance = it, tokensCount = null, ), ) } - MultilineLambdaItParameter:ReviewManagerRequester.kt$ReviewManagerRequester${ handleOnCompleteRequestTask( reviewManager = reviewManager, activity = context.findActivity(), task = it, onDismissClick = onDismissClick, ) } MultilineLambdaItParameter:SetRefreshStateTransformer.kt$SetRefreshStateTransformer${ it.mapNotNull { button -> when (button) { is WalletManageButton.Buy -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null } } } - MultilineLambdaItParameter:SetVisaInfoTransformer.kt$SetVisaInfoTransformer${ if (it is RefreshTokenExpiredException) { return getRefreshTokenExpiredState(prevState) } return prevState.copy( buttons = createVisaButtonsDimmed(), walletCardState = getErrorWalletCardState(prevState.walletCardState), balancesAndLimitBlockState = BalancesAndLimitsBlockState.Error, ) } - MultilineLambdaItParameter:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber${ Timber.e("Unable to get primary currency status: $it") return@onEach } MultilineLambdaItParameter:TokenListAnalyticsSender.kt$TokenListAnalyticsSender${ val status = it.value if (status is CryptoCurrencyStatus.Loaded) { sendTokenBalancesForSpecificBlockchains(it, status) } } MultilineLambdaItParameter:TokenListStateConverter.kt$TokenListStateConverter${ if (isExtend) { clickIntents.onAccountCollapseClick(it) } else { clickIntents.onAccountExpandClick(it) } } - MultilineLambdaItParameter:TxHistorySubscriber.kt$TxHistorySubscriber${ SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, error = it, clickIntents = clickIntents, ) } - MultilineLambdaItParameter:TxHistorySubscriberV2.kt$TxHistorySubscriberV2${ SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, error = it, clickIntents = clickIntents, ) } - MultilineLambdaItParameter:UpdateMultiWalletActionsTransformer.kt$UpdateMultiWalletActionsTransformer${ when (it) { is WalletManageButton.Buy -> { it.copy( enabled = buyStatus.isContent(), dimContent = !buyStatus.isContent(), ) } is WalletManageButton.Sell -> { it.copy( enabled = sellStatus.isContent(), dimContent = !sellStatus.isContent(), ) } is WalletManageButton.Swap -> { it.copy( enabled = swapStatus.isContent(), dimContent = !swapStatus.isContent(), ) } else -> it } } MultilineLambdaItParameter:UseCaseExt.kt${ Timber.e("Impossible to get primary currency status $it") null } MultilineLambdaItParameter:UseCaseExt.kt${ Timber.e("Impossible to get selected wallet $it") null } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e("Unable to get balances and limits: $it") return@launch } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e(it, "Failed to get transaction details") return@launch } - MultilineLambdaItParameter:VisaWalletIntents.kt$VisaWalletIntentsImplementor${ Timber.e(it, "Failed to get visa currency") return@launch } - MultilineLambdaItParameter:VisaWalletSubscriber.kt$VisaWalletSubscriber${ Timber.e(it, "Failed to load VISA currency") setFailedTxHistoryState(it) return@flow } - MultilineLambdaItParameter:VisaWalletSubscriber.kt$VisaWalletSubscriber${ Timber.e(it, "Failed to load tx history for wallet ${userWallet.walletId}") throw it } MultilineLambdaItParameter:WalletCard.kt${ haptic.performHapticFeedback(HapticFeedbackType.LongPress) isMenuVisible = true pressOffset = DpOffset(x = it.x.toDp(), y = it.y.toDp()) } MultilineLambdaItParameter:WalletCard.kt${ val press = PressInteraction.Press(it) interactionSource.emit(press) tryAwaitRelease() interactionSource.emit(PressInteraction.Release(press)) } - MultilineLambdaItParameter:WalletCardClickIntents.kt$WalletCardClickIntentsImplementor${ Timber.e("Unable to delete user wallet: $it") return@launch } - MultilineLambdaItParameter:WalletClickIntents.kt$WalletClickIntents${ if (!it.isLocked) { launch { walletContentFetcher(userWalletId = it.walletId) } } walletScreenContentLoader.load( userWallet = it, clickIntents = this@WalletClickIntents, coroutineScope = modelScope, ) } MultilineLambdaItParameter:WalletContentClickIntents.kt$WalletContentClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) return@launch } - MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) clipboardManager.setText(text = it, isSensitive = true) } - MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) shareManager.shareText(text = it) } MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ onAddressTypeSelected( userWalletId = userWalletId, currency = currency, addressModel = it, ) } - MultilineLambdaItParameter:WalletLoaderStorage.kt$WalletLoaderStorage${ it.forEach(Job::cancel) loaders.remove(id) } - MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletScreenContentLoader.load( userWallet = it, clickIntents = clickIntents, coroutineScope = modelScope, isRefresh = true, ) } - MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletsUpdateActionResolver.resolve( wallets = it, currentState = stateHolder.value, ) } - MultilineLambdaItParameter:WalletNFTListSubscriber.kt$WalletNFTListSubscriber${ stateHolder.update( SetNFTCollectionsTransformer( userWalletId = userWallet.walletId, nftCollections = it, onItemClick = { clickIntents.onNFTClick(userWallet) }, ), ) } MultilineLambdaItParameter:WalletNameMigrationUseCase.kt$WalletNameMigrationUseCase${ val defaultName = it.name val suggestedWalletName = suggestedWalletName(defaultName, existingNames) if (defaultName != suggestedWalletName) { userWalletsListRepository.saveWithoutLock(it.copy(name = suggestedWalletName), canOverride = true) } Timber.tag("Migrated names").e(it.walletId.toString() + " " + suggestedWalletName) } MultilineLambdaItParameter:WalletScreen.kt${ PaddingValues( bottom = it.calculateBottomPadding() + marketHintAproxHeight + 52.dp, ) } MultilineLambdaItParameter:WalletScreen.kt${ WalletSnackbarHost( snackbarHostState = it, event = state.event, modifier = Modifier .padding(bottom = TangemTheme.dimens.spacing4) .navigationBarsPadding(), ) } - MultilineLambdaItParameter:WalletScreen.kt${ balancesAndLimitsBlock( modifier = itemModifier, state = it.balancesAndLimitBlockState, ) } - MultilineLambdaItParameter:WalletScreen.kt${ findPortfolioVisibleState( portfolio = it, expandedState = expandedState, collapsedState = collapsedState, ) } MultilineLambdaItParameter:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } MultilineLambdaItParameter:WalletScreen.kt${ nftCollections( modifier = itemModifier, state = it.nftState, ) } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ Timber.e( """ Unable to get user wallet |- ID: $userWalletId |- Exception: $it """.trimIndent(), ) null } MultilineLambdaItParameter:WalletWarningsClickIntents.kt$WalletWarningsClickIntentsImplementor${ router.openOnboardingScreen( scanResponse = it.scanResponse, continueBackup = true, ) } MultilineLambdaItParameter:WalletWithFundsChecker.kt$WalletWithFundsChecker${ val amount = it.value.amount ?: return@any false !amount.isZero() } - NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) - NamedArguments:GetMultiWalletWarningsFactory.kt$GetMultiWalletWarningsFactory$addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) NamedArguments:TangemSnapFlingBehavior.kt$HighVelocityApproachAnimation$animateDecay(offset, animationState, decayAnimationSpec, onAnimationStep) NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$animateSnap( closestOffset, closestOffset, animationState, snapAnimationSpec, ) { delta -> remainingScrollOffset -= delta onRemainingScrollOffsetUpdate(remainingScrollOffset) } NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$animateSnap( remainingOffset, remainingOffset, animationState.copy(value = 0f), snapAnimationSpec, ) { delta -> remainingScrollOffset -= delta onAnimationStep(remainingScrollOffset) } NamedArguments:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$approach( initialTargetOffset, initialVelocity, animation, snapLayoutInfoProvider, density, onAnimationStep, ) NamedArguments:TangemSnapFlingBehavior.kt$approachAnimation( this, initialTargetOffset, initialVelocity, onAnimationStep, ) - NamedArguments:WalletContent.kt$tokensListItems(state.tokensListState, modifier, isBalanceHidden, portfolioVisibleState) NamedArguments:WalletContent.kt$txHistoryItems(state.txHistoryState, txHistoryItems, isBalanceHidden, modifier) - NamedArguments:WalletScreenContentLoader.kt$WalletScreenContentLoader$loadInternal(userWallet, clickIntents, coroutineScope, isRefresh = true) - NamedArguments:WalletScreenContentLoader.kt$WalletScreenContentLoader$loadInternal(userWallet, clickIntents, coroutineScope, isRefresh) NestedScopeFunctions:WalletScreen.kt$let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } NestedScopeFunctions:WalletScreen.kt$let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } NestedScopeFunctions:WalletScreen.kt$let { marketPriceBlockState -> marketPriceBlock(state = marketPriceBlockState, modifier = itemModifier) } NoNameShadowing:DefaultUserWalletsFetcher.kt$DefaultUserWalletsFetcher${ it.isMultiCurrency } NoNameShadowing:MultiCurrencyAccountContent.kt$modifier - NoNameShadowing:TxHistorySubscriber.kt$TxHistorySubscriber${ it.cachedIn(coroutineScope) } - NoNameShadowing:TxHistorySubscriberV2.kt$TxHistorySubscriberV2${ it.cachedIn(coroutineScope) } NoNameShadowing:WalletComponent.kt$WalletComponent$dialog NoNameShadowing:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ it is TokensListItemUM.Token } NoNameShadowing:WalletNFTItem.kt$modifier NoNameShadowing:WalletScreen.kt${ it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinCurrency - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$$bitcoinStatus - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${cryptoCurrencies?.size} - NullableToStringCall:DefaultPromoDeeplinkHandler.kt$DefaultPromoDeeplinkHandler$${networkStatuses?.size} - NullableToStringCall:WalletStateController.kt$WalletStateController$${transformer::class.simpleName} - NullableToStringCall:WalletSubscriber.kt$WalletSubscriber$${this::class.simpleName} PropertyUsedBeforeDeclaration:BaseWalletClickIntents.kt$BaseWalletClickIntents$_modelScope PropertyUsedBeforeDeclaration:BaseWalletClickIntents.kt$BaseWalletClickIntents$_router PropertyUsedBeforeDeclaration:OrganizeTokensModel.kt$OrganizeTokensModel$uiState PropertyUsedBeforeDeclaration:WalletScreenPreviewData.kt$WalletScreenPreviewData$buyButton PropertyUsedBeforeDeclaration:WalletStateController.kt$WalletStateController$mutableUiState ReusedModifierInstance:DefaultWalletEntryComponent.kt$DefaultWalletEntryComponent$Content(modifier) - ReusedModifierInstance:VisaTxDetailsBottomSheet.kt$LazyColumn( modifier = modifier.background(TangemTheme.colors.background.secondary), contentPadding = PaddingValues( bottom = TangemTheme.dimens.spacing16, ), verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12), horizontalAlignment = Alignment.CenterHorizontally, ) { item { TransactionBlock(config.transaction) } items(config.requests) { item -> BlockchainRequestBlock(item) } item { DisputeButton(config.onDisputeClick) } } ReusedModifierInstance:WalletNFTItem.kt$Image( modifier = modifier .background(TangemTheme.colors.stroke.primary), painter = painterResource(R.drawable.ic_nft_preview_more_16), contentDescription = null, ) ReusedModifierInstance:WalletNFTItem.kt$SubcomposeAsyncImage( modifier = modifier, model = s.url, loading = { RectangleShimmer(radius = 0.dp) }, error = { Box( modifier = Modifier.background(TangemTheme.colors.field.primary), ) }, contentDescription = null, ) ReusedModifierInstance:WalletNFTItem.kt$take(modifiers.size) SuspendFunSwallowedCancellation:WalletModel.kt$WalletModel$runCatching - UnnecessaryLet:BalancesAndLimitsBottomSheetConverter.kt$BalancesAndLimitsBottomSheetConverter$let(::formatAmount) - UnnecessaryLet:MultiWalletContentLoader.kt$MultiWalletContentLoader$let(::add) - UnnecessaryLet:SingleWalletWithTokenContentLoader.kt$SingleWalletWithTokenContentLoader$let(::add) UnnecessaryLet:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$let { abs(it) * sign(initialVelocity) // ensure offset sign is correct } UnnecessaryLet:WalletClickIntents.kt$WalletClickIntents$let(::add) UnnecessaryLet:WalletScreen.kt$let { (state.tokensListState as? WalletTokensListState.ContentState)?.let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } } UnnecessaryLet:WalletScreen.kt$let { it.organizeTokensButtonConfig?.let { config -> organizeTokensButton( modifier = itemModifier, isEnabled = config.isEnabled, onClick = config.onClick, ) } } - UnnecessarySafeCall:SetVisaInfoTransformer.kt$SetVisaInfoTransformer$visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) } UseEmptyCounterpart:DefaultUserWalletImageFetcher.kt$DefaultUserWalletImageFetcher$mapOf<String, ArtworkUM>() - UseEmptyCounterpart:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$mapOf() UseEmptyCounterpart:ExpandedAccountsHolder.kt$ExpandedAccountsHolder$setOf() UseEmptyCounterpart:PortfolioOrganizeTokensAnalyticsEvent.kt$PortfolioOrganizeTokensAnalyticsEvent$mapOf() UseEmptyCounterpart:PromoActivationAnalytics.kt$PromoActivationAnalytics$mapOf() - UseEmptyCounterpart:SingleWalletExpressStatusesSubscriber.kt$SingleWalletExpressStatusesSubscriber$listOf() - UseEmptyCounterpart:SingleWalletExpressStatusesSubscriberV2.kt$SingleWalletExpressStatusesSubscriberV2$listOf() UseEmptyCounterpart:TokenListStateConverter.kt$TokenListStateConverter$listOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.Basic$mapOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.MainScreen$mapOf() UseEmptyCounterpart:WalletScreenAnalyticsEvent.kt$WalletScreenAnalyticsEvent.PushBannerPromo$mapOf() - UseOrEmpty:CryptoCurrenciesIdsResolver.kt$CryptoCurrenciesIdsResolver$accountStatusList?.accountStatuses ?.filter { it.getCryptoTokenList() != TokenList.Empty } ?.associate { accountStatus -> val currencies = accountStatus.flattenCurrencies() accountStatus.account as Account.CryptoPortfolio to draggableTokens .asSequence() .filter { it.accountId == accountStatus.account.accountId.value } .mapNotNull { sortedToken -> currencies.firstOrNull { it.currency.id.value == sortedToken.id }?.currency } .toList() } ?: emptyMap() UseSumOfInsteadOfFlatMapSize:TokenListStateConverter.kt$TokenListStateConverter$flatMap(NetworkGroup::currencies) VarCouldBeVal:TangemSnapFlingBehavior.kt$TangemSnapFlingBehavior$private var motionScaleDuration = DefaultScrollMotionDurationScale - VarCouldBeVal:WalletModel.kt$WalletModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler<Unit>() 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 7a6c21dbc3..507b19eb6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -11,7 +11,6 @@ import com.tangem.core.analytics.utils.TrackingContextProxy import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.apptheme.GetAppThemeModeUseCase @@ -31,7 +30,10 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.router.InnerWalletRouter import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.utils.SelectedWalletAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.* +import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory +import com.tangem.feature.wallet.presentation.wallet.domain.WalletContentFetcher +import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver +import com.tangem.feature.wallet.presentation.wallet.domain.WalletNameMigrationUseCase import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContentLoader import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig @@ -78,7 +80,6 @@ internal class WalletModel @Inject constructor( private val walletNameMigrationUseCase: WalletNameMigrationUseCase, private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, private val walletImageResolver: WalletImageResolver, - private val tokenListStore: MultiWalletTokenListStore, private val onrampStatusFactory: OnrampStatusFactory, private val analyticsEventsHandler: AnalyticsEventHandler, private val walletContentFetcher: WalletContentFetcher, @@ -90,7 +91,6 @@ internal class WalletModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val yieldSupplyApyUpdateUseCase: YieldSupplyApyUpdateUseCase, private val tangemPayOnboardingRepository: OnboardingRepository, - private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayMainScreenCustomerInfoUseCase: TangemPayMainScreenCustomerInfoUseCase, private val getAppThemeModeUseCase: GetAppThemeModeUseCase, private val trackingContextProxy: TrackingContextProxy, @@ -112,8 +112,8 @@ internal class WalletModel @Inject constructor( private val refreshWalletJobHolder = JobHolder() private val updateTangemPayJobHolder = JobHolder() - private var needToRefreshWallet = false - private var expressTxStatusTaskScheduler = SingleTaskScheduler() + private var shouldRefreshWallet = false + private val expressTxStatusTaskScheduler = SingleTaskScheduler() init { trackScreenOpened() @@ -182,7 +182,6 @@ internal class WalletModel @Inject constructor( override fun onDestroy() { super.onDestroy() - tokenListStore.clear() stateHolder.clear() walletScreenContentLoader.cancelAll() } @@ -261,9 +260,9 @@ internal class WalletModel @Inject constructor( getWalletsUseCase() .conflate() .distinctUntilChanged() - .map { + .map { userWallets -> walletsUpdateActionResolver.resolve( - wallets = it, + wallets = userWallets, currentState = stateHolder.value, ) } @@ -355,7 +354,7 @@ internal class WalletModel @Inject constructor( refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() - needToRefreshWallet && !isBackground -> { + shouldRefreshWallet && !isBackground -> { triggerRefreshWalletQuotes() } } @@ -426,12 +425,12 @@ internal class WalletModel @Inject constructor( private fun needToRefreshTimer() { modelScope.launch { delay(REFRESH_WALLET_BACKGROUND_TIMER_MILLIS) - needToRefreshWallet = true + shouldRefreshWallet = true }.saveIn(refreshWalletJobHolder) } private fun triggerRefreshWalletQuotes() { - needToRefreshWallet = false + shouldRefreshWallet = false val state = stateHolder.uiState.value val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return modelScope.launch { @@ -462,7 +461,6 @@ internal class WalletModel @Inject constructor( // refresh loader to use actual user wallet walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, isRefresh = true, coroutineScope = modelScope, ) @@ -510,10 +508,9 @@ internal class WalletModel @Inject constructor( } private fun reloadWarnings(action: WalletsUpdateActionResolver.Action.ReloadWallets) { - action.wallets.forEach { + action.wallets.forEach { userWallet -> walletScreenContentLoader.load( - userWallet = it, - clickIntents = clickIntents, + userWallet = userWallet, coroutineScope = modelScope, isRefresh = true, ) @@ -532,7 +529,6 @@ internal class WalletModel @Inject constructor( walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -559,11 +555,9 @@ internal class WalletModel @Inject constructor( private fun reinitializeNewWallet(action: WalletsUpdateActionResolver.Action.ReinitializeNewWallet) { walletScreenContentLoader.cancel(action.prevWalletId) - tokenListStore.remove(action.prevWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -582,11 +576,9 @@ internal class WalletModel @Inject constructor( private fun reinitializeWallets(action: WalletsUpdateActionResolver.Action.ReinitializeWallets) { action.wallets.forEach { userWallet -> walletScreenContentLoader.cancel(userWallet.walletId) - tokenListStore.remove(userWallet.walletId) walletScreenContentLoader.load( userWallet = userWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -603,39 +595,20 @@ internal class WalletModel @Inject constructor( } private fun addWallet(action: WalletsUpdateActionResolver.Action.AddWallet) { - if (accountsFeatureToggles.isFeatureEnabled) { - fetchWalletContent(userWallet = action.selectedWallet) + fetchWalletContent(userWallet = action.selectedWallet) - stateHolder.update( - AddWalletTransformer( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - walletImageResolver = walletImageResolver, - ), - ) - - walletScreenContentLoader.load( + stateHolder.update( + AddWalletTransformer( userWallet = action.selectedWallet, clickIntents = clickIntents, - coroutineScope = modelScope, - ) - } else { - walletScreenContentLoader.load( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - coroutineScope = modelScope, - ) + walletImageResolver = walletImageResolver, + ), + ) - fetchWalletContent(userWallet = action.selectedWallet) - - stateHolder.update( - AddWalletTransformer( - userWallet = action.selectedWallet, - clickIntents = clickIntents, - walletImageResolver = walletImageResolver, - ), - ) - } + walletScreenContentLoader.load( + userWallet = action.selectedWallet, + coroutineScope = modelScope, + ) scrollToWallet(prevIndex = action.prevWalletIndex, newIndex = action.selectedWalletIndex) { stateHolder.update { @@ -648,11 +621,9 @@ internal class WalletModel @Inject constructor( private fun deleteWallet(action: WalletsUpdateActionResolver.Action.DeleteWallet) { walletScreenContentLoader.cancel(action.deletedWalletId) - tokenListStore.remove(action.deletedWalletId) walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) @@ -696,7 +667,6 @@ internal class WalletModel @Inject constructor( walletScreenContentLoader.load( userWallet = action.selectedWallet, - clickIntents = clickIntents, coroutineScope = modelScope, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index 41161fa227..8f03d6ae84 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -30,7 +30,7 @@ internal class WalletClickIntents @Inject constructor( private val currencyActionsClickIntentsImplementor: WalletCurrencyActionsClickIntentsImplementor, private val contentClickIntentsImplementor: WalletContentClickIntentsImplementor, private val pushPermissionClickIntentsImplementor: WalletPushPermissionClickIntentsImplementor, - private val stateHolder: WalletStateController, + private val stateController: WalletStateController, private val walletScreenContentLoader: WalletScreenContentLoader, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val selectWalletUseCase: SelectWalletUseCase, @@ -62,7 +62,7 @@ internal class WalletClickIntents @Inject constructor( fun onWalletChange(index: Int, onlyState: Boolean) { if (onlyState) { - stateHolder.update { it.copy(selectedWalletIndex = index) } + stateController.update { it.copy(selectedWalletIndex = index) } return } @@ -70,27 +70,23 @@ internal class WalletClickIntents @Inject constructor( launch { neverToShowWalletsScrollPreview() } val maybeUserWallet = selectWalletUseCase( - userWalletId = stateHolder.value.wallets[index].walletCardState.id, + userWalletId = stateController.value.wallets[index].walletCardState.id, ) - stateHolder.update { it.copy(selectedWalletIndex = index) } + stateController.update { it.copy(selectedWalletIndex = index) } - maybeUserWallet.onRight { - if (!it.isLocked) { - launch { walletContentFetcher(userWalletId = it.walletId) } + maybeUserWallet.onRight { userWallet -> + if (!userWallet.isLocked) { + launch { walletContentFetcher(userWalletId = userWallet.walletId) } } - walletScreenContentLoader.load( - userWallet = it, - clickIntents = this@WalletClickIntents, - coroutineScope = modelScope, - ) + walletScreenContentLoader.load(userWallet = userWallet, coroutineScope = modelScope) } } } fun onRefreshSwipe(showRefreshState: Boolean) { - when (stateHolder.getSelectedWallet()) { + when (stateController.getSelectedWallet()) { is WalletState.MultiCurrency.Content -> { refreshMultiCurrencyContent(showRefreshState) } @@ -111,7 +107,7 @@ internal class WalletClickIntents @Inject constructor( private fun refreshMultiCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) @@ -126,7 +122,7 @@ internal class WalletClickIntents @Inject constructor( } .awaitAll() - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), ) } @@ -137,7 +133,7 @@ internal class WalletClickIntents @Inject constructor( private fun refreshSingleCurrencyContent(showRefreshState: Boolean) { val userWallet = getSelectedWalletSyncUseCase.unwrap() ?: return - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = showRefreshState), ) @@ -147,12 +143,11 @@ internal class WalletClickIntents @Inject constructor( onrampStatusFactory.updateOnrmapTransactionStatuses(userWallet) walletScreenContentLoader.load( userWallet = userWallet, - clickIntents = this@WalletClickIntents, isRefresh = true, coroutineScope = modelScope, ) - stateHolder.update( + stateController.update( SetRefreshStateTransformer(userWalletId = userWallet.walletId, isRefreshing = false), ) } 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 f42ebbb24e..25d595780c 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 @@ -22,7 +22,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.DialogMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -147,8 +146,6 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val uiMessageSender: UiMessageSender, @@ -289,23 +286,19 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.io) { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountId = getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = userWalletId, - currency = cryptoCurrencyStatus.currency, - ) - .map { it.account.accountId } - .getOrNull() + val accountId = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWalletId, + currency = cryptoCurrencyStatus.currency, + ) + .map { it.account.accountId } + .getOrNull() - if (accountId == null) { - Timber.e("Account ID is null, cannot hide currency ${cryptoCurrencyStatus.currency.id}") - return@launch - } - - manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency) - } else { - removeCurrencyUseCase(userWalletId, cryptoCurrencyStatus.currency) + if (accountId == null) { + Timber.e("Account ID is null, cannot hide currency ${cryptoCurrencyStatus.currency.id}") + return@launch } + + manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency) .fold( ifLeft = { walletEventSender.send( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt index 4119478f5a..7303bfbb6b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/account/AccountDependencies.kt @@ -1,7 +1,6 @@ package com.tangem.feature.wallet.presentation.account import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -9,7 +8,6 @@ import javax.inject.Inject @ModelScoped internal class AccountDependencies @Inject constructor( - val accountsFeatureToggles: AccountsFeatureToggles, val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, val expandedAccountsHolder: ExpandedAccountsHolder, val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, 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 a243ddef48..d08cbc8cb1 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 @@ -11,7 +11,6 @@ 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.hotwallet.CheckHotWalletUpgradeBannerUseCase import com.tangem.domain.hotwallet.GetAccessCodeSkippedUseCase @@ -52,7 +51,6 @@ import javax.inject.Inject @Suppress("LongParameterList", "LargeClass") @ModelScoped internal class GetMultiWalletWarningsFactory @Inject constructor( - private val tokenListStore: MultiWalletTokenListStore, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, @@ -71,30 +69,15 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { val cardTypesResolver = (userWallet as? UserWallet.Cold)?.scanResponse?.cardTypesResolver - val accountStatusList by lazy { + val accountStatusListFlow by lazy { val params = SingleAccountStatusListProducer.Params(userWallet.walletId) accountDependencies.singleAccountStatusListSupplier(params) .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( - // todo account just use it, after delete accountsFeatureToggles - // accountStatusListFlow, + accountStatusListFlow, isReadyToShowRateAppUseCase().distinctUntilChanged(), isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), @@ -110,7 +93,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) .distinctUntilChanged(), ) { 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 } @@ -149,9 +131,19 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( addYieldPromoNotification(clickIntents, shouldShowYieldPromo) - addInformationalNotifications(userWallet, cardTypesResolver, flattenCurrencies, clickIntents) + addInformationalNotifications( + userWallet = userWallet, + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + clickIntents = clickIntents, + ) - addWarningNotifications(cardTypesResolver, flattenCurrencies, isNeedToBackup, clickIntents) + addWarningNotifications( + cardTypesResolver = cardTypesResolver, + flattenCurrencies = flattenCurrencies, + isNeedToBackup = isNeedToBackup, + clickIntents = clickIntents, + ) addPushReminderNotification( clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index c7fe144bd9..30c506aa5e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.domain import arrow.core.Either import arrow.core.right import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.producer.SingleAccountStatusProducer import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier import com.tangem.domain.card.CardTypesResolver @@ -15,7 +14,6 @@ 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.settings.IsReadyToShowRateAppUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase @@ -30,8 +28,6 @@ import javax.inject.Inject @ModelScoped @Suppress("LongParameterList") internal class GetSingleWalletWarningsFactory @Inject constructor( - private val accountsFeatureToggles: AccountsFeatureToggles, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val singleAccountStatusSupplier: SingleAccountStatusSupplier, private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, @@ -40,7 +36,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( private val getWalletsUseCase: GetWalletsUseCase, ) { - private var readyForRateAppNotification = false + private var isReadyForRateAppNotification = false fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow> { if (userWallet !is UserWallet.Cold) { @@ -54,7 +50,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( flow3 = isNeedToBackupUseCase(userWallet.walletId).conflate(), flow4 = getWalletsUseCase().conflate(), ) { maybePrimaryCurrencyStatus, isReadyToShowRating, isNeedToBackup, userWallets -> - readyForRateAppNotification = true + isReadyForRateAppNotification = true buildList { addUsedOutdatedDataNotification(maybePrimaryCurrencyStatus) @@ -120,8 +116,8 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( cardTypesResolver: CardTypesResolver, clickIntents: WalletClickIntents, ) { - val userHasWalletOrWallet2 = userWallets.filterIsInstance().any { - val typesResolver = it.scanResponse.cardTypesResolver + val hasWalletOrWallet2 = userWallets.filterIsInstance().any { coldWallet -> + val typesResolver = coldWallet.scanResponse.cardTypesResolver typesResolver.isTangemWallet() || typesResolver.isWallet2() } @@ -129,7 +125,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( element = WalletNotification.NoteMigration( onClick = { clickIntents.onNoteMigrationButtonClick(NOTE_MIGRATION_URL) }, ), - condition = cardTypesResolver.isTangemNote() && !userHasWalletOrWallet2, + condition = cardTypesResolver.isTangemNote() && !hasWalletOrWallet2, ) addIf( @@ -191,8 +187,8 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( selectedWallet: UserWallet.Cold, cryptoCurrencyStatus: CryptoCurrencyStatus?, ): Boolean { - return cryptoCurrencyStatus?.currency?.network?.let { - hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = it) + return cryptoCurrencyStatus?.currency?.network?.let { network -> + hasSingleWalletSignedHashesUseCase(userWallet = selectedWallet, network = network) .conflate() .distinctUntilChanged() .firstOrNull() @@ -209,7 +205,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( onDislikeClick = clickIntents::onDislikeAppClick, onCloseClick = clickIntents::onCloseRateAppWarningClick, ), - condition = isReadyToShowRating && readyForRateAppNotification, + condition = isReadyToShowRating && isReadyForRateAppNotification, ) } @@ -219,7 +215,7 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( element is WalletNotification.Warning || element is WalletNotification.NoteMigration ) { - readyForRateAppNotification = false + isReadyForRateAppNotification = false } element @@ -229,16 +225,12 @@ internal class GetSingleWalletWarningsFactory @Inject constructor( private fun getPrimaryCurrencyStatusFlow( userWallet: UserWallet, ): Flow> { - return if (accountsFeatureToggles.isFeatureEnabled) { - getAccountStatusFlow(userWallet).mapNotNull { accountStatus -> - accountStatus.flattenCurrencies().firstOrNull() - } - .distinctUntilChanged() - .conflate() - .map { it.right() } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) + return getAccountStatusFlow(userWallet).mapNotNull { accountStatus -> + accountStatus.flattenCurrencies().firstOrNull() } + .distinctUntilChanged() + .conflate() + .map { it.right() } } private fun getAccountStatusFlow(userWallet: UserWallet): Flow { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt deleted file mode 100644 index 1aa129675c..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/MultiWalletTokenListStore.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.domain - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetTokenListUseCase -import com.tangem.domain.tokens.error.TokenListError -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.shareIn -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject - -@ModelScoped -internal class MultiWalletTokenListStore @Inject constructor( - private val getTokenListUseCase: GetTokenListUseCase, -) { - - private val flows: ConcurrentHashMap> by lazy { - ConcurrentHashMap() - } - - fun addIfNot(userWalletId: UserWalletId, coroutineScope: CoroutineScope) { - if (flows[userWalletId] != null) { - Timber.d("Flow with token list for $userWalletId already exists") - return - } - - coroutineScope.ensureActive() - - flows[userWalletId] = getTokenListUseCase - .launch(userWalletId) - .shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(), - replay = 1, - ) - - Timber.d("Flow with token list for $userWalletId created") - } - - fun getOrThrow(userWalletId: UserWalletId): LceFlow { - return requireNotNull(flows[userWalletId]) { - "Flow with token list for $userWalletId doesn't exist" - } - } - - fun remove(userWalletId: UserWalletId) { - flows.remove(userWalletId) - - Timber.d("Flow with token list for $userWalletId removed") - } - - fun clear() { - flows.clear() - - Timber.d("All flows with token list cleared") - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt index 4d9bf94d7e..2b4ca41b34 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt @@ -1,52 +1,33 @@ package com.tangem.feature.wallet.presentation.wallet.loaders import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.* +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.MultiWalletContentLoader +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletContentLoader +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletWithTokenContentLoader +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.WalletContentLoader import javax.inject.Inject @Suppress("LongParameterList") @ModelScoped internal class WalletContentLoaderFactory @Inject constructor( - private val multiWalletContentLoaderFactory: MultiWalletContentLoaderFactory, - private val multiWalletContentLoaderV2Factory: MultiWalletContentLoaderV2.Factory, - private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoaderFactory, - private val singleWalletWithTokenContentLoaderV2Factory: SingleWalletWithTokenContentLoaderV2.Factory, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val singleWalletContentLoaderFactory: SingleWalletContentLoaderFactory, - private val singleWalletContentLoaderV2Factory: SingleWalletContentLoaderV2.Factory, + private val multiWalletContentLoaderFactory: MultiWalletContentLoader.Factory, + private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoader.Factory, + private val singleWalletContentLoaderFactory: SingleWalletContentLoader.Factory, ) { - fun create( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - isRefresh: Boolean = false, - ): WalletContentLoader? { + fun create(userWallet: UserWallet, isRefresh: Boolean = false): WalletContentLoader? { return when { userWallet.isMultiCurrency -> { - if (accountsFeatureToggles.isFeatureEnabled) { - multiWalletContentLoaderV2Factory.create(userWallet) - } else { - multiWalletContentLoaderFactory.create(userWallet, clickIntents) - } + multiWalletContentLoaderFactory.create(userWallet) } userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> { - if (accountsFeatureToggles.isFeatureEnabled) { - singleWalletWithTokenContentLoaderV2Factory.create(userWallet) - } else { - singleWalletWithTokenContentLoaderFactory.create(userWallet, clickIntents) - } + singleWalletWithTokenContentLoaderFactory.create(userWallet) } userWallet is UserWallet.Cold && !userWallet.isMultiCurrency -> { - if (accountsFeatureToggles.isFeatureEnabled) { - singleWalletContentLoaderV2Factory.create(userWallet, isRefresh) - } else { - singleWalletContentLoaderFactory.create(userWallet, clickIntents, isRefresh) - } + singleWalletContentLoaderFactory.create(userWallet, isRefresh) } else -> null } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt index 1fa83b51e4..ee6cde3b58 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletLoaderStorage.kt @@ -18,8 +18,8 @@ internal class WalletLoaderStorage @Inject constructor() { } fun remove(id: UserWalletId) { - loaders[id]?.let { - it.forEach(Job::cancel) + loaders[id]?.let { jobs -> + jobs.forEach(Job::cancel) loaders.remove(id) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt index dd68293363..8965195057 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletScreenContentLoader.kt @@ -4,7 +4,6 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import kotlinx.coroutines.CloseableCoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.newSingleThreadContext @@ -14,9 +13,8 @@ import javax.inject.Inject /** * Base wallet screen content loader. Use it to load content by [UserWallet]. * - * @property factory factory that creates loader - * @property storage storage that save loader's jobs - * @property dispatchers coroutine dispatchers provider + * @property factory factory that creates loader + * @property storage storage that save loader's jobs * [REDACTED_AUTHOR] */ @@ -33,25 +31,19 @@ internal class WalletScreenContentLoader @Inject constructor( * Load content by [UserWallet] * * @param userWallet user wallet - * @param clickIntents click intents * @param isRefresh flag that determinate if content must load again * @param coroutineScope coroutine scope */ - fun load( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - isRefresh: Boolean = false, - coroutineScope: CoroutineScope, - ) { + fun load(userWallet: UserWallet, isRefresh: Boolean = false, coroutineScope: CoroutineScope) { if (userWallet.isLocked) return val id = userWallet.walletId if (!storage.contains(id)) { - loadInternal(userWallet, clickIntents, coroutineScope, isRefresh) + loadInternal(userWallet, coroutineScope, isRefresh) } else { if (isRefresh) { storage.remove(id) - loadInternal(userWallet, clickIntents, coroutineScope, isRefresh = true) + loadInternal(userWallet, coroutineScope, isRefresh = true) } else { Timber.d("$id content loading has already started") } @@ -70,15 +62,9 @@ internal class WalletScreenContentLoader @Inject constructor( singleBackgroundDispatcher.close() } - private fun loadInternal( - userWallet: UserWallet, - clickIntents: WalletClickIntents, - coroutineScope: CoroutineScope, - isRefresh: Boolean, - ) { + private fun loadInternal(userWallet: UserWallet, coroutineScope: CoroutineScope, isRefresh: Boolean) { val loader = factory.create( userWallet = userWallet, - clickIntents = clickIntents, isRefresh = isRefresh, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 498fdd3e0d..98fdd2bf6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -1,92 +1,33 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject @Suppress("LongParameterList") -@Deprecated("Use MultiWalletContentLoaderV2 instead") -@ModelScoped -internal class MultiWalletContentLoader( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val tokenListStore: MultiWalletTokenListStore, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, +internal class MultiWalletContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val accountListSubscriberFactory: AccountListSubscriber.Factory, + private val walletNFTListSubscriberFactory: WalletNFTListSubscriberV2.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, + private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, + private val multiWalletActionButtonsSubscriberFactory: MultiWalletActionButtonsSubscriber.Factory, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return buildList { - MultiWalletTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - applyTokenListSortingUseCase = applyTokenListSortingUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ).let(::add) + override fun create(): List = listOf( + accountListSubscriberFactory.create(userWallet), + walletNFTListSubscriberFactory.create(userWallet), + checkWalletWithFundsSubscriberFactory.create(userWallet), + multiWalletWarningsSubscriberFactory.create(userWallet), + multiWalletActionButtonsSubscriberFactory.create(userWallet), + tangemPayMainSubscriberFactory.create(userWallet), + ) - WalletNFTListSubscriber( - userWallet = userWallet, - getNFTCollectionsUseCase = getNFTCollectionsUseCase, - stateHolder = stateHolder, - walletsRepository = walletsRepository, - clickIntents = clickIntents, - currenciesRepository = currenciesRepository, - ).let(::add) - - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ).let(::add) - - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - - add(tangemPayMainSubscriberFactory.create(userWallet)) - } + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletContentLoader } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt deleted file mode 100644 index 1d26373427..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderFactory.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.TangemPayMainSubscriber -import javax.inject.Inject - -@Suppress("LongParameterList") -@Deprecated("Use MultiWalletContentLoaderV2.Factory instead") -@ModelScoped -internal class MultiWalletContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val walletsRepository: WalletsRepository, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val currenciesRepository: CurrenciesRepository, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, - private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, -) { - - fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): WalletContentLoader { - return MultiWalletContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - stateHolder = stateHolder, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - applyTokenListSortingUseCase = applyTokenListSortingUseCase, - getStoryContentUseCase = getStoryContentUseCase, - walletsRepository = walletsRepository, - getNFTCollectionsUseCase = getNFTCollectionsUseCase, - currenciesRepository = currenciesRepository, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - tangemPayMainSubscriberFactory = tangemPayMainSubscriberFactory, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt deleted file mode 100644 index e6d8b5a984..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoaderV2.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class MultiWalletContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet, - private val accountListSubscriberFactory: AccountListSubscriber.Factory, - private val walletNFTListSubscriberV2Factory: WalletNFTListSubscriberV2.Factory, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, - private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOf( - accountListSubscriberFactory.create(userWallet = userWallet), - walletNFTListSubscriberV2Factory.create(userWallet = userWallet), - checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet), - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ), - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - getStoryContentUseCase = getStoryContentUseCase, - ), - tangemPayMainSubscriberFactory.create(userWallet), - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet): MultiWalletContentLoaderV2 - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index 34c986c644..ae08b51df3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -1,83 +1,34 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject @Suppress("LongParameterList") -internal class SingleWalletContentLoader( - private val userWallet: UserWallet.Cold, - private val clickIntents: WalletClickIntents, - private val isRefresh: Boolean, - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, +internal class SingleWalletContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Cold, + @Assisted private val isRefresh: Boolean, + private val primaryCurrencySubscriberFactory: PrimaryCurrencySubscriber.Factory, + private val singleWalletButtonsSubscriberFactory: SingleWalletButtonsSubscriber.Factory, + private val singleWalletNotificationsSubscriberFactory: SingleWalletNotificationsSubscriber.Factory, + private val singleWalletExpressStatusesSubscriberFactory: SingleWalletExpressStatusesSubscriber.Factory, + private val txHistorySubscriberFactory: TxHistorySubscriber.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return listOf( - PrimaryCurrencySubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - ), - SingleWalletButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, - ), - SingleWalletNotificationsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - ), - SingleWalletExpressStatusesSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - ), - TxHistorySubscriber( - userWallet = userWallet, - isRefresh = isRefresh, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - ), - ) + override fun create(): List = listOf( + primaryCurrencySubscriberFactory.create(userWallet), + singleWalletButtonsSubscriberFactory.create(userWallet), + singleWalletNotificationsSubscriberFactory.create(userWallet), + singleWalletExpressStatusesSubscriberFactory.create(userWallet), + txHistorySubscriberFactory.create(userWallet, isRefresh), + checkWalletWithFundsSubscriberFactory.create(userWallet), + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoader } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt deleted file mode 100644 index 4340bf544b..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderFactory.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -@Deprecated("Use SingleWalletContentLoaderV2.Factory instead") -internal class SingleWalletContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, -) { - - fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents, isRefresh: Boolean): WalletContentLoader { - return SingleWalletContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - isRefresh = isRefresh, - stateHolder = stateHolder, - getSingleCryptoCurrencyStatusUseCase = getSingleCryptoCurrencyStatusUseCase, - getCryptoCurrencyActionsUseCase = getCryptoCurrencyActionsUseCase, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - setWalletWithFundsFoundUseCase = setWalletWithFundsFoundUseCase, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - analyticsEventHandler = analyticsEventHandler, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt deleted file mode 100644 index 6c4711cb5f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderV2.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.account.AccountDependencies -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.* -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class SingleWalletContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet.Cold, - @Assisted private val isRefresh: Boolean, - private val clickIntents: WalletClickIntents, - private val stateHolder: WalletStateController, - private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, - private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val analyticsEventHandler: AnalyticsEventHandler, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val accountDependencies: AccountDependencies, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val dispatchers: CoroutineDispatcherProvider, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOf( - PrimaryCurrencySubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - stateController = stateHolder, - analyticsEventHandler = analyticsEventHandler, - ), - SingleWalletButtonsSubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - stateController = stateHolder, - clickIntents = clickIntents, - getCryptoCurrencyActionsUseCaseV2 = getCryptoCurrencyActionsUseCaseV2, - ), - SingleWalletNotificationsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getSingleWalletWarningsFactory = getSingleWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - ), - SingleWalletExpressStatusesSubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - getOnrampTransactionsUseCase = getOnrampTransactionsUseCase, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - onrampRemoveTransactionUseCase = onrampRemoveTransactionUseCase, - stateController = stateHolder, - clickIntents = clickIntents, - analyticsEventHandler = analyticsEventHandler, - ), - TxHistorySubscriberV2( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - txHistoryItemsCountUseCase = txHistoryItemsCountUseCase, - txHistoryItemsUseCase = txHistoryItemsUseCase, - isRefresh = isRefresh, - stateController = stateHolder, - clickIntents = clickIntents, - ), - CheckWalletWithFundsSubscriber( - userWallet = userWallet, - singleAccountStatusListSupplier = accountDependencies.singleAccountStatusListSupplier, - walletWithFundsChecker = walletWithFundsChecker, - dispatchers = dispatchers, - ), - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoaderV2 - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index a43c2c130e..81720d5bd6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -1,70 +1,29 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletActionButtonsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.CheckWalletWithFundsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenListSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject -@Deprecated("Use SingleWalletWithTokenContentLoaderV2 instead") -@Suppress("LongParameterList") -internal class SingleWalletWithTokenContentLoader( - private val userWallet: UserWallet.Cold, - private val clickIntents: WalletClickIntents, - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, +internal class SingleWalletWithTokenContentLoader @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Cold, + private val singleWalletWithTokenSubscriberFactory: SingleWalletWithTokenSubscriber.Factory, + private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { - override fun create(): List { - return buildList { - SingleWalletWithTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ).let(::add) - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ).let(::add) - MultiWalletActionButtonsSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - getStoryContentUseCase = getStoryContentUseCase, - ).let(::add) - } + override fun create(): List = listOf( + singleWalletWithTokenSubscriberFactory.create(userWallet), + multiWalletWarningsSubscriberFactory.create(userWallet), + checkWalletWithFundsSubscriberFactory.create(userWallet), + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenContentLoader } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt deleted file mode 100644 index a0fcbbd771..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderFactory.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.promo.GetStoryContentUseCase -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import javax.inject.Inject - -// TODO: Refactor -@Suppress("LongParameterList") -@Deprecated("Use SingleWalletWithTokenContentLoaderV2.Factory instead") -@ModelScoped -internal class SingleWalletWithTokenContentLoaderFactory @Inject constructor( - private val stateHolder: WalletStateController, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, - private val tokenListStore: MultiWalletTokenListStore, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getStoryContentUseCase: GetStoryContentUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) { - - fun create(userWallet: UserWallet.Cold, clickIntents: WalletClickIntents): SingleWalletWithTokenContentLoader { - return SingleWalletWithTokenContentLoader( - userWallet = userWallet, - clickIntents = clickIntents, - stateHolder = stateHolder, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - tokenListStore = tokenListStore, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - getStoryContentUseCase = getStoryContentUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt deleted file mode 100644 index f11999ca24..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoaderV2.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.loaders.implementors - -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender -import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarningsFactory -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.subscribers.CheckWalletWithFundsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Suppress("LongParameterList") -internal class SingleWalletWithTokenContentLoaderV2 @AssistedInject constructor( - @Assisted private val userWallet: UserWallet.Cold, - private val singleWalletWithTokenSubscriberFactory: SingleWalletWithTokenSubscriber.Factory, - private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, - private val clickIntents: WalletClickIntents, - private val stateController: WalletStateController, - private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, - private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, - private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, -) : WalletContentLoader(id = userWallet.walletId) { - - override fun create(): List = listOf( - singleWalletWithTokenSubscriberFactory.create(userWallet), - MultiWalletWarningsSubscriber( - userWallet = userWallet, - stateHolder = stateController, - clickIntents = clickIntents, - getMultiWalletWarningsFactory = getMultiWalletWarningsFactory, - walletWarningsAnalyticsSender = walletWarningsAnalyticsSender, - walletWarningsSingleEventSender = walletWarningsSingleEventSender, - ), - checkWalletWithFundsSubscriberFactory.create(userWallet), - ) - - @AssistedFactory - interface Factory { - fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenContentLoaderV2 - } -} \ No newline at end of file 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 deleted file mode 100644 index 929f5e07ad..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ /dev/null @@ -1,165 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -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.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.staking.model.StakingAvailability -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenListTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.TokenConverterParams -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import java.math.BigDecimal - -@Deprecated("Use AccountListSubscriber instead") -@Suppress("LongParameterList") -internal abstract class BasicTokenListSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val tokenListAnalyticsSender: TokenListAnalyticsSender, - private val walletWithFundsChecker: WalletWithFundsChecker, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : WalletSubscriber() { - - private val sendAnalyticsJobHolder = JobHolder() - private val onTokenListReceivedJobHolder = JobHolder() - - protected abstract fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow - - protected abstract suspend fun onTokenListReceived(maybeTokenList: Lce) - - override fun create(coroutineScope: CoroutineScope): Flow<*> { - return combine( - flow = tokenListFlow(coroutineScope) - .onEach { maybeTokenList -> - coroutineScope.launch { - sendTokenListAnalytics( - flattenCurrencies = maybeTokenList.getOrNull()?.flattenCurrencies(), - totalFiatBalance = maybeTokenList.getOrNull()?.totalFiatBalance, - ) - }.saveIn(sendAnalyticsJobHolder) - } - .distinctUntilChanged() - .onEach { maybeTokenList -> - coroutineScope.launch { - onTokenListReceived(maybeTokenList) - }.saveIn(onTokenListReceivedJobHolder) - }, - flow2 = appCurrencyFlow(), - flow3 = yieldSupplyApyFlow(), - flow4 = yieldSupplyGetShouldShowMainPromoFlow(), - transform = { maybeTokenList, appCurrency, yieldSupplyApyMap, shouldShowMainPromo -> - val tokenList = maybeTokenList.getOrElse( - ifLoading = { maybeContent -> - val isRefreshing = stateHolder.getWalletState(userWallet.walletId) - ?.pullToRefreshConfig - ?.isRefreshing == true - - maybeContent - ?.takeIf { !isRefreshing } - ?: return@combine - }, - ifError = { e -> - Timber.e("Failed to load token list: $e") - stateHolder.update( - SetTokenListErrorTransformer( - selectedWallet = userWallet, - error = e, - appCurrency = appCurrency, - ), - ) - return@combine - }, - ) - - updateContent( - params = TokenConverterParams.Wallet(PortfolioId(userWallet.walletId), tokenList), - appCurrency = appCurrency, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityListUseCase.invokeSync( - userWalletId = userWallet.walletId, - cryptoCurrencyList = tokenList.flattenCurrencies().map(CryptoCurrencyStatus::currency), - ), - shouldShowMainPromo = shouldShowMainPromo, - ) - - walletWithFundsChecker.check(tokenList) - }, - ) - } - - private suspend fun sendTokenListAnalytics( - flattenCurrencies: List?, - totalFiatBalance: TotalFiatBalance?, - ) { - val displayedState = stateHolder.getWalletStateIfSelected(userWallet.walletId) - - tokenListAnalyticsSender.send( - displayedUiState = displayedState, - userWallet = userWallet, - flattenCurrencies = flattenCurrencies ?: return, - totalFiatBalance = totalFiatBalance ?: return, - ) - } - - private fun updateContent( - params: TokenConverterParams, - appCurrency: AppCurrency, - yieldSupplyApyMap: Map, - stakingAvailabilityMap: Map, - shouldShowMainPromo: Boolean, - ) { - stateHolder.update( - SetTokenListTransformer( - params = params, - userWallet = userWallet, - appCurrency = appCurrency, - clickIntents = clickIntents, - yieldSupplyApyMap = yieldSupplyApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - shouldShowMainPromo = shouldShowMainPromo, - isAccountsModeEnabled = false, - ), - ) - } - - private fun appCurrencyFlow(): Flow = getSelectedAppCurrencyUseCase() - .map { - it.getOrElse { e -> - Timber.e("Failed to load app currency: $e") - AppCurrency.Default - } - } - .distinctUntilChanged() - - private fun yieldSupplyApyFlow(): Flow> = yieldSupplyApyFlowUseCase() - .distinctUntilChanged() - - private fun yieldSupplyGetShouldShowMainPromoFlow(): Flow = yieldSupplyGetShouldShowMainPromoUseCase() - .distinctUntilChanged() -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt index 426cac1ace..8ef8a70601 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletActionButtonsSubscriber.kt @@ -1,28 +1,37 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.promo.GetStoryContentUseCase import com.tangem.domain.promo.models.StoryContentIds -import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.UpdateMultiWalletActionButtonBadgeTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -internal class MultiWalletActionButtonsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class MultiWalletActionButtonsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val getStoryContentUseCase: GetStoryContentUseCase, ) : WalletSubscriber() { + override fun create(coroutineScope: CoroutineScope): Flow<*> = getStoryContentUseCase( id = StoryContentIds.STORY_FIRST_TIME_SWAP.id, ).map { maybeSwapStories -> val isSwapStoriesNotNull = maybeSwapStories.getOrNull() != null - stateHolder.update( + stateController.update( UpdateMultiWalletActionButtonBadgeTransformer( userWalletId = userWallet.walletId, showSwapBadge = isSwapStoriesNotNull, ), ) } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletActionButtonsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt deleted file mode 100644 index c3159ba551..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.ApplyTokenListSortingUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import kotlinx.coroutines.CoroutineScope - -@Deprecated("Use AccountListSubscriber instead") -@Suppress("LongParameterList") -internal class MultiWalletTokenListSubscriber( - private val userWallet: UserWallet, - private val tokenListStore: MultiWalletTokenListStore, - private val applyTokenListSortingUseCase: ApplyTokenListSortingUseCase, - stateHolder: WalletStateController, - clickIntents: WalletClickIntents, - tokenListAnalyticsSender: TokenListAnalyticsSender, - walletWithFundsChecker: WalletWithFundsChecker, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : BasicTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, -) { - - override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { - tokenListStore.addIfNot(userWallet.walletId, coroutineScope) - - return tokenListStore.getOrThrow(userWallet.walletId) - } - - override suspend fun onTokenListReceived(maybeTokenList: Lce) { - updateSortingIfNeeded(maybeTokenList) - } - - private suspend fun updateSortingIfNeeded(maybeTokenList: Lce<*, TokenList>) { - val tokenList = getTokenList(maybeTokenList) ?: return - - applyTokenListSortingUseCase( - userWalletId = userWallet.walletId, - sortedTokensIds = getCurrenciesIds(tokenList), - isGroupedByNetwork = tokenList is TokenList.GroupedByNetwork, - isSortedByBalance = tokenList.sortedBy == TokensSortType.BALANCE, - ) - } - - private fun getTokenList(lce: Lce<*, TokenList>): TokenList? { - val tokenList = lce.getOrNull(isPartialContentAccepted = false) - ?: return null - - return tokenList.takeIf { - tokenList.totalFiatBalance is TotalFiatBalance.Loaded && - tokenList.sortedBy == TokensSortType.BALANCE - } - } - - private fun getCurrenciesIds(tokenList: TokenList): List { - return tokenList.flattenCurrencies().map { it.currency.id } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index 577da2f275..8cc069470a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -8,14 +8,17 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetMultiWalletWarnin import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* -internal class MultiWalletWarningsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class MultiWalletWarningsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, private val getMultiWalletWarningsFactory: GetMultiWalletWarningsFactory, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, @@ -27,14 +30,14 @@ internal class MultiWalletWarningsSubscriber( .conflate() .distinctUntilChanged() .onEach { warnings -> - val displayedState = stateHolder.getWalletState(userWallet.walletId) + val displayedState = stateController.getWalletState(userWallet.walletId) // Wait until the wallet appears in the list - stateHolder.uiState.first { + stateController.uiState.first { it.wallets.any { walletState -> walletState.walletCardState.id == userWallet.walletId } } - stateHolder.update( + stateController.update( SetWarningsTransformer( userWalletId = userWallet.walletId, warnings = warnings, @@ -49,4 +52,9 @@ internal class MultiWalletWarningsSubscriber( ) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): MultiWalletWarningsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt index 6f564c38a2..bd063d00a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -1,62 +1,47 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.common.extensions.isZero import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.settings.SetWalletWithFundsFoundUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* -import timber.log.Timber +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import java.math.BigDecimal -@Deprecated("Use PrimaryCurrencySubscriberV2 instead") -internal class PrimaryCurrencySubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val setWalletWithFundsFoundUseCase: SetWalletWithFundsFoundUseCase, +internal class PrimaryCurrencySubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val stateController: WalletStateController, private val analyticsEventHandler: AnalyticsEventHandler, -) : WalletSubscriber() { +) : BasicSingleWalletSubscriber() { - override fun create( - coroutineScope: CoroutineScope, - ): Flow, AppCurrency>> { + override fun create(coroutineScope: CoroutineScope): Flow<*> { return combine( - flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId) - .conflate() - .distinctUntilChanged(), - flow2 = getSelectedAppCurrencyUseCase() - .conflate() - .distinctUntilChanged() - .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }, - transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency }, + flow = getPrimaryCurrencyStatusFlow(), + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + transform = ::Pair, ) - .onEach { maybeCurrencyStatusAndAppCurrency -> - val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse { - Timber.e("Unable to get primary currency status: $it") - return@onEach - } - - updateContent(status, maybeCurrencyStatusAndAppCurrency.second) + .onEach { (status, appCurrency) -> + updateContent(status, appCurrency) sendAnalyticsEvent(status) - checkWalletWithFunds(status) } } private fun updateContent(status: CryptoCurrencyStatus, appCurrency: AppCurrency) { - stateHolder.update( + stateController.update( SetPrimaryCurrencyTransformer( status = status, userWallet = userWallet, @@ -81,11 +66,11 @@ internal class PrimaryCurrencySubscriber( -> null } - cardBalanceState?.let { + cardBalanceState?.let { balanceState -> // do not send tokens count for single currency wallet analyticsEventHandler.send( event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( - balance = it, + balance = balanceState, tokensCount = null, ), ) @@ -100,7 +85,8 @@ internal class PrimaryCurrencySubscriber( } } - private suspend fun checkWalletWithFunds(status: CryptoCurrencyStatus) { - if (status.value.amount?.isZero() == false) setWalletWithFundsFoundUseCase() + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): PrimaryCurrencySubscriber } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt deleted file mode 100644 index 3aa5c39bb4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriberV2.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.common.extensions.isZero -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetPrimaryCurrencyTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.onEach -import java.math.BigDecimal - -internal class PrimaryCurrencySubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val stateController: WalletStateController, - private val analyticsEventHandler: AnalyticsEventHandler, -) : BasicSingleWalletSubscriber() { - - override fun create(coroutineScope: CoroutineScope): Flow<*> { - return combine( - flow = getPrimaryCurrencyStatusFlow(), - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - transform = ::Pair, - ) - .onEach { (status, appCurrency) -> - updateContent(status, appCurrency) - sendAnalyticsEvent(status) - } - } - - private fun updateContent(status: CryptoCurrencyStatus, appCurrency: AppCurrency) { - stateController.update( - SetPrimaryCurrencyTransformer( - status = status, - userWallet = userWallet, - appCurrency = appCurrency, - ), - ) - } - - private fun sendAnalyticsEvent(status: CryptoCurrencyStatus) { - val fiatAmount = status.value.fiatAmount - val cardBalanceState = when (status.value) { - is CryptoCurrencyStatus.Loaded, - is CryptoCurrencyStatus.NoAccount, - is CryptoCurrencyStatus.NoAmount, - -> createCardBalanceState(fiatAmount) - is CryptoCurrencyStatus.NoQuote -> AnalyticsParam.CardBalanceState.NoRate - is CryptoCurrencyStatus.Unreachable, - -> AnalyticsParam.CardBalanceState.BlockchainError - is CryptoCurrencyStatus.MissedDerivation, - is CryptoCurrencyStatus.Loading, - is CryptoCurrencyStatus.Custom, - -> null - } - - cardBalanceState?.let { - // do not send tokens count for single currency wallet - analyticsEventHandler.send( - event = WalletScreenAnalyticsEvent.Basic.BalanceLoaded( - balance = it, - tokensCount = null, - ), - ) - } - } - - private fun createCardBalanceState(fiatAmount: BigDecimal?): AnalyticsParam.CardBalanceState? { - return when { - fiatAmount == null -> null - fiatAmount.isZero() -> AnalyticsParam.CardBalanceState.Empty - else -> AnalyticsParam.CardBalanceState.Full - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt index af71e2d23f..f477c4d790 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -1,44 +1,43 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetCryptoCurrencyActionsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.onEach -@Deprecated("Use SingleWalletButtonsSubscriberV2 instead") -internal class SingleWalletButtonsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class SingleWalletButtonsSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, -) : WalletSubscriber() { + private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, +) : BasicSingleWalletSubscriber() { + @OptIn(ExperimentalCoroutinesApi::class) override fun create(coroutineScope: CoroutineScope): Flow { - return channelFlow { - getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> - getCryptoCurrencyActionsUseCase(userWallet = userWallet, status = status) - ?.let { send(it) } + return getPrimaryCurrencyStatusFlow() + .flatMapLatest { + getCryptoCurrencyActionsUseCaseV2(accountId = accountId, currency = it.currency) } - } - .onEach { actions -> - updateContent( - tokenActionsState = actions, - portfolioId = PortfolioId(userWallet.walletId), - ) + .onEach { + updateContent(tokenActionsState = it, portfolioId = PortfolioId(userWallet.walletId)) } } private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) { - stateHolder.update( + stateController.update( SetCryptoCurrencyActionsTransformer( tokenActionsState = tokenActionsState, userWallet = userWallet, @@ -48,9 +47,8 @@ internal class SingleWalletButtonsSubscriber( ) } - private suspend fun getCryptoCurrencyActionsUseCase(userWallet: UserWallet, status: CryptoCurrencyStatus) = - this.getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = status) - .conflate() - .distinctUntilChanged() - .firstOrNull() + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletButtonsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt deleted file mode 100644 index 85d65823fb..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriberV2.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetCryptoCurrencyActionsTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.onEach - -internal class SingleWalletButtonsSubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val getCryptoCurrencyActionsUseCaseV2: GetCryptoCurrencyActionsUseCaseV2, -) : BasicSingleWalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow { - return getPrimaryCurrencyStatusFlow() - .flatMapLatest { - getCryptoCurrencyActionsUseCaseV2(accountId = accountId, currency = it.currency) - } - .onEach { - updateContent(tokenActionsState = it, portfolioId = PortfolioId(userWallet.walletId)) - } - } - - private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) { - stateController.update( - SetCryptoCurrencyActionsTransformer( - tokenActionsState = tokenActionsState, - userWallet = userWallet, - clickIntents = clickIntents, - portfolioId = portfolioId, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt index d417654a2f..6a8d7f382b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -1,93 +1,92 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers -import arrow.core.Either -import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.onramp.GetOnrampTransactionsUseCase import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -import timber.log.Timber @Suppress("LongParameterList") -@Deprecated("Use SingleWalletExpressStatusesSubscriberV2 instead") -internal class SingleWalletExpressStatusesSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class SingleWalletExpressStatusesSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, + private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, + private val stateController: WalletStateController, private val clickIntents: WalletClickIntents, private val analyticsEventHandler: AnalyticsEventHandler, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, -) : WalletSubscriber() { - - override fun create( - coroutineScope: CoroutineScope, - ): Flow, AppCurrency>> { - return combine( - flow = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWallet.walletId) - .conflate() - .distinctUntilChanged(), - flow2 = getSelectedAppCurrencyUseCase() - .conflate() - .distinctUntilChanged() - .map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } }, - transform = { maybeCurrencyStatus, appCurrency -> maybeCurrencyStatus to appCurrency }, - ).onEach { maybeCurrencyStatusAndAppCurrency -> - val status = maybeCurrencyStatusAndAppCurrency.first.getOrElse { - Timber.e("Unable to get primary currency status: $it") - return@onEach - } +) : BasicSingleWalletSubscriber() { + @OptIn(ExperimentalCoroutinesApi::class) + override fun create(coroutineScope: CoroutineScope): Flow<*> { + val getOnrampTransactionsFlow = getPrimaryCurrencyStatusFlow().flatMapLatest { currencyStatus -> getOnrampTransactionsUseCase( userWalletId = userWallet.walletId, - cryptoCurrencyId = status.currency.id, - ).onEach { maybeTransaction -> + cryptoCurrencyId = currencyStatus.currency.id, + ) + .map { currencyStatus to it } + } + + return combine( + flow = getOnrampTransactionsFlow, + flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), + transform = ::toTriple, + ) + .onEach { (status, maybeTransaction, appCurrency) -> maybeTransaction.fold( ifRight = { onrampTxs -> onrampTxs.clearHiddenTerminal() - stateHolder.update( + stateController.update( SetExpressStatusesTransformer( userWalletId = userWallet.walletId, onrampTxs = onrampTxs, clickIntents = clickIntents, cryptoCurrencyStatus = status, - appCurrency = maybeCurrencyStatusAndAppCurrency.second, + appCurrency = appCurrency, analyticsEventHandler = analyticsEventHandler, ), ) }, ifLeft = { - stateHolder.update( + stateController.update( SetExpressStatusesTransformer( userWalletId = userWallet.walletId, - onrampTxs = listOf(), + onrampTxs = emptyList(), clickIntents = clickIntents, cryptoCurrencyStatus = status, - appCurrency = maybeCurrencyStatusAndAppCurrency.second, + appCurrency = appCurrency, analyticsEventHandler = analyticsEventHandler, ), ) }, ) } - .launchIn(coroutineScope) - } + } + + private fun toTriple(firstPair: Pair, second: C): Triple { + return Triple(firstPair.first, firstPair.second, second) } private suspend fun List.clearHiddenTerminal() { - this.filter { it.status.isHidden && it.status.isTerminal } + this + .filter { it.status.isHidden && it.status.isTerminal } .forEach { onrampRemoveTransactionUseCase(txId = it.txId) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletExpressStatusesSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt deleted file mode 100644 index 59581e98ce..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriberV2.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.onramp.GetOnrampTransactionsUseCase -import com.tangem.domain.onramp.OnrampRemoveTransactionUseCase -import com.tangem.domain.onramp.model.cache.OnrampTransaction -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetExpressStatusesTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -@Suppress("LongParameterList") -internal class SingleWalletExpressStatusesSubscriberV2( - override val userWallet: UserWallet, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val getOnrampTransactionsUseCase: GetOnrampTransactionsUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val onrampRemoveTransactionUseCase: OnrampRemoveTransactionUseCase, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, - private val analyticsEventHandler: AnalyticsEventHandler, -) : BasicSingleWalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow<*> { - val getOnrampTransactionsFlow = getPrimaryCurrencyStatusFlow().flatMapLatest { currencyStatus -> - getOnrampTransactionsUseCase( - userWalletId = userWallet.walletId, - cryptoCurrencyId = currencyStatus.currency.id, - ) - .map { currencyStatus to it } - } - - return combine( - flow = getOnrampTransactionsFlow, - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - transform = ::toTriple, - ) - .onEach { (status, maybeTransaction, appCurrency) -> - maybeTransaction.fold( - ifRight = { onrampTxs -> - onrampTxs.clearHiddenTerminal() - stateController.update( - SetExpressStatusesTransformer( - userWalletId = userWallet.walletId, - onrampTxs = onrampTxs, - clickIntents = clickIntents, - cryptoCurrencyStatus = status, - appCurrency = appCurrency, - analyticsEventHandler = analyticsEventHandler, - ), - ) - }, - ifLeft = { - stateController.update( - SetExpressStatusesTransformer( - userWalletId = userWallet.walletId, - onrampTxs = listOf(), - clickIntents = clickIntents, - cryptoCurrencyStatus = status, - appCurrency = appCurrency, - analyticsEventHandler = analyticsEventHandler, - ), - ) - }, - ) - } - } - - private fun toTriple(firstPair: Pair, second: C): Triple { - return Triple(firstPair.first, firstPair.second, second) - } - - private suspend fun List.clearHiddenTerminal() { - this - .filter { it.status.isHidden && it.status.isTerminal } - .forEach { onrampRemoveTransactionUseCase(txId = it.txId) } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt index 349dce0765..42839a3e87 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -7,6 +7,9 @@ import com.tangem.feature.wallet.presentation.wallet.domain.GetSingleWalletWarni import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope @@ -18,9 +21,9 @@ import kotlinx.coroutines.flow.onEach /** [REDACTED_AUTHOR] */ -internal class SingleWalletNotificationsSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, +internal class SingleWalletNotificationsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, + private val stateController: WalletStateController, private val getSingleWalletWarningsFactory: GetSingleWalletWarningsFactory, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val clickIntents: WalletClickIntents, @@ -31,10 +34,15 @@ internal class SingleWalletNotificationsSubscriber( .conflate() .distinctUntilChanged() .onEach { warnings -> - val displayedState = stateHolder.getWalletState(userWallet.walletId) + val displayedState = stateController.getWalletState(userWallet.walletId) - stateHolder.update(SetWarningsTransformer(userWallet.walletId, warnings, persistentListOf())) + stateController.update(SetWarningsTransformer(userWallet.walletId, warnings, persistentListOf())) walletWarningsAnalyticsSender.send(displayedState, warnings) } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): SingleWalletNotificationsSubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt deleted file mode 100644 index af21e47dc4..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenListSubscriber.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.usecase.YieldSupplyApyFlowUseCase -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender -import com.tangem.feature.wallet.presentation.wallet.domain.MultiWalletTokenListStore -import com.tangem.feature.wallet.presentation.wallet.domain.WalletWithFundsChecker -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import kotlinx.coroutines.CoroutineScope - -@Deprecated("Use SingleWalletWithTokenSubscriber instead") -@Suppress("LongParameterList") -internal class SingleWalletWithTokenListSubscriber( - private val userWallet: UserWallet.Cold, - private val tokenListStore: MultiWalletTokenListStore, - stateHolder: WalletStateController, - clickIntents: WalletClickIntents, - tokenListAnalyticsSender: TokenListAnalyticsSender, - walletWithFundsChecker: WalletWithFundsChecker, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase: YieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, -) : BasicTokenListSubscriber( - userWallet = userWallet, - stateHolder = stateHolder, - clickIntents = clickIntents, - tokenListAnalyticsSender = tokenListAnalyticsSender, - walletWithFundsChecker = walletWithFundsChecker, - getSelectedAppCurrencyUseCase = getSelectedAppCurrencyUseCase, - yieldSupplyApyFlowUseCase = yieldSupplyApyFlowUseCase, - stakingAvailabilityListUseCase = stakingAvailabilityListUseCase, - yieldSupplyGetShouldShowMainPromoUseCase = yieldSupplyGetShouldShowMainPromoUseCase, -) { - - override fun tokenListFlow(coroutineScope: CoroutineScope): LceFlow { - tokenListStore.addIfNot(userWallet.walletId, coroutineScope) - - return tokenListStore.getOrThrow(userWallet.walletId) - } - - override suspend fun onTokenListReceived(maybeTokenList: Lce) = Unit -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt index bef7b6defb..3bd4244ab3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt @@ -4,47 +4,46 @@ import androidx.paging.PagingData import androidx.paging.cachedIn import androidx.paging.map import arrow.core.Either +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.TxInfo import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.txhistory.models.TxHistoryListError import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.domain.collectLatest import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map -typealias MaybeTxHistoryCount = Either -typealias MaybeTxHistoryItems = Either>> - @Suppress("LongParameterList") -@Deprecated("Use TxHistorySubscriberV2 instead") -internal class TxHistorySubscriber( - private val userWallet: UserWallet.Cold, - private val isRefresh: Boolean, - private val stateHolder: WalletStateController, - private val clickIntents: WalletClickIntents, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, +internal class TxHistorySubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet.Cold, + @Assisted private val isRefresh: Boolean, + override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, -) : WalletSubscriber() { + private val stateController: WalletStateController, + private val clickIntents: WalletClickIntents, +) : BasicSingleWalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow> { return flow { - getSingleCryptoCurrencyStatusUseCase.collectLatest(userWalletId = userWallet.walletId) { status -> + getPrimaryCurrencyStatusFlow().collectLatest { status -> val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( userWalletId = userWallet.walletId, currency = status.currency, @@ -52,29 +51,32 @@ internal class TxHistorySubscriber( setLoadingTxHistoryState(maybeTxHistoryItemCount, status) - maybeTxHistoryItemCount.onRight { + maybeTxHistoryItemCount.onRight { _ -> val maybeTxHistoryItems = txHistoryItemsUseCase( userWalletId = userWallet.walletId, currency = status.currency, refresh = isRefresh, ).map { it.cachedIn(coroutineScope) } - setLoadedTxHistoryState(maybeTxHistoryItems, status.currency) + setLoadedTxHistoryState(maybeTxHistoryItems, currency = status.currency) } } } } - private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { - stateHolder.update( + private fun setLoadingTxHistoryState( + maybeTxHistoryItemCount: Either, + status: CryptoCurrencyStatus, + ) { + stateController.update( maybeTxHistoryItemCount.fold( ifLeft = { error -> SetTxHistoryCountErrorTransformer( userWallet = userWallet, error = error, pendingTransactions = status.value.pendingTransactions, - currency = status.currency, clickIntents = clickIntents, + currency = status.currency, ) }, ifRight = { txCount -> @@ -88,23 +90,26 @@ internal class TxHistorySubscriber( ) } - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { - stateHolder.update( + private fun setLoadedTxHistoryState( + maybeTxHistoryItems: Either>>, + currency: CryptoCurrency, + ) { + stateController.update( maybeTxHistoryItems.fold( - ifLeft = { + ifLeft = { error -> SetTxHistoryItemsErrorTransformer( userWalletId = userWallet.walletId, - error = it, + error = error, clickIntents = clickIntents, ) }, ifRight = { itemsFlow -> val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() val itemConverter = TxHistoryItemStateConverter( + currency = currency, symbol = blockchain.currency, decimals = blockchain.decimals(), clickIntents = clickIntents, - currency = currency, ) SetTxHistoryItemsTransformer( @@ -118,4 +123,9 @@ internal class TxHistorySubscriber( ), ) } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): TxHistorySubscriber + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt deleted file mode 100644 index 78c388f87f..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberV2.kt +++ /dev/null @@ -1,114 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import androidx.paging.PagingData -import androidx.paging.cachedIn -import androidx.paging.map -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.TxInfo -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryCountTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsErrorTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTxHistoryItemsTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TxHistoryItemStateConverter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.map - -@Suppress("LongParameterList") -internal class TxHistorySubscriberV2( - override val userWallet: UserWallet.Cold, - override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val txHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, - private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase, - private val isRefresh: Boolean, - private val stateController: WalletStateController, - private val clickIntents: WalletClickIntents, -) : BasicSingleWalletSubscriber() { - - override fun create(coroutineScope: CoroutineScope): Flow> { - return flow { - getPrimaryCurrencyStatusFlow().collectLatest { status -> - val maybeTxHistoryItemCount = txHistoryItemsCountUseCase( - userWalletId = userWallet.walletId, - currency = status.currency, - ) - - setLoadingTxHistoryState(maybeTxHistoryItemCount, status) - - maybeTxHistoryItemCount.onRight { - val maybeTxHistoryItems = txHistoryItemsUseCase( - userWalletId = userWallet.walletId, - currency = status.currency, - refresh = isRefresh, - ).map { it.cachedIn(coroutineScope) } - - setLoadedTxHistoryState(maybeTxHistoryItems, currency = status.currency) - } - } - } - } - - private fun setLoadingTxHistoryState(maybeTxHistoryItemCount: MaybeTxHistoryCount, status: CryptoCurrencyStatus) { - stateController.update( - maybeTxHistoryItemCount.fold( - ifLeft = { error -> - SetTxHistoryCountErrorTransformer( - userWallet = userWallet, - error = error, - pendingTransactions = status.value.pendingTransactions, - clickIntents = clickIntents, - currency = status.currency, - ) - }, - ifRight = { txCount -> - SetTxHistoryCountTransformer( - userWalletId = userWallet.walletId, - transactionsCount = txCount, - clickIntents = clickIntents, - ) - }, - ), - ) - } - - private fun setLoadedTxHistoryState(maybeTxHistoryItems: MaybeTxHistoryItems, currency: CryptoCurrency) { - stateController.update( - maybeTxHistoryItems.fold( - ifLeft = { - SetTxHistoryItemsErrorTransformer( - userWalletId = userWallet.walletId, - error = it, - clickIntents = clickIntents, - ) - }, - ifRight = { itemsFlow -> - val blockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() - val itemConverter = TxHistoryItemStateConverter( - currency = currency, - symbol = blockchain.currency, - decimals = blockchain.decimals(), - clickIntents = clickIntents, - ) - - SetTxHistoryItemsTransformer( - userWallet = userWallet, - flow = itemsFlow.map { items -> - items.map(itemConverter::convert) - }, - clickIntents = clickIntents, - ) - }, - ), - ) - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt deleted file mode 100644 index 34b9a3bc97..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.subscribers - -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.nft.GetNFTCollectionsUseCase -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.wallets.repository.WalletsRepository -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents -import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.transformers.RemoveNFTCollectionsTransformer -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetNFTCollectionsTransformer -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -@Deprecated("Use WalletNFTListSubscriberV2 instead") -internal class WalletNFTListSubscriber( - private val userWallet: UserWallet, - private val stateHolder: WalletStateController, - private val walletsRepository: WalletsRepository, - private val currenciesRepository: CurrenciesRepository, - private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, - private val clickIntents: WalletClickIntents, -) : WalletSubscriber() { - - @OptIn(ExperimentalCoroutinesApi::class) - override fun create(coroutineScope: CoroutineScope): Flow<*> = combine( - walletsRepository.nftEnabledStatus(userWallet.walletId), - currenciesRepository.getWalletCurrenciesUpdates(userWallet.walletId), - ) { nftEnabled, currencies -> nftEnabled to currencies } - .distinctUntilChanged() - .flatMapLatest { (nftEnabled, currencies) -> - // if NFT is enabled for this wallet and there are currencies, - // then start observing changes from store and apply transformer if need - if (nftEnabled && currencies.isNotEmpty()) { - getNFTCollectionsUseCase(userWallet.walletId) - .shareIn( - scope = coroutineScope, - started = SharingStarted.WhileSubscribed(), - replay = 1, - ) - .onEach { - stateHolder.update( - SetNFTCollectionsTransformer( - userWalletId = userWallet.walletId, - nftCollections = it, - onItemClick = { clickIntents.onNFTClick(userWallet) }, - ), - ) - } - } else { - // otherwise, hide NFT from wallet - stateHolder.update( - RemoveNFTCollectionsTransformer(userWallet.walletId), - ) - emptyFlow() - } - } -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt index b163d627d6..0242c1bf72 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/utils/DefaultUserWalletsFetcher.kt @@ -6,7 +6,6 @@ import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetWalletTotalBalanceUseCaseV2 import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.error.SelectedAppCurrencyError @@ -20,7 +19,6 @@ import com.tangem.domain.models.TotalFiatBalance 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.tokens.GetWalletTotalBalanceUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.wallet.impl.R @@ -39,8 +37,6 @@ import kotlinx.coroutines.flow.* @Suppress("LongParameterList") internal class DefaultUserWalletsFetcher @AssistedInject constructor( getWalletsUseCase: GetWalletsUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, private val getWalletTotalBalanceUseCaseV2: GetWalletTotalBalanceUseCaseV2, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, @@ -103,11 +99,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor( // We should not load balances in auth mode flowOf(Lce.Loading(walletIds.associateWith { TotalFiatBalance.Loading })) } else { - if (accountsFeatureToggles.isFeatureEnabled) { - getWalletTotalBalanceUseCaseV2(userWalletIds = walletIds) - } else { - getWalletTotalBalanceUseCase(walletIds).distinctUntilChanged() - } + getWalletTotalBalanceUseCaseV2(userWalletIds = walletIds) } } From 286e28223cfb72d4abdb592dd4163de221bd2b79 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 16:00:39 +0500 Subject: [PATCH 48/97] Updated on 2026-08-14 --- .../com/tangem/datasource/di/MoshiModule.kt | 10 +++ .../visa/entity/PaymentAccountStatusDM.kt | 53 ++++++++++++ data/visa/build.gradle.kts | 6 +- .../PaymentAccountStatusDMConverter.kt | 67 +++++++++++++++ .../tangem/data/pay/di/TangemPayDataModule.kt | 36 ++++++++ .../pay/store/PaymentAccountStatusesStore.kt | 82 ++++++++++++++++--- .../com/tangem/domain/models/kyc/KycStatus.kt | 8 ++ 7 files changed, 248 insertions(+), 14 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt 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 c1e4a4f7dc..de63c7bbcd 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.local.visa.entity.PaymentAccountStatusDM import com.tangem.datasource.utils.SerializeNullsFactory import com.tangem.domain.models.scan.serialization.* import dagger.Module @@ -45,6 +46,15 @@ class MoshiModule { .withSubtype(NetworkStatusDM.Verified::class.java, "amounts") .withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"), ) + .add( + NamePolymorphicAdapterFactory.of(PaymentAccountStatusDM::class.java) + .withSubtype(PaymentAccountStatusDM.NotCreated::class.java, "not_created") + .withSubtype(PaymentAccountStatusDM.UnderReview::class.java, "kyc_status") + .withSubtype(PaymentAccountStatusDM.IssuingCard::class.java, "issuing_card") + .withSubtype(PaymentAccountStatusDM.Locked::class.java, "locked") + .withSubtype(PaymentAccountStatusDM.Loaded::class.java, "balance") + .withSubtype(PaymentAccountStatusDM.CardIssueFailed::class.java, "card_issue_failed"), + ) .add( PolymorphicJsonAdapterFactory.of(NFTCollection.Identifier::class.java, "bc") .withSubtype(NFTCollection.Identifier.EVM::class.java, "evm") diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt new file mode 100644 index 0000000000..589fb4d915 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusDM.kt @@ -0,0 +1,53 @@ +@file:Suppress("BooleanPropertyNaming") +package com.tangem.datasource.local.visa.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import com.tangem.domain.models.kyc.KycStatus +import dev.onenowy.moshipolymorphicadapter.PolymorphicAdapterType +import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel +import java.math.BigDecimal + +/** + * Payment account status for storage in the local cache. + * + * @see [com.tangem.domain.pay.PaymentAccountStatus] + */ +@JsonClass(generateAdapter = true, generator = PolymorphicAdapterType.NAME_POLYMORPHIC_ADAPTER) +sealed interface PaymentAccountStatusDM { + + @NameLabel("not_created") + data class NotCreated( + @Json(name = "not_created") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("kyc_status") + data class UnderReview( + @Json(name = "kyc_status") val kycStatus: KycStatus, + ) : PaymentAccountStatusDM + + @NameLabel("issuing_card") + data class IssuingCard( + @Json(name = "issuing_card") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("locked") + data class Locked( + @Json(name = "locked") val marker: Boolean = true, + ) : PaymentAccountStatusDM + + @NameLabel("balance") + data class Loaded( + @Json(name = "card_id") val cardId: String, + @Json(name = "last_four_digits") val lastFourDigits: String, + @Json(name = "balance") val balance: BigDecimal, + @Json(name = "currency_code") val currencyCode: String, + @Json(name = "deposit_address") val depositAddress: String?, + @Json(name = "is_pin_set") val isPinSet: Boolean, + ) : PaymentAccountStatusDM + + @NameLabel("card_issue_failed") + data class CardIssueFailed( + @Json(name = "card_issue_failed") val marker: Boolean = true, + ) : PaymentAccountStatusDM +} \ No newline at end of file diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 2c2683c14e..6443faf2f5 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -38,9 +38,6 @@ dependencies { implementation(projects.domain.common) implementation(projects.features.swap.domain) - /** Feature API - remove after removing [HotWalletFeatureToggles] */ - implementation(projects.features.hotWallet.api) - /** Project - Utils */ implementation(projects.core.utils) @@ -51,6 +48,7 @@ dependencies { implementation(projects.libs.visa) /** Libs - Other */ + implementation(deps.androidx.datastore) implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.arrow.fx) @@ -68,6 +66,6 @@ dependencies { implementation(projects.libs.tangemSdkApi) /** DI */ - implementation(deps.hilt.core) + implementation(deps.hilt.android) kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt new file mode 100644 index 0000000000..db6fbcca4f --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusDMConverter.kt @@ -0,0 +1,67 @@ +package com.tangem.data.pay.converter + +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convert +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter.convertBack +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.utils.converter.TwoWayConverter + +/** + * Two-way converter between [PaymentAccountStatus] and [PaymentAccountStatusDM]. + * + * [convert] maps domain → data model. Returns null for transient statuses that should not be persisted + * (Loading, ExposedDevice, Unavailable, NotSynced). + * + * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. + */ +internal object PaymentAccountStatusDMConverter : + TwoWayConverter { + + override fun convert(value: PaymentAccountStatus): PaymentAccountStatusDM? { + return when (value) { + is PaymentAccountStatus.NotCreated -> PaymentAccountStatusDM.NotCreated() + is PaymentAccountStatus.UnderReview -> PaymentAccountStatusDM.UnderReview(kycStatus = value.kycStatus) + is PaymentAccountStatus.IssuingCard -> PaymentAccountStatusDM.IssuingCard() + is PaymentAccountStatus.Locked -> PaymentAccountStatusDM.Locked() + is PaymentAccountStatus.Loaded -> PaymentAccountStatusDM.Loaded( + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + balance = value.balance, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + ) + is PaymentAccountStatus.Error.CardIssueFailed -> PaymentAccountStatusDM.CardIssueFailed() + // Transient statuses are not persisted + is PaymentAccountStatus.Loading, + is PaymentAccountStatus.Error.ExposedDevice, + is PaymentAccountStatus.Error.Unavailable, + is PaymentAccountStatus.Error.NotSynced, + -> null + } + } + + override fun convertBack(value: PaymentAccountStatusDM?): PaymentAccountStatus { + return when (value) { + is PaymentAccountStatusDM.CardIssueFailed -> PaymentAccountStatus.Error.CardIssueFailed + is PaymentAccountStatusDM.NotCreated -> PaymentAccountStatus.NotCreated + is PaymentAccountStatusDM.IssuingCard -> PaymentAccountStatus.IssuingCard(source = StatusSource.CACHE) + is PaymentAccountStatusDM.Locked -> PaymentAccountStatus.Locked(source = StatusSource.CACHE) + is PaymentAccountStatusDM.UnderReview -> PaymentAccountStatus.UnderReview( + source = StatusSource.CACHE, + kycStatus = value.kycStatus, + ) + is PaymentAccountStatusDM.Loaded -> PaymentAccountStatus.Loaded( + source = StatusSource.CACHE, + cardId = value.cardId, + lastFourDigits = value.lastFourDigits, + balance = value.balance, + currencyCode = value.currencyCode, + depositAddress = value.depositAddress, + isPinSet = value.isPinSet, + ) + null -> PaymentAccountStatus.Error.Unavailable(source = StatusSource.CACHE) + } + } +} \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt index d988e0088c..565fdbe700 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/di/TangemPayDataModule.kt @@ -1,13 +1,23 @@ package com.tangem.data.pay.di +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import com.squareup.moshi.Moshi import com.tangem.data.pay.DefaultTangemPayCryptoCurrencyFactory import com.tangem.data.pay.DefaultTangemPayEligibilityManager import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.repository.* +import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.data.pay.usecase.DefaultGetTangemPayCurrencyStatusUseCase import com.tangem.data.pay.usecase.DefaultGetTangemPayCustomerIdUseCase import com.tangem.data.pay.usecase.DefaultTangemPayWithdrawUseCase +import com.tangem.datasource.di.NetworkMoshi +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher @@ -21,11 +31,15 @@ import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository import com.tangem.security.DeviceSecurityInfoProvider +import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -92,6 +106,28 @@ internal interface TangemPayDataModule { companion object { + @Provides + @Singleton + fun providePaymentAccountStatusesStore( + @NetworkMoshi moshi: Moshi, + @ApplicationContext context: Context, + dispatchers: CoroutineDispatcherProvider, + ): PaymentAccountStatusesStore { + return PaymentAccountStatusesStore( + runtimeStore = RuntimeSharedStore(), + persistenceDataStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(), + defaultValue = emptyMap(), + ), + produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, + scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), + ), + dispatchers = dispatchers, + ) + } + @Provides @Singleton fun providePaymentAccountStatusSupplier( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt index c6cfc742e0..5b8866a09a 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/store/PaymentAccountStatusesStore.kt @@ -1,24 +1,86 @@ package com.tangem.data.pay.store +import androidx.datastore.core.DataStore +import com.tangem.data.pay.converter.PaymentAccountStatusDMConverter +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.visa.entity.PaymentAccountStatusDM import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.PaymentAccountStatus +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import javax.inject.Inject -import javax.inject.Singleton +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch +import timber.log.Timber -@Suppress("UnusedParameter", "EmptyFunctionBlock", "FunctionOnlyReturningConstant") -@Singleton -internal class PaymentAccountStatusesStore @Inject constructor() { +internal typealias WalletIdWithPaymentStatus = Map +internal typealias WalletIdWithPaymentStatusDM = Map + +/** + * Store for payment account statuses with dual storage (runtime + persistence). + * + * @property runtimeStore runtime store for fast in-memory access + * @property persistenceDataStore persistence store for caching across app restarts + */ +internal class PaymentAccountStatusesStore( + private val runtimeStore: RuntimeSharedStore, + private val persistenceDataStore: DataStore, + dispatchers: CoroutineDispatcherProvider, +) { + + private val scope = CoroutineScope(context = SupervisorJob() + dispatchers.io) + + init { + scope.launch { + try { + val cachedStatuses = persistenceDataStore.data.firstOrNull() ?: return@launch + runtimeStore.store( + value = cachedStatuses.mapValues { (_, statusDM) -> + PaymentAccountStatusDMConverter.convertBack(statusDM) + }, + ) + } catch (e: Exception) { + Timber.e(e, "Error while loading cached payment account statuses") + } + } + } fun get(userWalletId: UserWalletId): Flow { - return emptyFlow() + return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } } - fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { - return null + suspend fun getSyncOrNull(userWalletId: UserWalletId): PaymentAccountStatus? { + return runtimeStore.getSyncOrNull()?.get(userWalletId.stringValue) } - fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { + suspend fun store(userWalletId: UserWalletId, status: PaymentAccountStatus) { + coroutineScope { + launch { storeInRuntime(userWalletId = userWalletId, status = status) } + launch { storeInPersistence(userWalletId = userWalletId, status = status) } + } + } + + suspend fun contains(userWalletId: UserWalletId): Boolean { + return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue) + } + + private suspend fun storeInRuntime(userWalletId: UserWalletId, status: PaymentAccountStatus) { + runtimeStore.update(default = emptyMap()) { stored -> + stored.toMutableMap().apply { + put(key = userWalletId.stringValue, value = status) + } + } + } + + private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatus) { + val statusDM = PaymentAccountStatusDMConverter.convert(value = status) ?: return + persistenceDataStore.updateData { storedStatuses -> + storedStatuses.toMutableMap().apply { + put(key = userWalletId.stringValue, value = statusDM) + } + } } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt index 0546090329..7297bc7c87 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/kyc/KycStatus.kt @@ -1,20 +1,28 @@ package com.tangem.domain.models.kyc +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + private const val APPROVED_KYC_STATUS = "approved" private const val IN_PROGRESS_KYC_STATUS = "in_progress" private const val DECLINED_KYC_STATUS = "declined" +@JsonClass(generateAdapter = false) enum class KycStatus { /** Initial state */ + @Json(name = "init") INIT, /** Performing the check */ + @Json(name = "in_progress") PENDING, /** SumSub approved */ + @Json(name = "approved") APPROVED, /** The check failed, documents rejected */ + @Json(name = "declined") REJECTED, ; From aeef2b394588f299249b77e9d522c27cc4f52f88 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 15:24:36 +0400 Subject: [PATCH 49/97] Updated on 2026-08-14 --- .../com/tangem/tap/ApplicationEntryPoint.kt | 6 +- .../java/com/tangem/tap/TangemApplication.kt | 15 ++--- .../java/com/tangem/tap/di/ActivityModule.kt | 8 +-- .../tap/network/auth/DefaultAuthProvider.kt | 14 ++-- .../auth/DefaultP2PEthPoolAuthProvider.kt | 6 +- .../auth/DefaultStakeKitAuthProvider.kt | 6 +- .../tangem/tap/network/auth/di/AuthModule.kt | 14 ++-- .../moonpay/MoonPayService.kt | 17 +++-- .../tap/proxy/redux/DaggerGraphState.kt | 2 - .../core/abtests/di/ABTestsManagerModule.kt | 7 +- .../manager/impl/AmplitudeABTestsManager.kt | 5 +- .../datasource/api/common/config/BlockAid.kt | 9 +-- .../datasource/api/common/config/Express.kt | 10 +-- .../datasource/api/common/config/TangemPay.kt | 16 ++--- .../api/common/config/YieldSupply.kt | 8 +-- .../crypto/Sha256SignatureVerifier.kt | 8 +-- .../tangem/datasource/di/ApiConfigsModule.kt | 22 +++--- .../tangem/datasource/di/SecurityModule.kt | 6 +- .../di/local/config/ConfigModule.kt | 10 +-- .../DefaultEnvironmentConfigStorage.kt | 41 ------------ .../environment/EnvironmentConfigStorage.kt | 20 ------ .../api/common/config/ApiConfigTest.kt | 12 ++-- .../managers/MockEnvironmentConfigStorage.kt | 45 ------------- .../managers/ProdApiConfigsManagerTest.kt | 67 +++++++++++++++---- .../tangem/data/onramp/di/OnrampDataModule.kt | 6 +- .../onramp/legacy/MercuryoTopUpRepository.kt | 4 +- .../DefaultBlockchainSDKFactory.kt | 8 +-- .../di/BlockchainSDKFactoryModule.kt | 6 +- 28 files changed, 158 insertions(+), 240 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt delete mode 100644 core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt diff --git a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt index da98e59b92..ba01875acf 100644 --- a/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt +++ b/app/src/main/java/com/tangem/tap/ApplicationEntryPoint.kt @@ -19,7 +19,7 @@ import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -57,12 +57,12 @@ import dagger.hilt.components.SingletonComponent @Suppress("TooManyFunctions") interface ApplicationEntryPoint { - fun getEnvironmentConfigStorage(): EnvironmentConfigStorage - fun getAppStateHolder(): AppStateHolder fun getIssuersConfigStorage(): IssuersConfigStorage + fun getEnvironmentConfig(): EnvironmentConfig + fun getFeatureTogglesManager(): FeatureTogglesManager fun getExcludedBlockchainsManager(): ExcludedBlockchainsManager diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index de3252872f..7e7ecd4baa 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -36,7 +36,6 @@ import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.api.common.createNetworkLoggingInterceptor import com.tangem.datasource.connection.NetworkConnectionManager import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.logs.AppLogsStore import com.tangem.datasource.local.preferences.AppPreferencesStore @@ -80,7 +79,6 @@ import dagger.hilt.EntryPoints import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.rekotlin.Store import timber.log.Timber @@ -97,12 +95,12 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. private val appStateHolder: AppStateHolder get() = entryPoint.getAppStateHolder() - private val environmentConfigStorage: EnvironmentConfigStorage - get() = entryPoint.getEnvironmentConfigStorage() - private val issuersConfigStorage: IssuersConfigStorage get() = entryPoint.getIssuersConfigStorage() + private val environmentConfig: EnvironmentConfig + get() = entryPoint.getEnvironmentConfig() + private val featureTogglesManager: FeatureTogglesManager get() = entryPoint.getFeatureTogglesManager() @@ -306,9 +304,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. Timber.i(excludedBlockchainsManager.toString()) } - runBlocking { - initWithConfigDependency(environmentConfig = environmentConfigStorage.initialize()) - } + initWithConfigDependency(environmentConfig = environmentConfig) abTestsManager.init() @@ -345,7 +341,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. appStateHolder.mainStore = store wcInitializeUseCase.init( - projectId = environmentConfigStorage.getConfigSync().walletConnectProjectId, + projectId = environmentConfig.walletConnectProjectId, ) } @@ -376,7 +372,6 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. shareManager = shareManager, appRouter = appRouter, transactionSignerFactory = transactionSignerFactory, - environmentConfigStorage = environmentConfigStorage, onboardingV2FeatureToggles = onboardingV2FeatureToggles, onboardingRepository = onboardingRepository, excludedBlockchains = excludedBlockchains, diff --git a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt index 1f40496096..e5600cc208 100644 --- a/app/src/main/java/com/tangem/tap/di/ActivityModule.kt +++ b/app/src/main/java/com/tangem/tap/di/ActivityModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.di import com.tangem.datasource.api.moonpay.MoonPayApi -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.card.ScanCardUseCase import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.exchange.RampStateManager @@ -69,14 +69,14 @@ internal object ActivityModule { @Provides @Singleton fun provideExchangeService( - environmentConfigStorage: EnvironmentConfigStorage, getSelectedWalletUseCase: GetSelectedWalletUseCase, moonPayApi: MoonPayApi, + environmentConfig: EnvironmentConfig, ): SellService { return MoonPayService( api = moonPayApi, - apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiKey }, - secretKeyProvider = Provider { environmentConfigStorage.getConfigSync().moonPayApiSecretKey }, + apiKey = environmentConfig.moonPayApiKey, + secretKey = environmentConfig.moonPayApiSecretKey, userWalletProvider = { getSelectedWalletUseCase.sync().getOrNull() }, ) } diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt index 1c102c9e0c..027441c202 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultAuthProvider.kt @@ -3,7 +3,7 @@ package com.tangem.tap.network.auth import com.tangem.common.extensions.toHexString import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.ApiEnvironment -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.utils.Provider @@ -11,7 +11,7 @@ import com.tangem.utils.ProviderSuspend internal class DefaultAuthProvider( private val userWalletsListRepository: UserWalletsListRepository, - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : AuthProvider { override suspend fun getCardPublicKey(): String { @@ -47,11 +47,11 @@ internal class DefaultAuthProvider( ApiEnvironment.DEV, ApiEnvironment.DEV_2, ApiEnvironment.DEV_3, - -> environmentConfigStorage.getConfigSync().tangemApiKeyDev + -> environmentConfig.tangemApiKeyDev ApiEnvironment.STAGE_2, ApiEnvironment.STAGE, - -> environmentConfigStorage.getConfigSync().tangemApiKeyStage - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().tangemApiKey + -> environmentConfig.tangemApiKeyStage + ApiEnvironment.PROD -> environmentConfig.tangemApiKey } ?: error("No tangem tech api config provided") } } @@ -60,8 +60,8 @@ internal class DefaultAuthProvider( return ProviderSuspend { when (apiEnvironment.invoke()) { ApiEnvironment.DEV, - -> environmentConfigStorage.getConfigSync().gaslessTxApiKeyDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().gaslessTxApiKey + -> environmentConfig.gaslessTxApiKeyDev + ApiEnvironment.PROD -> environmentConfig.gaslessTxApiKey else -> error("No gasless tx api config provided for ${apiEnvironment.invoke()}") } ?: error("No gasless tx api config provided") } diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt index 93b0595647..9ed1541c3e 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultP2PEthPoolAuthProvider.kt @@ -1,15 +1,15 @@ package com.tangem.tap.network.auth -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig import com.tangem.lib.auth.P2PEthPoolAuthProvider internal class DefaultP2PEthPoolAuthProvider( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : P2PEthPoolAuthProvider { override fun getApiKey(): String { - val keys = environmentConfigStorage.getConfigSync().p2pApiKey + val keys = environmentConfig.p2pApiKey ?: error("No P2P api keys provided") return if (P2PEthPoolStakingConfig.USE_TESTNET) keys.hoodi else keys.mainnet diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt index ba0534ae88..079cad327a 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultStakeKitAuthProvider.kt @@ -1,13 +1,13 @@ package com.tangem.tap.network.auth -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.lib.auth.StakeKitAuthProvider internal class DefaultStakeKitAuthProvider( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : StakeKitAuthProvider { override fun getApiKey(): String { - return environmentConfigStorage.getConfigSync().stakeKitApiKey ?: error("No StakeKit api key provided") + return environmentConfig.stakeKitApiKey ?: error("No StakeKit api key provided") } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index e6cba79706..6ef9e06f96 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -1,7 +1,7 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.api.common.AuthProvider -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider @@ -22,11 +22,11 @@ internal class AuthModule { @Singleton fun provideAuthProvider( userWalletsListRepository: UserWalletsListRepository, - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, ): AuthProvider { return DefaultAuthProvider( userWalletsListRepository = userWalletsListRepository, - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, ) } @@ -38,14 +38,14 @@ internal class AuthModule { @Provides @Singleton - fun provideStakeKitAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): StakeKitAuthProvider { - return DefaultStakeKitAuthProvider(environmentConfigStorage) + fun provideStakeKitAuthProvider(environmentConfig: EnvironmentConfig): StakeKitAuthProvider { + return DefaultStakeKitAuthProvider(environmentConfig) } @Provides @Singleton - fun provideP2PEthPoolAuthProvider(environmentConfigStorage: EnvironmentConfigStorage): P2PEthPoolAuthProvider { - return DefaultP2PEthPoolAuthProvider(environmentConfigStorage) + fun provideP2PEthPoolAuthProvider(environmentConfig: EnvironmentConfig): P2PEthPoolAuthProvider { + return DefaultP2PEthPoolAuthProvider(environmentConfig) } @Provides diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt index 91e45c1573..742e44ec5e 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/moonpay/MoonPayService.kt @@ -19,7 +19,6 @@ import com.tangem.tap.domain.model.Currency import com.tangem.tap.network.exchangeServices.SellService import com.tangem.tap.network.exchangeServices.SellServiceInitializationStatus import com.tangem.tap.network.exchangeServices.moonpay.models.MoonPayAvailableCurrency -import com.tangem.utils.Provider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import timber.log.Timber @@ -28,8 +27,8 @@ import javax.crypto.spec.SecretKeySpec class MoonPayService( private val api: MoonPayApi, - private val apiKeyProvider: Provider, - private val secretKeyProvider: Provider, + private val apiKey: String, + private val secretKey: String, private val userWalletProvider: () -> UserWallet?, ) : SellService { @@ -47,18 +46,18 @@ class MoonPayService( _initializationStatus.value = lceLoading() performRequest { - val userStatus = when (val result = performRequest { api.getUserStatus(apiKeyProvider()) }) { + val userStatus = when (val result = performRequest { api.getUserStatus(apiKey) }) { is Result.Failure -> { - Timber.e("Failed to load user status", result.error) + Timber.e(result.error, "Failed to load user status") _initializationStatus.value = result.error.lceError() return@performRequest } is Result.Success -> result.data } - val currencies = when (val result = performRequest { api.getCurrencies(apiKeyProvider()) }) { + val currencies = when (val result = performRequest { api.getCurrencies(apiKey) }) { is Result.Failure -> { - Timber.e("Failed to load currencies", result.error) + Timber.e(result.error, "Failed to load currencies") _initializationStatus.value = result.error.lceError() return@performRequest } @@ -163,7 +162,7 @@ class MoonPayService( val uri = Uri.Builder() .scheme(SCHEME) .authority(URL_SELL) - .appendQueryParameter("apiKey", apiKeyProvider()) + .appendQueryParameter("apiKey", apiKey) .appendQueryParameter("baseCurrencyCode", moonpayCurrency.currencyCode.uppercase()) .appendQueryParameter("refundWalletAddress", walletAddress) .appendQueryParameter("redirectURL", "tangem://redirect_sell?currency_id=${cryptoCurrency.id.value}") @@ -179,7 +178,7 @@ class MoonPayService( private fun createSignature(data: String): String { val sha256Hmac = Mac.getInstance("HmacSHA256") - val secretKey = SecretKeySpec(secretKeyProvider().toByteArray(), "HmacSHA256") + val secretKey = SecretKeySpec(secretKey.toByteArray(), "HmacSHA256") sha256Hmac.init(secretKey) val sha256encoded = sha256Hmac.doFinal("?$data".toByteArray()) return Base64.encodeToString(sha256encoded, Base64.NO_WRAP) diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt index 133c325afc..d86379f650 100644 --- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt +++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt @@ -11,7 +11,6 @@ import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.data.card.TransactionSignerFactory import com.tangem.datasource.connection.NetworkConnectionManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore @@ -63,7 +62,6 @@ data class DaggerGraphState( val shareManager: ShareManager? = null, val appRouter: AppRouter? = null, val transactionSignerFactory: TransactionSignerFactory? = null, - val environmentConfigStorage: EnvironmentConfigStorage? = null, val onboardingV2FeatureToggles: OnboardingV2FeatureToggles? = null, val onboardingRepository: OnboardingRepository? = null, val excludedBlockchains: ExcludedBlockchains? = null, diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt index 3934f4ba5c..30c1e3b07e 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/di/ABTestsManagerModule.kt @@ -5,8 +5,7 @@ import com.tangem.core.abtests.BuildConfig import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.abtests.manager.impl.AmplitudeABTestsManager import com.tangem.core.abtests.manager.impl.StubABTestsManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage -import com.tangem.utils.Provider +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -24,7 +23,7 @@ internal object ABTestsManagerModule { @Singleton fun provideABTestsManager( application: Application, - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, dispatchers: CoroutineDispatcherProvider, ): ABTestsManager { return if (BuildConfig.AB_TESTS_ENABLED) { @@ -32,7 +31,7 @@ internal object ABTestsManagerModule { } else { AmplitudeABTestsManager( application = application, - apiKeyProvider = Provider { environmentConfigStorage.getConfigSync().amplitudeApiKey }, + apiKey = environmentConfig.amplitudeApiKey, scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), ) } diff --git a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt index d2b6dbf52c..2307363d90 100644 --- a/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt +++ b/core/ab-tests/src/main/kotlin/com/tangem/core/abtests/manager/impl/AmplitudeABTestsManager.kt @@ -7,14 +7,13 @@ import com.amplitude.experiment.ExperimentConfig import com.amplitude.experiment.ExperimentUser import com.tangem.core.abtests.manager.ABTestsManager import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.utils.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import timber.log.Timber internal class AmplitudeABTestsManager( val application: Application, - val apiKeyProvider: Provider, + val apiKey: String, val scope: CoroutineScope, ) : ABTestsManager { @@ -28,7 +27,7 @@ internal class AmplitudeABTestsManager( client = Experiment.initializeWithAmplitudeAnalytics( application = application, - apiKey = apiKeyProvider(), + apiKey = apiKey, config = ExperimentConfig .builder() .automaticFetchOnAmplitudeIdentityChange(true) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt index f05797c816..7fcda27661 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/BlockAid.kt @@ -1,11 +1,10 @@ package com.tangem.datasource.api.common.config -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend -import kotlinx.coroutines.flow.first internal class BlockAid( - private val configStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, ) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = ApiEnvironment.PROD @@ -21,9 +20,7 @@ internal class BlockAid( put( key = "X-API-KEY", value = ProviderSuspend { - requireNotNull( - configStorage.getConfig().first { !it.blockAidApiKey.isNullOrEmpty() }.blockAidApiKey, - ) + requireNotNull(environmentConfig.blockAidApiKey) }, ) put("accept", ProviderSuspend { "application/json" }) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt index 4104dae025..b9923936b3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/Express.kt @@ -1,7 +1,7 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.utils.RequestHeader import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.utils.ProviderSuspend @@ -11,13 +11,13 @@ import com.tangem.utils.version.AppVersionProvider /** * Express [ApiConfig] * - * @property environmentConfigStorage environment config storage + * @property environmentConfig environment config * @property expressAuthProvider express auth provider * @property appVersionProvider app version provider * @property appInfoProvider app info provider */ internal class Express( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val expressAuthProvider: ExpressAuthProvider, private val appVersionProvider: AppVersionProvider, private val appInfoProvider: AppInfoProvider, @@ -100,9 +100,9 @@ internal class Express( private fun getApiKey(isProd: Boolean): String { return if (isProd) { - environmentConfigStorage.getConfigSync().express + environmentConfig.express } else { - environmentConfigStorage.getConfigSync().devExpress + environmentConfig.devExpress } ?.apiKey ?: error("No express config provided") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt index b494cbc70c..5c20eca2cf 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/TangemPay.kt @@ -1,13 +1,13 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend import com.tangem.utils.version.AppVersionProvider internal sealed class TangemPay( + private val environmentConfig: EnvironmentConfig, private val appVersionProvider: AppVersionProvider, - private val environmentConfigStorage: EnvironmentConfigStorage, ) : ApiConfig() { override val defaultEnvironment: ApiEnvironment = getInitialEnvironment() @@ -61,8 +61,8 @@ internal sealed class TangemPay( return when (apiEnvironment) { ApiEnvironment.MOCK, ApiEnvironment.DEV, - -> environmentConfigStorage.getConfigSync().bffStaticTokenDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().bffStaticToken + -> environmentConfig.bffStaticTokenDev + ApiEnvironment.PROD -> environmentConfig.bffStaticToken ApiEnvironment.STAGE, ApiEnvironment.STAGE_2, ApiEnvironment.DEV_2, @@ -72,9 +72,9 @@ internal sealed class TangemPay( } class Bff( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ) : TangemPay(appVersionProvider, environmentConfigStorage) { + ) : TangemPay(environmentConfig, appVersionProvider) { override fun getBaseUrl(apiEnvironment: ApiEnvironment): String { return when (apiEnvironment) { ApiEnvironment.DEV -> "https://api.dev.us.paera.com/bff-v2/" @@ -90,9 +90,9 @@ internal sealed class TangemPay( } class Auth( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ) : TangemPay(appVersionProvider, environmentConfigStorage) { + ) : TangemPay(environmentConfig, appVersionProvider) { override fun getBaseUrl(apiEnvironment: ApiEnvironment): String { return when (apiEnvironment) { ApiEnvironment.DEV -> "https://api.dev.us.paera.com/" diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt index b5e7ea38c6..ed04143824 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/config/YieldSupply.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.api.common.config import com.tangem.datasource.BuildConfig import com.tangem.datasource.api.common.AuthProvider -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.utils.RequestHeader import com.tangem.utils.ProviderSuspend import com.tangem.utils.info.AppInfoProvider @@ -10,7 +10,7 @@ import com.tangem.utils.version.AppVersionProvider /** YieldSupply [ApiConfig] */ internal class YieldSupply( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val appVersionProvider: AppVersionProvider, private val authProvider: AuthProvider, private val appInfoProvider: AppInfoProvider, @@ -78,8 +78,8 @@ internal class YieldSupply( ApiEnvironment.DEV_3, ApiEnvironment.STAGE, ApiEnvironment.STAGE_2, - -> environmentConfigStorage.getConfigSync().yieldModuleApiKeyDev - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().yieldModuleApiKey + -> environmentConfig.yieldModuleApiKeyDev + ApiEnvironment.PROD -> environmentConfig.yieldModuleApiKey } ?: error("No tangem tech api config provided") } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt index 6938d499d7..40ffd06873 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt @@ -5,10 +5,10 @@ import com.tangem.crypto.CryptoUtils import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment import com.tangem.datasource.api.common.config.managers.ApiConfigsManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig internal class Sha256SignatureVerifier( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val apiConfigsManager: ApiConfigsManager, ) : DataSignatureVerifier { @@ -24,8 +24,8 @@ internal class Sha256SignatureVerifier( private fun getPubKey(): String? { val expressConfig = apiConfigsManager.getEnvironmentConfig(ApiConfig.ID.Express) return when (expressConfig.environment) { - ApiEnvironment.PROD -> environmentConfigStorage.getConfigSync().express?.signVerifierPublicKey - else -> environmentConfigStorage.getConfigSync().devExpress?.signVerifierPublicKey + ApiEnvironment.PROD -> environmentConfig.express?.signVerifierPublicKey + else -> environmentConfig.devExpress?.signVerifierPublicKey } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt index 20d4c632d5..e8db8ec219 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/ApiConfigsModule.kt @@ -2,7 +2,7 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.config.* -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider import com.tangem.lib.auth.StakeKitAuthProvider @@ -21,13 +21,13 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideExpressConfig( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, expressAuthProvider: ExpressAuthProvider, appVersionProvider: AppVersionProvider, appInfoProvider: AppInfoProvider, ): ApiConfig { return Express( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, expressAuthProvider = expressAuthProvider, appVersionProvider = appVersionProvider, appInfoProvider = appInfoProvider, @@ -73,12 +73,12 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideYieldSupplyConfig( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, authProvider: AuthProvider, appInfoProvider: AppInfoProvider, ): ApiConfig = YieldSupply( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, authProvider = authProvider, appInfoProvider = appInfoProvider, @@ -87,21 +87,21 @@ internal object ApiConfigsModule { @Provides @IntoSet fun provideTangemPayBffConfig( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ): ApiConfig = TangemPay.Bff(appVersionProvider, environmentConfigStorage) + ): ApiConfig = TangemPay.Bff(environmentConfig, appVersionProvider) @Provides @IntoSet fun provideTangemPayAuthConfig( + environmentConfig: EnvironmentConfig, appVersionProvider: AppVersionProvider, - environmentConfigStorage: EnvironmentConfigStorage, - ): ApiConfig = TangemPay.Auth(appVersionProvider, environmentConfigStorage) + ): ApiConfig = TangemPay.Auth(environmentConfig, appVersionProvider) @Provides @IntoSet - fun provideBlockAidConfig(environmentConfigStorage: EnvironmentConfigStorage): ApiConfig { - return BlockAid(environmentConfigStorage) + fun provideBlockAidConfig(environmentConfig: EnvironmentConfig): ApiConfig { + return BlockAid(environmentConfig) } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt index 82d2ca69da..c04614c1f7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt @@ -3,7 +3,7 @@ package com.tangem.datasource.di import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.crypto.Sha256SignatureVerifier -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -17,9 +17,9 @@ internal object SecurityModule { @Provides @Singleton fun provideDataSignatureVerifier( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, apiConfigsManager: ApiConfigsManager, ): DataSignatureVerifier { - return Sha256SignatureVerifier(environmentConfigStorage, apiConfigsManager) + return Sha256SignatureVerifier(environmentConfig, apiConfigsManager) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt index 0ed4457201..c8052af564 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/local/config/ConfigModule.kt @@ -1,9 +1,8 @@ package com.tangem.datasource.di.local.config import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.local.config.environment.DefaultEnvironmentConfigStorage import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.converter.GeneratedEnvironmentConfigConverter import com.tangem.datasource.local.config.issuers.DefaultIssuersConfigStorage import com.tangem.datasource.local.config.issuers.IssuersConfigStorage import com.tangem.datasource.local.config.providers.BlockchainProvidersStorage @@ -23,11 +22,8 @@ internal object ConfigModule { @Provides @Singleton - fun provideEnvironmentConfigStorage(assetLoader: AssetLoader): EnvironmentConfigStorage { - return DefaultEnvironmentConfigStorage( - assetLoader = assetLoader, - environmentConfigStore = RuntimeStateStore(defaultValue = EnvironmentConfig()), - ) + fun provideEnvironmentConfig(): EnvironmentConfig { + return GeneratedEnvironmentConfigConverter.convert() } @Provides diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt deleted file mode 100644 index 1ad363973d..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/DefaultEnvironmentConfigStorage.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.tangem.datasource.local.config.environment - -import com.tangem.datasource.BuildConfig -import com.tangem.datasource.asset.loader.AssetLoader -import com.tangem.datasource.local.config.environment.converter.EnvironmentConfigConverter -import com.tangem.datasource.local.config.environment.models.EnvironmentConfigModel -import com.tangem.datasource.local.datastore.RuntimeStateStore -import kotlinx.coroutines.flow.Flow -import timber.log.Timber - -/** - * Default implementation for storing [EnvironmentConfig] - * - * @property assetLoader asset loader - * @property environmentConfigStore config store - */ -internal class DefaultEnvironmentConfigStorage( - private val assetLoader: AssetLoader, - private val environmentConfigStore: RuntimeStateStore, -) : EnvironmentConfigStorage { - - override suspend fun initialize(): EnvironmentConfig { - val environmentConfigModel = assetLoader.load(fileName = CONFIG_FILE_NAME) - ?: return environmentConfigStore.get().value - - val config = EnvironmentConfigConverter.convert(value = environmentConfigModel) - environmentConfigStore.store(value = config) - - Timber.i("Config [$CONFIG_FILE_NAME] loaded successfully") - - return config - } - - override fun getConfig(): Flow = environmentConfigStore.get() - - override fun getConfigSync(): EnvironmentConfig = environmentConfigStore.get().value - - private companion object { - const val CONFIG_FILE_NAME = "tangem-app-config/config_${BuildConfig.ENVIRONMENT}" - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt deleted file mode 100644 index a46edc5d9a..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfigStorage.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.datasource.local.config.environment - -import kotlinx.coroutines.flow.Flow - -/** - * Storage for [EnvironmentConfig] - * -[REDACTED_AUTHOR] - */ -interface EnvironmentConfigStorage { - - /** Initialize and return [EnvironmentConfig] */ - suspend fun initialize(): EnvironmentConfig - - /** Get [EnvironmentConfig] as [Flow] */ - fun getConfig(): Flow - - /** Get [EnvironmentConfig] synchronously */ - fun getConfigSync(): EnvironmentConfig -} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt index 2e0ccb145b..5650f72e16 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/ApiConfigTest.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.api.common.config import com.google.common.truth.Truth import com.tangem.datasource.api.common.AuthProvider +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.utils.ProviderSuspend import io.mockk.clearMocks import io.mockk.every @@ -19,6 +20,7 @@ class ApiConfigTest { private val appAuthProvider = mockk() private val apiKeyProvider = mockk>() + private val environmentConfig = mockk() @BeforeEach fun setup() { @@ -47,7 +49,7 @@ class ApiConfigTest { when (it) { ApiConfig.ID.Express -> { Express( - environmentConfigStorage = mockk(), + environmentConfig = environmentConfig, expressAuthProvider = mockk(), appVersionProvider = mockk(), appInfoProvider = mockk(), @@ -55,7 +57,7 @@ class ApiConfigTest { } ApiConfig.ID.YieldSupply -> { YieldSupply( - environmentConfigStorage = mockk(), + environmentConfig = environmentConfig, appVersionProvider = mockk(), authProvider = appAuthProvider, appInfoProvider = mockk(), @@ -70,14 +72,14 @@ class ApiConfigTest { } ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = mockk()) ApiConfig.ID.TangemPay -> TangemPay.Bff( + environmentConfig = environmentConfig, appVersionProvider = mockk(), - environmentConfigStorage = mockk() ) ApiConfig.ID.TangemPayAuth -> TangemPay.Auth( + environmentConfig = environmentConfig, appVersionProvider = mockk(), - environmentConfigStorage = mockk() ) - ApiConfig.ID.BlockAid -> BlockAid(configStorage = mockk()) + ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig) ApiConfig.ID.MoonPay -> MoonPay() ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = mockk()) ApiConfig.ID.News -> News( diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt deleted file mode 100644 index 530d4de06e..0000000000 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/MockEnvironmentConfigStorage.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.datasource.api.common.config.managers - -import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage -import com.tangem.datasource.local.config.environment.models.ExpressModel -import kotlinx.coroutines.flow.flowOf - -/** - * Mock [EnvironmentConfigStorage] implementation for [ProdApiConfigsManagerTest] - * -[REDACTED_AUTHOR] - */ -internal class MockEnvironmentConfigStorage : EnvironmentConfigStorage { - - private val environmentConfig = EnvironmentConfig( - express = ExpressModel(apiKey = EXPRESS_API_KEY, signVerifierPublicKey = "vocibus"), - devExpress = ExpressModel(apiKey = EXPRESS_DEV_API_KEY, signVerifierPublicKey = "pellentesque"), - blockAidApiKey = BLOCK_AID_API_KEY, - tangemApiKey = TANGEM_API_KEY, - tangemApiKeyDev = TANGEM_API_KEY_DEV, - bffStaticToken = TANGEM_PAY_BFF_KEY, - bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV, - tangemApiKeyStage = TANGEM_API_KEY_STAGE, - yieldModuleApiKey = YIELD_MODULE_KEY, - yieldModuleApiKeyDev = YIELD_MODULE_KEY_DEV, - ) - - override suspend fun initialize() = environmentConfig - override fun getConfig() = flowOf(environmentConfig) - override fun getConfigSync() = environmentConfig - - companion object { - const val EXPRESS_API_KEY = "express_api_key" - const val EXPRESS_DEV_API_KEY = "express_dev_api_key" - const val BLOCK_AID_API_KEY = "block_aid_api_key" - const val TANGEM_API_KEY = "tangem_api_key" - const val TANGEM_API_KEY_DEV = "tangem_api_key_dev" - const val TANGEM_PAY_BFF_KEY = "tangem_pay_bff_key" - const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev" - const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key" - const val TANGEM_API_KEY_STAGE = "tangem_api_key_stage" - const val YIELD_MODULE_KEY = "yield_module_api_key" - const val YIELD_MODULE_KEY_DEV = "yield_module_api_key_dev" - } -} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt index 76572d804f..7b324258f2 100644 --- a/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/api/common/config/managers/ProdApiConfigsManagerTest.kt @@ -10,10 +10,8 @@ import com.tangem.datasource.api.common.config.ApiConfig.Companion.EXTERNAL_BUIL import com.tangem.datasource.api.common.config.ApiConfig.Companion.INTERNAL_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.MOCKED_BUILD_TYPE import com.tangem.datasource.api.common.config.ApiConfig.Companion.RELEASE_BUILD_TYPE -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.BLOCK_AID_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_GASLESS_API_KEY -import com.tangem.datasource.api.common.config.managers.MockEnvironmentConfigStorage.Companion.TANGEM_PAY_BFF_KEY_DEV +import com.tangem.datasource.local.config.environment.EnvironmentConfig +import com.tangem.datasource.local.config.environment.models.ExpressModel import com.tangem.domain.staking.model.ethpool.P2PEthPoolStakingConfig import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.lib.auth.P2PEthPoolAuthProvider @@ -39,7 +37,7 @@ import java.util.TimeZone @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class ProdApiConfigsManagerTest { - private val environmentConfigStorage = MockEnvironmentConfigStorage() + private val environmentConfig = createMockEnvironmentConfig() private val appVersionProvider = mockk() private val expressAuthProvider = mockk() private val stakeKitAuthProvider = mockk() @@ -94,7 +92,7 @@ internal class ProdApiConfigsManagerTest { when (it) { ApiConfig.ID.Express -> { Express( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, expressAuthProvider = expressAuthProvider, appVersionProvider = appVersionProvider, appInfoProvider = appInfoProvider, @@ -102,7 +100,7 @@ internal class ProdApiConfigsManagerTest { } ApiConfig.ID.YieldSupply -> { YieldSupply( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, authProvider = appAuthProvider, appInfoProvider = appInfoProvider, @@ -117,14 +115,14 @@ internal class ProdApiConfigsManagerTest { } ApiConfig.ID.StakeKit -> StakeKit(stakeKitAuthProvider = stakeKitAuthProvider) ApiConfig.ID.TangemPay -> TangemPay.Bff( + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, - environmentConfigStorage = environmentConfigStorage, ) ApiConfig.ID.TangemPayAuth -> TangemPay.Auth( + environmentConfig = environmentConfig, appVersionProvider = appVersionProvider, - environmentConfigStorage = environmentConfigStorage, ) - ApiConfig.ID.BlockAid -> BlockAid(configStorage = environmentConfigStorage) + ApiConfig.ID.BlockAid -> BlockAid(environmentConfig = environmentConfig) ApiConfig.ID.MoonPay -> MoonPay() ApiConfig.ID.P2PEthPool -> P2PEthPool(p2pAuthProvider = p2pEthPoolAuthProvider) ApiConfig.ID.News -> News( @@ -188,9 +186,9 @@ internal class ProdApiConfigsManagerTest { headers = mapOf( "api-key" to ProviderSuspend { if (environment == ApiEnvironment.PROD) { - MockEnvironmentConfigStorage.EXPRESS_API_KEY + EXPRESS_API_KEY } else { - MockEnvironmentConfigStorage.EXPRESS_DEV_API_KEY + EXPRESS_DEV_API_KEY } }, "session-id" to ProviderSuspend { EXPRESS_SESSION_ID }, @@ -237,7 +235,7 @@ internal class ProdApiConfigsManagerTest { environment = ApiEnvironment.PROD, baseUrl = "https://yield.tangem.org/", headers = mapOf( - "api-key" to ProviderSuspend { MockEnvironmentConfigStorage.YIELD_MODULE_KEY }, + "api-key" to ProviderSuspend { YIELD_MODULE_KEY }, "card_id" to ProviderSuspend { APP_CARD_ID }, "card_public_key" to ProviderSuspend { APP_CARD_PUBLIC_KEY }, "version" to ProviderSuspend { VERSION_NAME }, @@ -426,5 +424,48 @@ internal class ProdApiConfigsManagerTest { const val P2P_API_KEY = "p2p_api_key" const val APP_CARD_ID = "app_card_id" const val APP_CARD_PUBLIC_KEY = "Bearer app_public_key" + + // Mock config values + const val TANGEM_API_KEY = "tangem_api_key" + const val TANGEM_GASLESS_API_KEY = "tangem_gasless_api_key" + const val TANGEM_PAY_BFF_KEY_DEV = "tangem_pay_bff_key_dev" + const val BLOCK_AID_API_KEY = "block_aid_api_key" + const val EXPRESS_API_KEY = "express_api_key" + const val EXPRESS_DEV_API_KEY = "express_dev_api_key" + const val YIELD_MODULE_KEY = "yield_module_key" + + fun createMockEnvironmentConfig(): EnvironmentConfig { + return EnvironmentConfig( + moonPayApiKey = "moon_pay_api_key", + moonPayApiSecretKey = "moon_pay_secret_key", + mercuryoWidgetId = "mercuryo_widget_id", + mercuryoSecret = "mercuryo_secret", + blockchainSdkConfig = mockk(relaxed = true), + amplitudeApiKey = "amplitude_api_key", + appsFlyerApiKey = "appsflyer_api_key", + appsAppId = "apps_app_id", + walletConnectProjectId = "wallet_connect_project_id", + express = ExpressModel( + apiKey = EXPRESS_API_KEY, + signVerifierPublicKey = "express_public_key", + ), + devExpress = ExpressModel( + apiKey = EXPRESS_DEV_API_KEY, + signVerifierPublicKey = "express_dev_public_key", + ), + stakeKitApiKey = STAKE_KIT_API_KEY, + p2pApiKey = null, + blockAidApiKey = BLOCK_AID_API_KEY, + tangemApiKey = TANGEM_API_KEY, + tangemApiKeyDev = TANGEM_API_KEY, + tangemApiKeyStage = TANGEM_API_KEY, + yieldModuleApiKey = YIELD_MODULE_KEY, + yieldModuleApiKeyDev = YIELD_MODULE_KEY, + bffStaticToken = TANGEM_PAY_BFF_KEY_DEV, + bffStaticTokenDev = TANGEM_PAY_BFF_KEY_DEV, + gaslessTxApiKeyDev = TANGEM_GASLESS_API_KEY, + gaslessTxApiKey = TANGEM_GASLESS_API_KEY, + ) + } } } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 88a6e8f021..156a72544e 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -18,7 +18,7 @@ import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore @@ -131,11 +131,11 @@ internal object OnrampDataModule { @Provides @Singleton fun provideMercuryoRepository( - environmentConfigStorage: EnvironmentConfigStorage, + environmentConfig: EnvironmentConfig, dispatchersProvider: CoroutineDispatcherProvider, ): LegacyTopUpRepository { return MercuryoTopUpRepository( - environmentConfigStorage = environmentConfigStorage, + environmentConfig = environmentConfig, dispatchersProvider = dispatchersProvider, ) } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt index a2b80fec76..9eaee443e1 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/legacy/MercuryoTopUpRepository.kt @@ -5,7 +5,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.extensions.calculateSha512 import com.tangem.common.extensions.toHexString import com.tangem.datasource.local.config.environment.EnvironmentConfig -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.onramp.repositories.LegacyTopUpRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -13,14 +12,13 @@ import kotlinx.coroutines.withContext import javax.inject.Inject internal class MercuryoTopUpRepository @Inject constructor( - private val environmentConfigStorage: EnvironmentConfigStorage, + private val environmentConfig: EnvironmentConfig, private val dispatchersProvider: CoroutineDispatcherProvider, ) : LegacyTopUpRepository { override suspend fun getTopUpUrl(cryptoCurrency: CryptoCurrency, walletAddress: String): String = withContext(dispatchersProvider.default) { val blockchain = cryptoCurrency.network.toBlockchain() - val environmentConfig = environmentConfigStorage.getConfigSync() val builder = Uri.Builder() .scheme(LegacyTopUpRepository.SCHEME) diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt index f4ca59e975..a812c22529 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/DefaultBlockchainSDKFactory.kt @@ -1,8 +1,8 @@ package com.tangem.blockchainsdk +import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchainsdk.providers.BlockchainProvidersTypesManager -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage import com.tangem.datasource.local.config.providers.models.ProviderModel import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.CoroutineScope @@ -15,16 +15,16 @@ internal typealias BlockchainProvidersResponse = Map /** * Implementation of Blockchain SDK components factory * + * @property blockchainSdkConfig blockchain SDK config * @property blockchainProvidersTypesManager blockchain providers types manager - * @property environmentConfigStorage environment config storage * @property walletManagerFactoryCreator wallet manager factory creator * @param dispatchers coroutine dispatchers provider * [REDACTED_AUTHOR] */ internal class DefaultBlockchainSDKFactory( + private val blockchainSdkConfig: BlockchainSdkConfig, private val blockchainProvidersTypesManager: BlockchainProvidersTypesManager, - private val environmentConfigStorage: EnvironmentConfigStorage, private val walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ) : BlockchainSDKFactory { @@ -43,7 +43,7 @@ internal class DefaultBlockchainSDKFactory( private fun createWalletManagerFactory(): Flow { return combine( - flow = environmentConfigStorage.getConfig().map { it.blockchainSdkConfig }, + flow = flowOf(blockchainSdkConfig), flow2 = blockchainProvidersTypesManager.get(), // flow3 = subscribe on feature toggles changes, TODO: [REDACTED_JIRA] transform = walletManagerFactoryCreator::create, diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt index cce9a8c942..a8c5f4b3f5 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/di/BlockchainSDKFactoryModule.kt @@ -19,7 +19,7 @@ import com.tangem.blockchainsdk.providers.ProdBlockchainProvidersTypesManager import com.tangem.blockchainsdk.providers.dev.BlockchainProvidersResponseSerializer import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi -import com.tangem.datasource.local.config.environment.EnvironmentConfigStorage +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.libs.blockchain_sdk.BuildConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -39,14 +39,14 @@ internal object BlockchainSDKFactoryModule { @Provides @Singleton fun provideBlockchainSDKFactory( + environmentConfig: EnvironmentConfig, blockchainProvidersTypesManager: BlockchainProvidersTypesManager, - environmentConfigStorage: EnvironmentConfigStorage, walletManagerFactoryCreator: WalletManagerFactoryCreator, dispatchers: CoroutineDispatcherProvider, ): BlockchainSDKFactory { return DefaultBlockchainSDKFactory( + blockchainSdkConfig = environmentConfig.blockchainSdkConfig, blockchainProvidersTypesManager = blockchainProvidersTypesManager, - environmentConfigStorage = environmentConfigStorage, walletManagerFactoryCreator = walletManagerFactoryCreator, dispatchers = dispatchers, ) From 3b86b97c12e39068946d3349cf1212923a8096ac Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 11:40:46 +0000 Subject: [PATCH 50/97] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 423909e1bc..ae62f1e18f 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -7,7 +7,7 @@ tangemBlockchainSdk = "releases-5.34-1430" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-581" +tangemCardSdk = "develop-578" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From ff5df41d6adfa270486fcd0dee7232e61a370350 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 16:31:29 +0400 Subject: [PATCH 51/97] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 1 - .../tangem/tap/routing/utils/ChildFactory.kt | 52 +- .../tap/routing/utils/DeepLinkFactory.kt | 4 +- .../tap/routing/utils/DeepLinkFactoryTest.kt | 3 - .../configs/feature_toggles_config.json | 4 - .../entry/featuretoggle/FeedFeatureToggle.kt | 1 - .../featuretoggle/DefaultFeedFeatureToggle.kt | 3 - .../details/MarketsTokenDetailsComponent.kt | 40 -- .../markets/entry/MarketsEntryComponent.kt | 24 - .../DefaultMarketsTokenDetailsComponent.kt | 164 ----- .../analytics/MarketDetailsAnalyticsEvent.kt | 84 --- .../details/impl/di/ComponentModule.kt | 20 - .../markets/details/impl/di/ModelModule.kt | 20 - .../impl/model/MarketsTokenDetailsModel.kt | 645 ------------------ .../model/converters/DescriptionConverter.kt | 45 -- .../converters/ExchangeItemStateConverter.kt | 68 -- .../model/converters/InsightsConverter.kt | 168 ----- .../impl/model/converters/LinksConverter.kt | 49 -- .../impl/model/converters/MetricsConverter.kt | 152 ----- .../converters/PricePerformanceConverter.kt | 71 -- .../converters/SecurityScoreConverter.kt | 56 -- .../converters/TokenMarketInfoConverter.kt | 81 --- .../impl/model/formatter/Formatters.kt | 76 --- .../formatter/MarketsDateTimeFormatters.kt | 140 ---- .../impl/model/state/QuotesStateUpdater.kt | 96 --- .../impl/model/state/TokenNetworksState.kt | 12 - .../impl/ui/MarketsTokenDetailsContent.kt | 351 ---------- .../ui/components/ExchangesBottomSheet.kt | 191 ------ .../impl/ui/components/InfoBottomSheet.kt | 75 -- .../details/impl/ui/components/InfoPoint.kt | 186 ----- .../impl/ui/components/InsightsBlock.kt | 204 ------ .../details/impl/ui/components/LinksBlock.kt | 219 ------ .../impl/ui/components/ListedOnBlock.kt | 138 ---- .../ui/components/MarketTokenDetailsChart.kt | 84 --- .../impl/ui/components/MetricsBlock.kt | 168 ----- .../ui/components/PricePerformanceBlock.kt | 256 ------- .../impl/ui/components/ScoreStarsBlock.kt | 96 --- .../impl/ui/components/SecurityScoreBlock.kt | 134 ---- .../ui/components/SecurityScoreBottomSheet.kt | 190 ------ .../ui/components/TokenMarketDetailsBody.kt | 198 ------ .../ui/preview/MarketsTokenDetailsPreview.kt | 129 ---- .../ui/preview/SecurityScorePreviewData.kt | 60 -- .../ui/state/ExchangesBottomSheetContent.kt | 73 -- .../impl/ui/state/InfoBottomSheetContent.kt | 13 - .../details/impl/ui/state/InfoPointUM.kt | 14 - .../details/impl/ui/state/InsightsUM.kt | 12 - .../markets/details/impl/ui/state/LinksUM.kt | 18 - .../details/impl/ui/state/ListedOnUM.kt | 44 -- .../impl/ui/state/MarketsTokenDetailsUM.kt | 70 -- .../details/impl/ui/state/MetricsUM.kt | 7 - .../impl/ui/state/PricePerformanceUM.kt | 17 - .../state/SecurityScoreBottomSheetContent.kt | 25 - .../details/impl/ui/state/SecurityScoreUM.kt | 10 - .../impl/DefaultMarketsEntryComponent.kt | 102 --- .../entry/impl/MarketsEntryChildFactory.kt | 52 -- .../markets/entry/impl/di/ComponentModule.kt | 18 - .../entry/impl/ui/EntryBottomSheetContent.kt | 128 ---- .../add/api/AddToPortfolioComponent.kt | 18 - .../add/api/AddToPortfolioManager.kt | 38 -- .../portfolio/add/api/AvailableToAddData.kt | 62 -- .../portfolio/add/impl/AddTokenComponent.kt | 55 -- .../add/impl/ChooseNetworkComponent.kt | 45 -- .../impl/DefaultAddToPortfolioComponent.kt | 210 ------ .../add/impl/TokenActionsComponent.kt | 79 --- .../converter/AvailableToAddDataConverter.kt | 119 ---- .../impl/di/AddToPortfolioComponentModule.kt | 21 - .../add/impl/di/AddToPortfolioModelModule.kt | 38 -- .../add/impl/model/AddToPortfolioModel.kt | 381 ----------- .../add/impl/model/AddToPortfolioRoutes.kt | 28 - .../portfolio/add/impl/model/AddTokenModel.kt | 133 ---- .../add/impl/model/AddTokenUiBuilder.kt | 107 --- .../model/CheckCurrencyUnsupportedDelegate.kt | 77 --- .../add/impl/model/ChooseNetworkModel.kt | 60 -- .../add/impl/model/TokenActionsModel.kt | 76 --- .../add/impl/model/TokenActionsUiBuilder.kt | 44 -- .../add/impl/ui/ChooseNetworkContent.kt | 110 --- .../impl/ui/DefaultAddToPortfolioManager.kt | 73 -- .../add/impl/ui/TokenActionsContent.kt | 204 ------ .../add/impl/ui/state/ChooseNetworkUM.kt | 9 - .../add/impl/ui/state/TokenActionsUM.kt | 10 - .../api/MarketsPortfolioComponent.kt | 29 - .../impl/DefaultMarketsPortfolioComponent.kt | 90 --- .../impl/analytics/PortfolioAnalyticsEvent.kt | 74 -- .../portfolio/impl/di/ComponentModule.kt | 20 - .../markets/portfolio/impl/di/ModelModule.kt | 20 - .../portfolio/impl/loader/PortfolioData.kt | 33 - .../impl/loader/PortfolioDataLoader.kt | 135 ---- .../model/AddToPortfolioBSContentUMFactory.kt | 161 ----- .../impl/model/AddToPortfolioManager.kt | 190 ------ .../impl/model/BlockchainRowUMConverter.kt | 66 -- .../impl/model/MarketsPortfolioModel.kt | 426 ------------ .../impl/model/MarketsPortfolioRoute.kt | 17 - .../impl/model/MyPortfolioUMFactory.kt | 150 ---- .../impl/model/NewMarketsPortfolioDelegate.kt | 351 ---------- .../impl/model/PortfolioBSVisibilityModel.kt | 14 - .../impl/model/PortfolioTokenUMConverter.kt | 126 ---- .../portfolio/impl/model/PortfolioUIData.kt | 20 - .../impl/model/SelectNetworkUMConverter.kt | 37 - .../impl/model/TokenActionsHandler.kt | 178 ----- .../impl/model/TokensPortfolioUMConverter.kt | 113 --- .../impl/ui/AddToPortfolioBottomSheet.kt | 383 ----------- .../markets/portfolio/impl/ui/MyPortfolio.kt | 339 --------- .../portfolio/impl/ui/PortfolioItem.kt | 155 ----- .../impl/ui/PortfolioQuickActions.kt | 249 ------- .../impl/ui/TokenActionsBottomSheet.kt | 86 --- .../impl/ui/WalletSelectorBottomSheet.kt | 140 ---- .../PreviewAddToPortfolioBSContentProvider.kt | 86 --- .../preview/PreviewMyPortfolioUMProvider.kt | 147 ---- .../ui/state/AddToPortfolioBSContentUM.kt | 15 - .../portfolio/impl/ui/state/MyPortfolioUM.kt | 51 -- .../impl/ui/state/PortfolioTokenUM.kt | 39 -- .../portfolio/impl/ui/state/QuickActionUM.kt | 51 -- .../impl/ui/state/SelectNetworkUM.kt | 13 - .../impl/ui/state/TokenActionsBSContentUM.kt | 58 -- .../ui/state/WalletSelectorBSContentUM.kt | 10 - .../block/impl/model/formatter/Formatters.kt | 12 + .../token/block/impl/ui/TokenMarketBlock.kt | 2 +- .../wallet/child/wallet/WalletComponent.kt | 25 +- .../wallet/child/wallet/model/WalletModel.kt | 9 - .../common/preview/WalletScreenPreviewData.kt | 1 - .../wallet/state/WalletStateController.kt | 1 - .../wallet/state/model/WalletScreenState.kt | 1 - .../presentation/wallet/ui/WalletScreen.kt | 6 +- .../presentation/wallet/ui/WalletScreen2.kt | 6 +- 124 files changed, 36 insertions(+), 11457 deletions(-) delete mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt delete mode 100644 features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt delete mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt create mode 100644 features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index ca62baa8b2..08985168ae 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -148,7 +148,6 @@ abstract class BaseTestCase : TestCase( "SWAP_REDESIGN_ENABLED" to false, "HOT_WALLET_ENABLED" to true, "ACCOUNTS_FEATURE_ENABLED" to true, - "FEED_ENABLED" to true, "GASLESS_TRANSACTIONS_ENABLED" to true, ) ) 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 aa9b5efd77..631b5f6ba3 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 @@ -18,7 +18,6 @@ import com.tangem.features.details.component.DetailsComponent import com.tangem.features.disclaimer.api.components.DisclaimerComponent import com.tangem.features.feed.entry.components.FeedEntryComponent import com.tangem.features.feed.entry.components.FeedEntryRoute -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.home.api.HomeComponent import com.tangem.features.hotwallet.* import com.tangem.features.kyc.KycComponent @@ -26,7 +25,6 @@ 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 import com.tangem.features.nft.component.NFTComponent import com.tangem.features.onboarding.v2.entry.OnboardingEntryComponent @@ -67,7 +65,6 @@ internal class ChildFactory @Inject constructor( private val walletHardwareBackupComponentFactory: WalletHardwareBackupComponent.Factory, private val disclaimerComponentFactory: DisclaimerComponent.Factory, private val manageTokensComponentFactory: ManageTokensComponent.Factory, - private val marketsTokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, private val marketsTokenListComponentFactory: MarketsTokenListComponent.FactoryScreen, private val onrampComponentFactory: OnrampComponent.Factory, private val onrampSuccessComponentFactory: OnrampSuccessComponent.Factory, @@ -117,7 +114,6 @@ internal class ChildFactory @Inject constructor( private val kycComponentFactory: KycComponent.Factory, private val yieldSupplyEntryComponentFactory: YieldSupplyEntryComponent.Factory, private val feedEntryComponentFactory: FeedEntryComponent.Factory, - private val feedFeatureToggle: FeedFeatureToggle, ) { @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -193,39 +189,21 @@ internal class ChildFactory @Inject constructor( ) } is AppRoute.MarketsTokenDetails -> { - if (feedFeatureToggle.isFeedEnabled) { - createComponentChild( - context = context, - params = FeedEntryRoute.MarketTokenDetails( - token = route.token, - appCurrency = route.appCurrency, - shouldShowPortfolio = route.shouldShowPortfolio, - analyticsParams = route.analyticsParams?.let { params -> - FeedEntryRoute.MarketTokenDetails.AnalyticsParams( - blockchain = params.blockchain, - source = params.source, - ) - }, - ), - componentFactory = feedEntryComponentFactory, - ) - } else { - createComponentChild( - context = context, - params = MarketsTokenDetailsComponent.Params( - token = route.token, - appCurrency = route.appCurrency, - shouldShowPortfolio = route.shouldShowPortfolio, - analyticsParams = route.analyticsParams?.let { params -> - MarketsTokenDetailsComponent.AnalyticsParams( - blockchain = params.blockchain, - source = params.source, - ) - }, - ), - componentFactory = marketsTokenDetailsComponentFactory, - ) - } + createComponentChild( + context = context, + params = FeedEntryRoute.MarketTokenDetails( + token = route.token, + appCurrency = route.appCurrency, + shouldShowPortfolio = route.shouldShowPortfolio, + analyticsParams = route.analyticsParams?.let { params -> + FeedEntryRoute.MarketTokenDetails.AnalyticsParams( + blockchain = params.blockchain, + source = params.source, + ) + }, + ), + componentFactory = feedEntryComponentFactory, + ) } is AppRoute.Onramp -> { createComponentChild( diff --git a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt index 27c065e869..4bc37fc48e 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/DeepLinkFactory.kt @@ -7,7 +7,6 @@ import com.tangem.common.routing.DeepLinkScheme import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler @@ -53,7 +52,6 @@ internal class DeepLinkFactory @Inject constructor( private val promoDeepLink: PromoDeeplinkHandler.Factory, private val onboardVisaDeepLink: OnboardVisaDeepLinkHandler.Factory, private val newsDetailsDeepLink: NewsDetailsDeepLinkHandler.Factory, - private val feedFeatureToggle: FeedFeatureToggle, ) { private val permittedAppRoute = MutableStateFlow(false) @@ -127,7 +125,7 @@ internal class DeepLinkFactory @Inject constructor( onboardVisaDeepLink.create(deeplinkUri) return } - deeplinkUri.path?.startsWith("/news") == true && feedFeatureToggle.isFeedEnabled -> { + deeplinkUri.path?.startsWith("/news") == true -> { newsDetailsDeepLink.create(coroutineScope, deeplinkUri) return } diff --git a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt index 19782fca91..4d40266dc8 100644 --- a/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt +++ b/app/src/test/kotlin/com/tangem/tap/routing/utils/DeepLinkFactoryTest.kt @@ -5,7 +5,6 @@ import com.tangem.common.routing.AppRoute import com.tangem.data.card.sdk.CardSdkProvider import com.tangem.feature.referral.api.deeplink.ReferralDeepLinkHandler import com.tangem.features.feed.entry.deeplink.NewsDetailsDeepLinkHandler -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.markets.deeplink.MarketsDeepLinkHandler import com.tangem.features.markets.deeplink.MarketsTokenDetailDeepLinkHandler import com.tangem.features.onramp.deeplink.BuyDeepLinkHandler @@ -87,7 +86,6 @@ class DeepLinkFactoryTest { private val newsDeeplink = mockk(relaxed = true) { every { create(any(), any()) } returns mockk() } - private val feedFeatureToggle = mockk() private val mockedUri = mockk(relaxed = true) private val isFromOnNewIntent: Boolean = false @@ -112,7 +110,6 @@ class DeepLinkFactoryTest { promoDeepLink = promoDeepLinkFactory, onboardVisaDeepLink = onboardVisaDeepLink, newsDetailsDeepLink = newsDeeplink, - feedFeatureToggle = feedFeatureToggle, ) @OptIn(ExperimentalCoroutinesApi::class) diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 21f4ed8abd..f79453a6f5 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -28,10 +28,6 @@ "name": "ACCOUNTS_FEATURE_ENABLED", "version": "5.33.0" }, - { - "name": "FEED_ENABLED", - "version": "5.33.0" - }, { "name": "APP_REDESIGN_ENABLED", "version": "undefined" diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt index 72d5656bad..1513ec7932 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/featuretoggle/FeedFeatureToggle.kt @@ -1,6 +1,5 @@ package com.tangem.features.feed.entry.featuretoggle interface FeedFeatureToggle { - val isFeedEnabled: Boolean val isEarnBlockEnabled: Boolean } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt index 38b3458572..3e15b106b0 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/featuretoggle/DefaultFeedFeatureToggle.kt @@ -7,9 +7,6 @@ internal class DefaultFeedFeatureToggle( private val featureTogglesManager: FeatureTogglesManager, ) : FeedFeatureToggle { - override val isFeedEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("FEED_ENABLED") - override val isEarnBlockEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled("EARN_BLOCK_ENABLED") } \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt deleted file mode 100644 index 0136e2532f..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/details/MarketsTokenDetailsComponent.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.tangem.features.markets.details - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import kotlinx.serialization.Serializable - -@Stable -interface MarketsTokenDetailsComponent : ComposableContentComponent { - - @Serializable - data class Params( - val token: TokenMarketParams, - val appCurrency: AppCurrency, - val shouldShowPortfolio: Boolean, - val analyticsParams: AnalyticsParams?, - ) - - @Serializable - data class AnalyticsParams( - val blockchain: String?, - val source: String, - ) - - @Composable - fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt b/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt deleted file mode 100644 index 05a7e68670..0000000000 --- a/features/markets/api/src/main/kotlin/com/tangem/features/markets/entry/MarketsEntryComponent.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.tangem.features.markets.entry - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState - -@Stable -interface MarketsEntryComponent { - - @Composable - fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) - - interface Factory { - fun create(context: AppComponentContext): MarketsEntryComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt deleted file mode 100644 index e6f564100f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/DefaultMarketsTokenDetailsComponent.kt +++ /dev/null @@ -1,164 +0,0 @@ -package com.tangem.features.markets.details.impl - -import androidx.activity.compose.BackHandler -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.lifecycle.compose.LifecycleStartEffect -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network -import com.tangem.core.analytics.api.AnalyticsEventHandler -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.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.MarketsTokenDetailsComponent.Params -import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent -import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel -import com.tangem.features.markets.details.impl.model.state.TokenNetworksState -import com.tangem.features.markets.details.impl.ui.MarketsTokenDetailsContent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch - -@Stable -internal class DefaultMarketsTokenDetailsComponent @AssistedInject constructor( - @Assisted appComponentContext: AppComponentContext, - @Assisted params: Params, - analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, - portfolioComponentFactory: MarketsPortfolioComponent.Factory, -) : AppComponentContext by appComponentContext, MarketsTokenDetailsComponent { - - // applying l2 compatibility - private val updatedParams = params.copy( - token = params.token.copy( - id = CryptoCurrency.RawID(getTokenIdIfL2Network(params.token.id.value)), - ), - ) - private val analyticsParams = params.analyticsParams - - private val model: MarketsTokenDetailsModel = getOrCreateModel(updatedParams) - - private val portfolioComponent: MarketsPortfolioComponent? = if (updatedParams.shouldShowPortfolio) { - portfolioComponentFactory.create( - context = child("my_portfolio"), - params = MarketsPortfolioComponent.Params( - updatedParams.token, - analyticsParams = analyticsParams?.source?.let { MarketsPortfolioComponent.AnalyticsParams(it) }, - ), - ) - } else { - null - } - - init { - componentScope.launch { - model.networksState.collectLatest { networksState -> - when (networksState) { - is TokenNetworksState.NetworksAvailable -> portfolioComponent?.setTokenNetworks( - networksState.networks, - ) - TokenNetworksState.NoNetworksAvailable -> portfolioComponent?.setNoNetworksAvailable() - else -> {} - } - } - } - - // === Analytics === - if (analyticsParams != null) { - analyticsEventHandler.send( - MarketDetailsAnalyticsEvent.EventBuilder( - token = params.token, - ).screenOpened( - blockchain = analyticsParams.blockchain, - source = analyticsParams.source, - ), - ) - } - } - - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - LifecycleStartEffect(Unit) { - model.isVisibleOnScreen.value = true - onStopOrDispose { - model.isVisibleOnScreen.value = false - } - } - - val state by model.state.collectAsStateWithLifecycle() - val bsState by bottomSheetState - - LaunchedEffect(bsState) { - model.isVisibleOnScreen.value = bsState == BottomSheetState.EXPANDED - } - - BackHandler(enabled = bsState == BottomSheetState.EXPANDED) { - navigateBack() - } - - MarketsTokenDetailsContent( - modifier = modifier, - backgroundColor = LocalMainBottomSheetColor.current.value, - addTopBarStatusBarPadding = false, - state = state, - onBackClick = ::navigateBack, - backButtonEnabled = bsState == BottomSheetState.EXPANDED, - onHeaderSizeChange = onHeaderSizeChange, - isAccountEnabled = accountsFeatureToggles.isFeatureEnabled, - portfolioBlock = portfolioComponent?.let { component -> - { blockModifier -> - component.Content(blockModifier) - } - }, - ) - } - - @Composable - override fun Content(modifier: Modifier) { - LifecycleStartEffect(Unit) { - model.isVisibleOnScreen.value = true - onStopOrDispose { - model.isVisibleOnScreen.value = false - } - } - - val state by model.state.collectAsStateWithLifecycle() - - MarketsTokenDetailsContent( - modifier = modifier, - backgroundColor = TangemTheme.colors.background.tertiary, - addTopBarStatusBarPadding = true, - state = state, - onBackClick = ::navigateBack, - backButtonEnabled = true, - onHeaderSizeChange = {}, - isAccountEnabled = accountsFeatureToggles.isFeatureEnabled, - portfolioBlock = portfolioComponent?.let { component -> - { blockModifier -> - component.Content(blockModifier) - } - }, - ) - } - - private fun navigateBack() = router.pop() - - @AssistedFactory - interface Factory : MarketsTokenDetailsComponent.Factory { - override fun create(context: AppComponentContext, params: Params): DefaultMarketsTokenDetailsComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt deleted file mode 100644 index c3674eb4cc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/analytics/MarketDetailsAnalyticsEvent.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.features.markets.details.impl.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketParams - -internal class MarketDetailsAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { - - data class EventBuilder( - val token: TokenMarketParams, - ) { - fun screenOpened(blockchain: String?, source: String) = MarketDetailsAnalyticsEvent( - event = "Token Chart Screen Opened", - params = buildMap { - put("Token", token.symbol) - blockchain?.let { put("blockchain", it) } - put("Source", source) - }, - ) - - fun intervalChanged(intervalType: IntervalType, interval: PriceChangeInterval) = MarketDetailsAnalyticsEvent( - event = "Button - Period", - params = mapOf( - "Token" to token.symbol, - "Period" to interval.toAnalyticsString(), - "Source" to intervalType.source, - ), - ) - - fun readMoreClicked() = MarketDetailsAnalyticsEvent( - event = "Button - Read More", - params = mapOf( - "Token" to token.symbol, - ), - ) - - fun linkClicked(linkTitle: String) = MarketDetailsAnalyticsEvent( - event = "Button - Links", - params = mapOf( - "Token" to token.symbol, - "Link" to linkTitle, - ), - ) - - fun exchangesScreenOpened() = MarketDetailsAnalyticsEvent( - event = "Exchanges Screen Opened", - params = mapOf( - "Token" to token.symbol, - ), - ) - - fun securityScoreOpened() = MarketDetailsAnalyticsEvent( - event = "Security Score Info", - params = mapOf("Token" to token.symbol), - ) - - fun securityScoreProviderClicked(provider: String) = MarketDetailsAnalyticsEvent( - event = "Security Score Provider Clicked", - params = mapOf( - "Token" to token.symbol, - "Provider" to provider, - ), - ) - } - - enum class IntervalType(val source: String) { - Chart("Chart"), - PricePerformance("Price"), - Insights("Insights"), - } -} - -private fun PriceChangeInterval.toAnalyticsString() = when (this) { - PriceChangeInterval.H24 -> "24h" - PriceChangeInterval.WEEK -> "7d" - PriceChangeInterval.MONTH -> "1m" - PriceChangeInterval.MONTH3 -> "3m" - PriceChangeInterval.MONTH6 -> "6m" - PriceChangeInterval.YEAR -> "1y" - PriceChangeInterval.ALL_TIME -> "All" -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt deleted file mode 100644 index 8ef6de84d7..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ComponentModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.details.impl.di - -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.impl.DefaultMarketsTokenDetailsComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface ComponentModule { - - @Binds - @Singleton - fun bindMarketsTokenDetailsComponent( - factory: DefaultMarketsTokenDetailsComponent.Factory, - ): MarketsTokenDetailsComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt deleted file mode 100644 index 2fda5dee58..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/di/ModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.details.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.details.impl.model.MarketsTokenDetailsModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface ModelModule { - - @Binds - @IntoMap - @ClassKey(MarketsTokenDetailsModel::class) - fun provideMarketsTokenDetailsModel(model: MarketsTokenDetailsModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt deleted file mode 100644 index f2a44120b9..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/MarketsTokenDetailsModel.kt +++ /dev/null @@ -1,645 +0,0 @@ -package com.tangem.features.markets.details.impl.model - -import androidx.compose.runtime.Stable -import arrow.core.Either -import arrow.core.getOrElse -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.common.ui.charts.state.MarketChartData -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.common.ui.charts.state.sorted -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.navigation.url.UrlOpener -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains -import com.tangem.domain.feedback.SendFeedbackEmailUseCase -import com.tangem.domain.feedback.models.FeedbackEmailType -import com.tangem.domain.markets.* -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.settings.usercountry.GetUserCountryUseCase -import com.tangem.domain.settings.usercountry.models.UserCountry -import com.tangem.domain.settings.usercountry.models.needApplyFCARestrictions -import com.tangem.domain.wallets.usecase.GetWalletsUseCase -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.details.impl.analytics.MarketDetailsAnalyticsEvent -import com.tangem.features.markets.details.impl.model.converters.DescriptionConverter -import com.tangem.features.markets.details.impl.model.converters.ExchangeItemStateConverter -import com.tangem.features.markets.details.impl.model.converters.TokenMarketInfoConverter -import com.tangem.features.markets.details.impl.model.formatter.* -import com.tangem.features.markets.details.impl.model.state.QuotesStateUpdater -import com.tangem.features.markets.details.impl.model.state.TokenNetworksState -import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import org.joda.time.DateTime -import java.math.BigDecimal -import java.util.Locale -import javax.inject.Inject - -@Suppress("LargeClass", "LongParameterList") -@Stable -@ModelScoped -internal class MarketsTokenDetailsModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, - private val getTokenMarketInfoUseCase: GetTokenMarketInfoUseCase, - private val getTokenFullQuotesUseCase: GetTokenFullQuotesUseCase, - private val getTokenExchangesUseCase: GetTokenExchangesUseCase, - private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val urlOpener: UrlOpener, - private val analyticsEventHandler: AnalyticsEventHandler, - private val excludedBlockchains: ExcludedBlockchains, - private val getUserCountryUseCase: GetUserCountryUseCase, - private val getUserWalletsUseCase: GetWalletsUseCase, -) : Model() { - - private val quotesJob = JobHolder() - private var userCountry: UserCountry? = null - private val params = paramsContainer.require() - private val analyticsEventBuilder = MarketDetailsAnalyticsEvent.EventBuilder(token = params.token) - - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - }.stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = params.appCurrency, - ) - - private val infoConverter = TokenMarketInfoConverter( - appCurrency = Provider { currentAppCurrency.value }, - onInfoClick = { showBottomSheet(it) }, - onListedOnClick = ::onListedOnClick, - onLinkClick = { link -> - urlOpener.openUrl(link.url) - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.linkClicked(linkTitle = link.title)) - }, - onSecurityScoreInfoClick = { content -> - showBottomSheet(content) - - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.securityScoreOpened()) - }, - onSecurityScoreProviderLinkClick = { provider -> - provider.urlData?.fullUrl?.let { url -> - urlOpener.openUrl(url) - } - - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.securityScoreProviderClicked(provider.name)) - }, - // === Analytics === - onPricePerformanceIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance, - interval = interval, - ), - ) - }, - onInsightsIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.Insights, - interval = interval, - ), - ) - }, - needApplyFCARestrictions = Provider { - userCountry.needApplyFCARestrictions() - }, - // ================== - ) - - private val descriptionConverter = DescriptionConverter( - onReadModeClicked = { content -> - showBottomSheet(content) - // === Analytics === - analyticsEventHandler.send(analyticsEventBuilder.readMoreClicked()) - }, - needApplyFCARestrictions = Provider { - userCountry.needApplyFCARestrictions() - }, - onGeneratedAINotificationClick = { - modelScope.launch { - sendFeedbackEmailUseCase( - type = FeedbackEmailType.CurrencyDescriptionError( - currencyId = params.token.id.value, - currencyName = params.token.name, - ), - ) - } - }, - ) - - private val chartDataProducer = MarketChartDataProducer.build(dispatcher = dispatchers.default) { - chartData = MarketChartData.NoData.Loading - - updateLook { currentLook -> - val percentChangeType = params.token.tokenQuotes.h24Percent.percentChangeType() - - currentLook.copy( - type = percentChangeType.toChartType(), - xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(PriceChangeInterval.H24), - yAxisFormatter = { value -> - value.format { - fiat( - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ).price() - } - }, - ) - } - } - - private val currentQuotes = MutableStateFlow( - TokenQuotes( - currentPrice = params.token.tokenQuotes.currentPrice, - h24ChangePercent = params.token.tokenQuotes.h24Percent, - weekChangePercent = params.token.tokenQuotes.weekPercent, - monthChangePercent = params.token.tokenQuotes.monthPercent, - m3ChangePercent = null, - m6ChangePercent = null, - yearChangePercent = null, - allTimeChangePercent = null, - ), - ) - - private val currentTokenInfo = MutableStateFlow(null) - private val lastUpdatedTimestamp = MutableStateFlow(DateTime.now().millis) - - val isVisibleOnScreen = MutableStateFlow(false) - val networksState = MutableStateFlow(TokenNetworksState.Loading) - - val state = MutableStateFlow( - MarketsTokenDetailsUM( - tokenName = params.token.name, - priceText = params.token.tokenQuotes.currentPrice.format { - fiat( - fiatCurrencyCode = currentAppCurrency.value.code, - fiatCurrencySymbol = currentAppCurrency.value.symbol, - ).price() - }, - dateTimeText = resourceReference(R.string.common_today), - priceChangePercentText = params.token.tokenQuotes.h24Percent?.format { percent() }, - priceChangeType = params.token.tokenQuotes.h24Percent.percentChangeType(), - iconUrl = params.token.imageUrl, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = chartDataProducer, - onLoadRetryClick = ::onLoadRetryClicked, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = ::onMarkerPointSelected, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = ::onSelectedIntervalChange, - isMarkerSet = false, - body = MarketsTokenDetailsUM.Body.Loading, - triggerPriceChange = consumedEvent(), - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - ), - ) - - private val quotesStateUpdater = QuotesStateUpdater( - currentAppCurrency = Provider { currentAppCurrency.value }, - state = state, - currentQuotes = currentQuotes, - lastUpdatedTimestamp = lastUpdatedTimestamp, - currentTokenInfo = currentTokenInfo, - onPricePerformanceIntervalChanged = { interval -> - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.PricePerformance, - interval = interval, - ), - ) - }, - ) - - private val loadChartJobHolder = JobHolder() - - init { - userCountry = getUserCountryUseCase.invokeSync().getOrNull() - ?: UserCountry.Other(Locale.getDefault().country) - // reload screen if currency changed - modelScope.launch { - currentAppCurrency - .filter { it != params.appCurrency } - .collectLatest { _ -> - initialLoad() - } - } - - initialLoad() - } - - private fun initialLoad() { - loadInfo() - loadChart(state.value.selectedInterval) - modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS) - } - - private fun loadQuotes() { - modelScope.launch { - val result = getTokenFullQuotesUseCase( - tokenId = params.token.id, - appCurrency = currentAppCurrency.value, - tokenSymbol = params.token.symbol, - ) - - result.onRight { res -> - updateQuotes(res) - } - } - } - - private fun loadChart(interval: PriceChangeInterval) { - modelScope.launch { - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - ), - ) - } - - chartDataProducer.runTransactionSuspend { - chartData = MarketChartData.NoData.Loading - } - - val chart = getTokenPriceChartUseCase.invoke( - appCurrency = currentAppCurrency.value, - interval = interval, - tokenId = params.token.id, - tokenSymbol = params.token.symbol, - preview = false, - ) - - state.update { currentState -> - currentState.copy( - selectedInterval = interval, - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - ), - ) - } - - chart - .onRight { updateTokenChart(it) } - .onLeft { - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.ERROR, - ), - body = if (currentState.body is MarketsTokenDetailsUM.Body.Error) { - MarketsTokenDetailsUM.Body.Nothing - } else { - currentState.body - }, - ) - } - } - }.saveIn(loadChartJobHolder) - } - - private suspend fun updateTokenChart(tokenChart: TokenChart) { - val xAxisFormatter = MarketsDateTimeFormatters.getChartXFormatterByInterval(state.value.selectedInterval) - - chartDataProducer.runTransactionSuspend { - chartData = MarketChartData.Data( - y = tokenChart.priceY.toImmutableList(), - x = tokenChart.timeStamps.map { it.toBigDecimal() }.toImmutableList(), - ).sorted() - - updateLook { currentLook -> - currentLook.copy( - xAxisFormatter = xAxisFormatter, - type = state.value.priceChangeType.toChartType(), - ) - } - } - - state.update { currentState -> - currentState.copy( - chartState = currentState.chartState.copy( - status = MarketsTokenDetailsUM.ChartState.Status.DATA, - ), - body = if (currentState.body is MarketsTokenDetailsUM.Body.Nothing) { - MarketsTokenDetailsUM.Body.Error(onLoadRetryClick = ::onLoadRetryClicked) - } else { - currentState.body - }, - ) - } - } - - private fun loadInfo() { - state.update { currentState -> - currentState.copy( - body = MarketsTokenDetailsUM.Body.Loading, - ) - } - - modelScope.launch { - val tokenMarketInfo = getTokenMarketInfoUseCase( - appCurrency = currentAppCurrency.value, - tokenId = params.token.id, - tokenSymbol = params.token.symbol, - ) - - tokenMarketInfo.fold( - ifRight = { result -> updateInfo(result) }, - ifLeft = { - state.update { currentState -> - if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.DATA) { - currentState.copy( - body = MarketsTokenDetailsUM.Body.Error( - onLoadRetryClick = ::onLoadRetryClicked, - ), - ) - } else { - currentState.copy( - body = MarketsTokenDetailsUM.Body.Nothing, - ) - } - } - }, - ) - } - } - - private fun updateInfo(newInfo: TokenMarketInfo) { - lastUpdatedTimestamp.value = DateTime.now().millis - - currentTokenInfo.value = newInfo - currentQuotes.value = newInfo.quotes - - val percent = newInfo.quotes.getPercentByInterval(interval = state.value.selectedInterval) - - state.update { currentState -> - currentState.copy( - priceText = newInfo.quotes.currentPrice.format { - fiat( - fiatCurrencySymbol = currentAppCurrency.value.symbol, - fiatCurrencyCode = currentAppCurrency.value.code, - ).price() - }, - priceChangePercentText = newInfo.quotes.getFormattedPercentByInterval( - interval = currentState.selectedInterval, - ), - priceChangeType = percent.percentChangeType(), - body = MarketsTokenDetailsUM.Body.Content( - description = descriptionConverter.convert(newInfo), - infoBlocks = infoConverter.convert(newInfo), - ), - ) - } - - val areAllWalletsHot = getUserWalletsUseCase.invokeSync().all { it is UserWallet.Hot } - - val networks = newInfo.networks?.filter { network -> - BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, - excludedBlockchains = excludedBlockchains, - hotExcludedBlockchains = hotWalletExcludedBlockchains, - hasOnlyHotWallets = areAllWalletsHot, - ) - } - - networksState.value = if (networks.isNullOrEmpty()) { - TokenNetworksState.NoNetworksAvailable - } else { - TokenNetworksState.NetworksAvailable(networks) - } - - chartDataProducer.runTransaction { - updateLook { currentLook -> - currentLook.copy(type = percent.percentChangeType().toChartType()) - } - } - } - - private suspend fun updateQuotes(newQuotes: TokenQuotes) { - val populatedNewQuotes = currentQuotes.value.populateWith(newQuotes) - - quotesStateUpdater.updateQuotes(newQuotes = populatedNewQuotes) - - val percent = populatedNewQuotes - .getPercentByInterval(interval = state.value.selectedInterval) - - chartDataProducer.runTransaction { - updateLook { - it.copy(type = percent.percentChangeType().toChartType()) - } - } - } - - private fun onSelectedIntervalChange(interval: PriceChangeInterval) { - if (state.value.selectedInterval == interval) return - - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.intervalChanged( - intervalType = MarketDetailsAnalyticsEvent.IntervalType.Chart, - interval = interval, - ), - ) - // ================== - - val quotes = currentQuotes.value - val priceChangePercent = quotes.getFormattedPercentByInterval(interval) - - state.update { currentState -> - currentState.copy( - priceChangePercentText = priceChangePercent, - selectedInterval = interval, - priceChangeType = quotes.getPercentByInterval(interval)?.percentChangeType() - ?: PriceChangeType.NEUTRAL, - dateTimeText = getDefaultDateTimeString(interval), - ) - } - - loadChart(interval) - - if (priceChangePercent.isEmpty()) { - loadQuotes() - } - } - - @Suppress("MagicNumber") - private fun onMarkerPointSelected(markerTimestamp: BigDecimal?, price: BigDecimal?) { - val currentState = state.value - - val dateTimeText = markerTimestamp?.let { timestamp -> - MarketsDateTimeFormatters.formatDateByIntervalWithMarker( - interval = currentState.selectedInterval, - markerTimestamp = timestamp, - ) - } ?: getDefaultDateTimeString(currentState.selectedInterval) - - val priceText = (price ?: currentQuotes.value.currentPrice).format { - fiat( - fiatCurrencySymbol = currentAppCurrency.value.symbol, - fiatCurrencyCode = currentAppCurrency.value.code, - ).price() - } - - val percent = price?.let { selectedPrice -> - getChangePercentBetween( - previousPrice = selectedPrice, - currentPrice = currentQuotes.value.currentPrice, - ) - } ?: currentQuotes.value.getPercentByInterval(currentState.selectedInterval) - - val percentText = percent?.format { percent() }.orEmpty() - - state.update { stateToUpdate -> - stateToUpdate.copy( - isMarkerSet = markerTimestamp != null, - dateTimeText = dateTimeText, - priceText = priceText, - priceChangePercentText = percentText, - priceChangeType = percent.percentChangeType(), - ) - } - - chartDataProducer.runTransaction { - updateLook { currentLook -> - currentLook.copy( - type = percent.percentChangeType().toChartType(), - ) - } - } - } - - private fun showBottomSheet(content: TangemBottomSheetConfigContent) { - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy( - isShown = true, - onDismissRequest = ::hideBottomSheet, - content = content, - ), - ) - } - } - - private fun hideBottomSheet() { - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(isShown = false), - ) - } - } - - private fun onLoadRetryClicked() { - val currentState = state.value - - if (currentState.chartState.status == MarketsTokenDetailsUM.ChartState.Status.ERROR) { - loadChart(currentState.selectedInterval) - } - - if (currentState.body is MarketsTokenDetailsUM.Body.Error || - currentState.body is MarketsTokenDetailsUM.Body.Nothing - ) { - loadInfo() - modelScope.loadQuotesWithTimer(QUOTES_UPDATE_INTERVAL_MILLIS) - } - } - - private fun onListedOnClick(exchangesCount: Int) { - modelScope.launch { - analyticsEventHandler.send(analyticsEventBuilder.exchangesScreenOpened()) - - showBottomSheet(content = ExchangesBottomSheetContent.Loading(exchangesCount)) - - val maybeExchanges = getTokenExchangesUseCase(tokenId = params.token.id) - - // Delay to show the bottom sheet - delay(timeMillis = 400L) - - updateExchangeBSContent(maybeExchanges = maybeExchanges, exchangesCount = exchangesCount) - } - } - - private fun updateExchangeBSContent( - maybeExchanges: Either>, - exchangesCount: Int, - ) { - val content = maybeExchanges - .fold( - ifLeft = { - ExchangesBottomSheetContent.Error(onRetryClick = { onListedOnClick(exchangesCount) }) - }, - ifRight = { exchanges -> - ExchangesBottomSheetContent.Content( - exchangeItems = ExchangeItemStateConverter.convertList(exchanges).toImmutableList(), - ) - }, - ) - - state.update { stateToUpdate -> - stateToUpdate.copy( - bottomSheetConfig = stateToUpdate.bottomSheetConfig.copy(content = content), - ) - } - } - - private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { - launch { - while (true) { - delay(timeMillis) - // Update quotes only when content is visible on the screen - isVisibleOnScreen.first { it } - - loadQuotes() - } - }.saveIn(quotesJob) - } - - private fun getDefaultDateTimeString(interval: PriceChangeInterval): TextReference { - return MarketsDateTimeFormatters.formatDateByInterval( - interval = interval, - startTimestamp = MarketsDateTimeFormatters.getStartTimestampByInterval( - interval = interval, - currentTimestamp = lastUpdatedTimestamp.value, - ), - ) - } - - private companion object { - const val QUOTES_UPDATE_INTERVAL_MILLIS = 60000L - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt deleted file mode 100644 index ae495c8db0..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/DescriptionConverter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -@Stable -internal class DescriptionConverter( - private val onReadModeClicked: (InfoBottomSheetContent) -> Unit, - private val onGeneratedAINotificationClick: () -> Unit, - private val needApplyFCARestrictions: Provider, -) : Converter { - - override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.Description? { - if (needApplyFCARestrictions()) return null - val shortDesc = value.shortDescription ?: return null - return MarketsTokenDetailsUM.Description( - shortDescription = stringReference(shortDesc), - fullDescription = value.fullDescription?.let(::stringReference), - onReadMoreClick = { - onReadModeClicked( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_about_token_title, - wrappedList( - value.name, - ), - ), - body = stringReference(value.fullDescription.orEmpty()), - generatedAINotificationUM = InfoBottomSheetContent.GeneratedAINotificationUM( - onClick = onGeneratedAINotificationClick, - ), - ), - ) - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt deleted file mode 100644 index 096f0d0430..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/ExchangeItemStateConverter.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import com.tangem.core.ui.components.audits.AuditLabelUM -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.markets.TokenMarketExchange -import com.tangem.domain.markets.TokenMarketExchange.TrustScore -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter - -/** - * Converter from [TokenMarketExchange] to [TokenItemState] - * -[REDACTED_AUTHOR] - */ -internal object ExchangeItemStateConverter : Converter { - - override fun convert(value: TokenMarketExchange): TokenItemState { - return TokenItemState.Content( - id = value.id, - iconState = CurrencyIconState.CoinIcon( - url = value.imageUrl, - fallbackResId = R.drawable.ic_alert_24, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value.name)), - fiatAmountState = TokenItemState.FiatAmountState.Content( - text = value.volumeInUsd.format { - fiat( - fiatCurrencyCode = "USD", - fiatCurrencySymbol = "$", - ).price() - }, - ), - subtitleState = TokenItemState.SubtitleState.TextContent( - value = stringReference(value = if (value.isCentralized) "CEX" else "DEX"), - ), - subtitle2State = TokenItemState.Subtitle2State.LabelContent( - auditLabelUM = value.trustScore.toAuditLabelUM(), - ), - onItemClick = null, - onItemLongClick = null, - ) - } - - private fun TrustScore.toAuditLabelUM(): AuditLabelUM { - return when (this) { - TrustScore.Risky -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_risky), - type = AuditLabelUM.Type.Prohibition, - ) - TrustScore.Caution -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_caution), - type = AuditLabelUM.Type.Warning, - ) - TrustScore.Trusted -> AuditLabelUM( - text = resourceReference(id = R.string.markets_token_details_exchange_trust_score_trusted), - type = AuditLabelUM.Type.Permit, - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt deleted file mode 100644 index 65273ee0bb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/InsightsConverter.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.format.bigdecimal.compact -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.rawCompact -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.InsightsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList -import java.math.BigDecimal - -@Stable -internal class InsightsConverter( - private val appCurrency: Provider, - private val onInfoClick: (InfoBottomSheetContent) -> Unit, - private val onIntervalChanged: (PriceChangeInterval) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.Insights): InsightsUM { - return with(value) { - InsightsUM( - h24Info = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.day, - holdersChange = holdersChange?.day, - liquidityChange = liquidityChange?.day, - buyPressureChange = buyPressureChange?.day, - ), - weekInfo = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.week, - holdersChange = holdersChange?.week, - liquidityChange = liquidityChange?.week, - buyPressureChange = buyPressureChange?.week, - ), - monthInfo = createInfoPointList( - experiencedBuyerChange = experiencedBuyerChange?.month, - holdersChange = holdersChange?.month, - liquidityChange = liquidityChange?.month, - buyPressureChange = buyPressureChange?.month, - ), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_insights), - body = resourceReference( - R.string.markets_insights_info_description_message, - wrappedList(value.sourceNetworks.joinToString { it.name }), - ), - ), - ) - }, - onIntervalChanged = onIntervalChanged, - ) - } - } - - private fun createInfoPointList( - experiencedBuyerChange: BigDecimal?, - holdersChange: BigDecimal?, - liquidityChange: BigDecimal?, - buyPressureChange: BigDecimal?, - ): ImmutableList { - return listOfNotNull( - experiencedBuyerChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = experiencedBuyerChange.convertChange(), - change = experiencedBuyerChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_experienced_buyers_full), - body = resourceReference(R.string.markets_token_details_experienced_buyers_description), - ), - ) - }, - ) - }, - buyPressureChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = buyPressureChange.convertChange(isFiatValue = true), - change = buyPressureChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_buy_pressure_full), - body = resourceReference(R.string.markets_token_details_buy_pressure_description), - ), - ) - }, - ) - }, - holdersChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = holdersChange.convertChange(), - change = holdersChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_holders_full), - body = resourceReference(R.string.markets_token_details_holders_description), - ), - ) - }, - ) - }, - liquidityChange?.let { - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = liquidityChange.convertChange(), - change = liquidityChange.changeType(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_liquidity_full), - body = resourceReference(R.string.markets_token_details_liquidity_description), - ), - ) - }, - ) - }, - ).toImmutableList() - } - - private fun BigDecimal.changeType(): InfoPointUM.ChangeType? { - return when { - this > BigDecimal.ZERO -> InfoPointUM.ChangeType.UP - this < BigDecimal.ZERO -> InfoPointUM.ChangeType.DOWN - else -> null - } - } - - private fun BigDecimal.convertChange(isFiatValue: Boolean = false): String { - val value = if (isFiatValue) { - this.abs().format { - val currency = appCurrency() - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).compact() - } - } else { - this.abs().format { - rawCompact() - } - } - - return when { - this > BigDecimal.ZERO -> StringsSigns.PLUS + value - this < BigDecimal.ZERO -> StringsSigns.MINUS + value - this == BigDecimal.ZERO -> value - else -> StringsSigns.DASH_SIGN - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt deleted file mode 100644 index ca676ee2c5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/LinksConverter.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.details.impl.ui.state.LinksUM.Link -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -@Stable -internal class LinksConverter( - private val onLinkClick: (LinksUM.Link) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.Links): LinksUM { - return LinksUM( - officialLinks = value.officialLinks?.map { it.convert() }.orEmpty().toImmutableList(), - social = value.social?.map { it.convert() }.orEmpty().toImmutableList(), - repository = value.repository?.map { it.convert() }.orEmpty().toImmutableList(), - blockchainSite = value.blockchainSite?.map { it.convert() }.orEmpty().toImmutableList(), - onLinkClick = onLinkClick, - ) - } - - private fun TokenMarketInfo.Link.convert(): LinksUM.Link { - return LinksUM.Link( - title = title, - iconRes = getIconById(id), - url = link, - ) - } - - private fun getIconById(id: String?): Int { - return when (id) { - "linkedin" -> R.drawable.ic_linkedin_24 - "discord" -> R.drawable.ic_discord_24 - "youtube" -> R.drawable.ic_youtube_24 - "telegram" -> R.drawable.ic_telegram_24 - "github" -> R.drawable.ic_github_24 - "twitter" -> R.drawable.ic_twitter_24 - "facebook" -> R.drawable.ic_facebook_24 - "reddit" -> R.drawable.ic_reddit_24 - "instagram" -> R.drawable.ic_instagram_24 - "whitepaper" -> R.drawable.ic_doc_24 - else -> R.drawable.ic_arrow_top_right_24 - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt deleted file mode 100644 index c91039839b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/MetricsConverter.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.format.bigdecimal.compact -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.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.MetricsUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.persistentListOf -import java.math.BigDecimal - -@Stable -internal class MetricsConverter( - private val appCurrency: Provider, - private val tokenSymbol: String, - private val onInfoClick: (InfoBottomSheetContent) -> Unit, -) : Converter { - - @Suppress("LongMethod") - override fun convert(value: TokenMarketInfo.Metrics): MetricsUM { - return with(value) { - MetricsUM( - metrics = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_capitalization), - value = marketCap.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_market_capitalization_full, - ), - body = resourceReference( - R.string.markets_token_details_market_capitalization_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_rating), - value = marketRating?.toString() ?: StringsSigns.DASH_SIGN, - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_market_rating_full), - body = resourceReference(R.string.markets_token_details_market_rating_description), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_trading_volume), - value = volume24h.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_trading_volume_full), - body = resourceReference( - R.string.markets_token_details_trading_volume_24h_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), - value = fullyDilutedValuation.formatAmount(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference( - R.string.markets_token_details_fully_diluted_valuation_full, - ), - body = resourceReference( - R.string.markets_token_details_fully_diluted_valuation_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_circulating_supply), - value = circulatingSupply.formatAmount(crypto = true), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_circulating_supply_full), - body = resourceReference( - R.string.markets_token_details_circulating_supply_description, - ), - ), - ) - }, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_max_supply), - value = maxSupply.formatMaxSupply(), - onInfoClick = { - onInfoClick( - InfoBottomSheetContent( - title = resourceReference(R.string.markets_token_details_max_supply_full), - body = resourceReference(R.string.markets_token_details_total_supply_description), - ), - ) - }, - ), - ), - ) - } - } - - private fun BigDecimal?.formatMaxSupply(): String { - when (this) { - null -> return StringsSigns.DASH_SIGN - BigDecimal.ZERO -> return StringsSigns.INFINITY_SIGN - } - - return this.formatAmount(crypto = true) - } - - private fun BigDecimal?.formatAmount(crypto: Boolean = false): String { - if (this == null) return StringsSigns.DASH_SIGN - - return if (crypto) { - format { - crypto( - symbol = tokenSymbol, - decimals = 2, - ).compact() - } - } else { - val currency = appCurrency() - - format { - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).compact() - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt deleted file mode 100644 index c3aa321cb1..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/PricePerformanceConverter.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM -import com.tangem.utils.Provider -import com.tangem.utils.StringsSigns -import java.math.BigDecimal -import java.math.RoundingMode - -@Stable -internal class PricePerformanceConverter( - private val appCurrency: Provider, - private val onIntervalChanged: (PriceChangeInterval) -> Unit, -) { - - fun convert(value: TokenMarketInfo.PricePerformance, currentPrice: BigDecimal): PricePerformanceUM { - return PricePerformanceUM( - h24 = value.day.convert(currentPrice), - month = value.month.convert(currentPrice), - all = value.allTime.convert(currentPrice), - onIntervalChanged = onIntervalChanged, - ) - } - - private fun TokenMarketInfo.Range?.convert(currentPrice: BigDecimal): PricePerformanceUM.Value { - if (this == null || this.low == null || this.high == null) { - return PricePerformanceUM.Value( - low = StringsSigns.DASH_SIGN, - high = StringsSigns.DASH_SIGN, - indicatorFraction = 0f, - ) - } - - return PricePerformanceUM.Value( - low = low.convert(), - high = high.convert(), - indicatorFraction = calculateFraction(currentPrice), - ) - } - - private fun BigDecimal?.convert(): String { - val currency = appCurrency() - - return format { - fiat( - fiatCurrencyCode = currency.code, - fiatCurrencySymbol = currency.symbol, - ).price() - } - } - - private fun TokenMarketInfo.Range.calculateFraction(currentPrice: BigDecimal): Float { - val lowValue = low - val highValue = high - return when { - lowValue == null || highValue == null || highValue == BigDecimal.ZERO || currentPrice < lowValue -> 0f - currentPrice > highValue || lowValue == highValue -> 1f - else -> { - (currentPrice - lowValue).divide(highValue - lowValue, RoundingMode.HALF_UP) - .setScale(2, RoundingMode.HALF_UP) - .toFloat().coerceAtMost(1f) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt deleted file mode 100644 index d3bec1f927..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/SecurityScoreConverter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.model.formatter.MarketsDateTimeFormatters -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM -import com.tangem.features.markets.impl.R -import com.tangem.utils.converter.Converter - -@Stable -internal class SecurityScoreConverter( - private val onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit, - private val onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketInfo.SecurityData): SecurityScoreUM { - val ratingsCount = value.securityScoreProviderData.size - return SecurityScoreUM( - score = value.totalSecurityScore, - description = pluralReference( - id = R.plurals.markets_token_details_based_on_ratings, - count = ratingsCount, - formatArgs = wrappedList(ratingsCount), - ), - onInfoClick = { - onSecurityScoreInfoClick( - SecurityScoreBottomSheetContent( - title = resourceReference(R.string.markets_token_details_security_score), - description = resourceReference(R.string.markets_token_details_security_score_description), - providers = value.securityScoreProviderData.map { provider -> - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = provider.providerName, - lastAuditDate = provider.lastAuditDate?.let { date -> - MarketsDateTimeFormatters.formatAsDate(date.millis) - }, - score = provider.securityScore, - urlData = provider.urlData?.let { urlData -> - SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = urlData.fullUrl, - rootHost = urlData.rootHost, - ) - }, - iconUrl = provider.iconUrl, - ) - }, - onProviderLinkClick = onSecurityScoreProviderLinkClick, - ), - ) - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt deleted file mode 100644 index e7aa05eeb5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/converters/TokenMarketInfoConverter.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.tangem.features.markets.details.impl.model.converters - -import androidx.compose.runtime.Stable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.details.impl.ui.state.ListedOnUM -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter - -@Stable -@Suppress("LongParameterList") -internal class TokenMarketInfoConverter( - private val appCurrency: Provider, - private val needApplyFCARestrictions: Provider, - private val onInfoClick: (TangemBottomSheetConfigContent) -> Unit, - private val onListedOnClick: (Int) -> Unit, - onSecurityScoreInfoClick: (SecurityScoreBottomSheetContent) -> Unit, - onLinkClick: (LinksUM.Link) -> Unit, - onSecurityScoreProviderLinkClick: (SecurityScoreBottomSheetContent.SecurityScoreProviderUM) -> Unit, - onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, - onInsightsIntervalChanged: (PriceChangeInterval) -> Unit, -) : Converter { - - private val insightsConverter = InsightsConverter( - appCurrency = appCurrency, - onInfoClick = onInfoClick, - onIntervalChanged = onInsightsIntervalChanged, - ) - - private val securityScoreConverter = SecurityScoreConverter( - onSecurityScoreInfoClick = onSecurityScoreInfoClick, - onSecurityScoreProviderLinkClick = onSecurityScoreProviderLinkClick, - ) - private val pricePerformanceConverter = PricePerformanceConverter( - appCurrency = appCurrency, - onIntervalChanged = onPricePerformanceIntervalChanged, - ) - private val linksConverter = LinksConverter(onLinkClick = onLinkClick) - - override fun convert(value: TokenMarketInfo): MarketsTokenDetailsUM.InformationBlocks { - val metricsConverter = MetricsConverter( - tokenSymbol = value.symbol, - appCurrency = appCurrency, - onInfoClick = onInfoClick, - ) - - val exchangesAmount = value.exchangesAmount - val insights = if (needApplyFCARestrictions()) { - null - } else { - value.insights?.let { insightsConverter.convert(it) } - } - val securityScore = if (needApplyFCARestrictions()) { - null - } else { - value.securityData?.let { securityScoreConverter.convert(it) } - } - return MarketsTokenDetailsUM.InformationBlocks( - insights = insights, - securityScore = securityScore, - metrics = value.metrics?.let { metricsConverter.convert(it) }, - pricePerformance = value.pricePerformance?.let { performance -> - pricePerformanceConverter.convert( - value = performance, - currentPrice = value.quotes.currentPrice, - ) - }, - listedOn = if (exchangesAmount != null && exchangesAmount > 0) { - ListedOnUM.Content(onClick = { onListedOnClick(exchangesAmount) }, amount = exchangesAmount) - } else { - ListedOnUM.Empty - }, - links = value.links?.let { linksConverter.convert(it) }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt deleted file mode 100644 index bc1aeb77e3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/Formatters.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.features.markets.details.impl.model.formatter - -import com.tangem.common.ui.charts.state.MarketChartLook -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.getFiatPriceAmountWithScale -import com.tangem.core.ui.format.bigdecimal.percent -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenQuotes -import java.math.BigDecimal -import java.math.RoundingMode - -internal fun TokenQuotes.getFormattedPercentByInterval(interval: PriceChangeInterval): String { - val percent = when (interval) { - PriceChangeInterval.H24 -> h24ChangePercent - PriceChangeInterval.WEEK -> weekChangePercent - PriceChangeInterval.MONTH -> monthChangePercent - PriceChangeInterval.MONTH3 -> m3ChangePercent - PriceChangeInterval.MONTH6 -> m6ChangePercent - PriceChangeInterval.YEAR -> yearChangePercent - PriceChangeInterval.ALL_TIME -> allTimeChangePercent - } - - return percent?.format { percent() }.orEmpty() -} - -internal fun TokenQuotes.getPercentByInterval(interval: PriceChangeInterval): BigDecimal? { - return when (interval) { - PriceChangeInterval.H24 -> h24ChangePercent - PriceChangeInterval.WEEK -> weekChangePercent - PriceChangeInterval.MONTH -> monthChangePercent - PriceChangeInterval.MONTH3 -> m3ChangePercent - PriceChangeInterval.MONTH6 -> m6ChangePercent - PriceChangeInterval.YEAR -> yearChangePercent - PriceChangeInterval.ALL_TIME -> allTimeChangePercent - } -} - -@Suppress("MagicNumber") -internal fun BigDecimal?.percentChangeType(): PriceChangeType { - val scaled = this?.setScale(4, RoundingMode.HALF_UP) - return when { - scaled == null -> PriceChangeType.NEUTRAL - scaled > BigDecimal.ZERO -> PriceChangeType.UP - scaled < BigDecimal.ZERO -> PriceChangeType.DOWN - else -> PriceChangeType.NEUTRAL - } -} - -@Suppress("MagicNumber") -internal fun getChangePercentBetween(currentPrice: BigDecimal, previousPrice: BigDecimal): BigDecimal { - return if (previousPrice == BigDecimal.ZERO) { - BigDecimal.ZERO - } else { - currentPrice.subtract(previousPrice).divide(previousPrice, 4, RoundingMode.HALF_UP) - } -} - -internal fun getFormattedPriceChange(currentPrice: BigDecimal, updatedPrice: BigDecimal): PriceChangeType { - val current = getFiatPriceAmountWithScale(value = currentPrice).first - val updated = getFiatPriceAmountWithScale(value = updatedPrice).first - - return when { - updated > current -> PriceChangeType.UP - updated < current -> PriceChangeType.DOWN - else -> PriceChangeType.NEUTRAL - } -} - -internal fun PriceChangeType.toChartType(): MarketChartLook.Type { - return when (this) { - PriceChangeType.UP -> MarketChartLook.Type.Growing - PriceChangeType.DOWN -> MarketChartLook.Type.Falling - PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt deleted file mode 100644 index f36474bbee..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.features.markets.details.impl.model.formatter - -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.utils.DateTimeFormatters -import com.tangem.core.ui.utils.formatAsDateTime -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.impl.R -import com.tangem.utils.H24_MILLIS -import com.tangem.utils.WEEK_MILLIS -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import java.math.BigDecimal - -internal object MarketsDateTimeFormatters { - - private val dateTimeMMMFormatter by lazy { - DateTimeFormatters.getBestFormatterBySkeleton("dd MMM Hm") - } - - private val dateFormatter = DateTimeFormatters.dateDDMMYYYY - - fun getChartXFormatterByInterval(interval: PriceChangeInterval): (BigDecimal) -> String { - return when (interval) { - PriceChangeInterval.H24 -> { value: BigDecimal -> - value.toLong().formatAsDateTime(DateTimeFormatters.timeFormatter) - } - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - PriceChangeInterval.MONTH6, - -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd) - } - PriceChangeInterval.YEAR -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateMMMdd) - } - PriceChangeInterval.ALL_TIME -> { value -> - value.toLong().formatAsDateTime(DateTimeFormatters.dateYYYY) - } - } - } - - fun formatDateByInterval(interval: PriceChangeInterval, startTimestamp: Long): TextReference { - return when (interval) { - PriceChangeInterval.H24 -> resourceReference(R.string.common_today) - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - startTimestamp.formatAsDateTime(dateTimeMMMFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - startTimestamp.formatAsDateTime(dateFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.ALL_TIME -> resourceReference(R.string.common_all) - } - } - - fun formatDateByIntervalWithMarker(interval: PriceChangeInterval, markerTimestamp: BigDecimal): TextReference { - return when (interval) { - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - markerTimestamp.toLong().formatAsDateTime(dateTimeMMMFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - PriceChangeInterval.ALL_TIME, - -> { - resourceReference( - R.string.common_range_with_space, - wrappedList( - stringReference( - markerTimestamp.toLong().formatAsDateTime(dateFormatter), - ), - resourceReference(R.string.common_now), - ), - ) - } - } - } - - @Suppress("MagicNumber") - fun getStartTimestampByInterval(interval: PriceChangeInterval, currentTimestamp: Long): Long { - return when (interval) { - PriceChangeInterval.H24 -> currentTimestamp - H24_MILLIS - PriceChangeInterval.WEEK -> currentTimestamp - WEEK_MILLIS - PriceChangeInterval.MONTH -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(1).millis - PriceChangeInterval.MONTH3 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(3).millis - PriceChangeInterval.MONTH6 -> DateTime(currentTimestamp, DateTimeZone.UTC).minusMonths(6).millis - PriceChangeInterval.YEAR -> DateTime(currentTimestamp, DateTimeZone.UTC).minusYears(1).millis - PriceChangeInterval.ALL_TIME -> 0 - } - } - - fun getDefaultDateTimeString(interval: PriceChangeInterval, currentTimestamp: Long): TextReference { - return formatDateByInterval( - interval = interval, - startTimestamp = getStartTimestampByInterval( - interval = interval, - currentTimestamp = currentTimestamp, - ), - ) - } - - fun formatAsDate(timestamp: Long): String { - return timestamp.formatAsDateTime(dateFormatter) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt deleted file mode 100644 index 116fb390b4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/QuotesStateUpdater.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.features.markets.details.impl.model.state - -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.event.triggeredEvent -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.format.bigdecimal.price -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenQuotes -import com.tangem.features.markets.details.impl.model.converters.PricePerformanceConverter -import com.tangem.features.markets.details.impl.model.formatter.* -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.utils.Provider -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.update -import org.joda.time.DateTime -import java.math.BigDecimal - -internal class QuotesStateUpdater( - private val currentAppCurrency: Provider, - private val state: MutableStateFlow, - private val currentQuotes: MutableStateFlow, - private val lastUpdatedTimestamp: MutableStateFlow, - private val currentTokenInfo: MutableStateFlow, - private val onPricePerformanceIntervalChanged: (PriceChangeInterval) -> Unit, -) { - private val pricePerformanceConverter = PricePerformanceConverter( - currentAppCurrency, - onIntervalChanged = onPricePerformanceIntervalChanged, - ) - - suspend fun updateQuotes(newQuotes: TokenQuotes) { - val triggerPriceChangeType = getFormattedPriceChange( - currentPrice = currentQuotes.value.currentPrice, - updatedPrice = newQuotes.currentPrice, - ) - val trigger = if (triggerPriceChangeType != PriceChangeType.NEUTRAL) { - triggeredEvent( - data = triggerPriceChangeType, - onConsume = { - state.update { it.copy(triggerPriceChange = consumedEvent()) } - }, - ) - } else { - consumedEvent() - } - - val percent = newQuotes.getPercentByInterval(interval = state.value.selectedInterval) - val priceChangeType = percent.percentChangeType() - - // wait until marker is removed - state.first { it.isMarkerSet.not() } - - currentQuotes.value = newQuotes - lastUpdatedTimestamp.value = DateTime.now().millis - - state.update { stateToUpdate -> - stateToUpdate.copy( - priceText = newQuotes.currentPrice.format { - fiat( - fiatCurrencySymbol = currentAppCurrency().symbol, - fiatCurrencyCode = currentAppCurrency().code, - ).price() - }, - priceChangePercentText = newQuotes.getFormattedPercentByInterval( - interval = stateToUpdate.selectedInterval, - ), - priceChangeType = priceChangeType, - triggerPriceChange = trigger, - dateTimeText = MarketsDateTimeFormatters.getDefaultDateTimeString( - stateToUpdate.selectedInterval, - currentTimestamp = lastUpdatedTimestamp.value, - ), - body = stateToUpdate.body.updatePricePerformance(newQuotes.currentPrice), - ) - } - } - - private fun MarketsTokenDetailsUM.Body.updatePricePerformance(price: BigDecimal): MarketsTokenDetailsUM.Body { - val currentPricePerformance = currentTokenInfo.value?.pricePerformance ?: return this - - return if (this is MarketsTokenDetailsUM.Body.Content) { - copy( - infoBlocks = infoBlocks.copy( - pricePerformance = pricePerformanceConverter.convert(currentPricePerformance, price), - ), - ) - } else { - this - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt deleted file mode 100644 index dbe01ddcdc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/state/TokenNetworksState.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.markets.details.impl.model.state - -import com.tangem.domain.markets.TokenMarketInfo - -internal sealed class TokenNetworksState { - - data object Loading : TokenNetworksState() - - data object NoNetworksAvailable : TokenNetworksState() - - data class NetworksAvailable(val networks: List) : TokenNetworksState() -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt deleted file mode 100644 index a732ce35a6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/MarketsTokenDetailsContent.kt +++ /dev/null @@ -1,351 +0,0 @@ -package com.tangem.features.markets.details.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.snap -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -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.components.SpacerH16 -import com.tangem.core.ui.components.SpacerH32 -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.components.marketprice.PriceChangeInPercent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.EventEffect -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.components.* -import com.tangem.features.markets.details.impl.ui.preview.MarketsTokenDetailsPreview -import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf - -@Suppress("LongParameterList") -@Composable -internal fun MarketsTokenDetailsContent( - state: MarketsTokenDetailsUM, - backgroundColor: Color, - addTopBarStatusBarPadding: Boolean, - onBackClick: () -> Unit, - onHeaderSizeChange: (Dp) -> Unit, - backButtonEnabled: Boolean, - isAccountEnabled: Boolean, - modifier: Modifier = Modifier, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - Content( - modifier = modifier, - backgroundColor = backgroundColor, - state = state, - onBackClick = onBackClick, - onHeaderSizeChange = onHeaderSizeChange, - backButtonEnabled = backButtonEnabled, - portfolioBlock = portfolioBlock, - isAccountEnabled = isAccountEnabled, - addTopBarStatusBarInsets = addTopBarStatusBarPadding, - ) - - when (state.bottomSheetConfig.content) { - is InfoBottomSheetContent -> InfoBottomSheet(config = state.bottomSheetConfig) - is SecurityScoreBottomSheetContent -> SecurityScoreBottomSheet(config = state.bottomSheetConfig) - is ExchangesBottomSheetContent -> ExchangesBottomSheet(config = state.bottomSheetConfig) - } -} - -@Suppress("LongParameterList") -@Composable -private fun Content( - state: MarketsTokenDetailsUM, - backgroundColor: Color, - addTopBarStatusBarInsets: Boolean, - onBackClick: () -> Unit, - onHeaderSizeChange: (Dp) -> Unit, - backButtonEnabled: Boolean, - isAccountEnabled: Boolean, - modifier: Modifier = Modifier, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - val density = LocalDensity.current - val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } - val lazyListState = rememberLazyListState() - - Column( - modifier = modifier - .drawBehind { drawRect(backgroundColor) } - .let { if (addTopBarStatusBarInsets) it.statusBarsPadding() else it } - .fillMaxSize(), - ) { - TopBar( - modifier = Modifier.onGloballyPositioned { coordinates -> - if (coordinates.size.height > 0) { - with(density) { - onHeaderSizeChange(coordinates.size.height.toDp()) - } - } - }, - lazyListState = lazyListState, - tokenName = state.tokenName, - tokenPrice = state.priceText, - isBackButtonEnabled = backButtonEnabled, - onBackClick = onBackClick, - ) - - SpacerH4() - - LazyColumn( - state = lazyListState, - contentPadding = PaddingValues(bottom = bottomBarHeight), - ) { - item("header") { - Header( - state = state, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - item { SpacerH16() } - item("intervalSelector") { - IntervalSelector( - trendInterval = state.selectedInterval, - onIntervalClick = state.onSelectedIntervalChange, - isEnabled = state.body !is MarketsTokenDetailsUM.Body.Nothing, - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - item { SpacerH32() } - item("chart") { - MarketTokenDetailsChart( - modifier = Modifier.fillMaxWidth(), - backgroundColor = backgroundColor, - state = state.chartState, - ) - } - item { SpacerH16() } - - tokenMarketDetailsBody( - state = state.body, - isAccountEnabled = isAccountEnabled, - portfolioBlock = portfolioBlock, - ) - } - } -} - -@Composable -private fun TopBar( - lazyListState: LazyListState, - tokenName: String, - tokenPrice: String, - isBackButtonEnabled: Boolean, - onBackClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val shouldShowPriceSubtitle by remember { - derivedStateOf { - lazyListState.firstVisibleItemIndex > 1 - } - } - - TangemTopAppBar( - modifier = modifier, - title = tokenName, - subtitle = if (shouldShowPriceSubtitle) tokenPrice else null, - startButton = TopAppBarButtonUM.Back( - onBackClicked = onBackClick, - enabled = isBackButtonEnabled, - ), - ) -} - -@Composable -private fun Header(state: MarketsTokenDetailsUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.weight(1f)) { - TokenPriceText( - price = state.priceText, - triggerPriceChange = state.triggerPriceChange, - ) - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) { - Text( - text = state.dateTimeText.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - if (state.priceChangePercentText != null) { - PriceChangeInPercent( - valueInPercent = state.priceChangePercentText, - type = state.priceChangeType, - textStyle = TangemTheme.typography.caption2, - ) - } - } - } - SpacerW4() - CoinIcon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size48), - url = state.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - } -} - -@Composable -private fun TokenPriceText( - price: String, - triggerPriceChange: StateEvent, - modifier: Modifier = Modifier, -) { - val growColor = TangemTheme.colors.text.accent - val fallColor = TangemTheme.colors.text.warning - val generalColor = TangemTheme.colors.text.primary1 - - val color = remember(generalColor) { Animatable(generalColor) } - - EventEffect(triggerPriceChange) { changeType -> - val nextColor = when (changeType) { - PriceChangeType.UP, - -> growColor - PriceChangeType.DOWN -> fallColor - PriceChangeType.NEUTRAL -> return@EventEffect - } - - color.animateTo(nextColor, snap()) - color.animateTo(generalColor, tween(durationMillis = 500)) - } - - Text( - text = price, - modifier = modifier, - color = color.value, - autoSize = TextAutoSize.StepBased(maxFontSize = TangemTheme.typography.head.fontSize), - maxLines = 1, - style = TangemTheme.typography.head, - ) -} - -@Composable -private fun IntervalSelector( - trendInterval: PriceChangeInterval, - isEnabled: Boolean, - onIntervalClick: (PriceChangeInterval) -> Unit, - modifier: Modifier = Modifier, -) { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - PriceChangeInterval.MONTH3, - PriceChangeInterval.MONTH6, - PriceChangeInterval.YEAR, - PriceChangeInterval.ALL_TIME, - ), - color = TangemTheme.colors.button.secondary, - initialSelectedItem = trendInterval, - onClick = onIntervalClick, - isEnabled = isEnabled, - modifier = modifier, - ) { - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - vertical = TangemTheme.dimens.spacing4, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = it.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = if (isEnabled) { - TangemTheme.colors.text.primary1 - } else { - TangemTheme.colors.text.disabled - }, - ) - } - } -} - -@Composable -fun PriceChangeInterval.getText(): TextReference { - return when (this) { - PriceChangeInterval.H24 -> resourceReference(R.string.markets_selector_interval_24h_title) - PriceChangeInterval.WEEK -> resourceReference(R.string.markets_selector_interval_7d_title) - PriceChangeInterval.MONTH -> resourceReference(R.string.markets_selector_interval_1m_title) - PriceChangeInterval.MONTH3 -> resourceReference(R.string.markets_selector_interval_3m_title) - PriceChangeInterval.MONTH6 -> resourceReference(R.string.markets_selector_interval_6m_title) - PriceChangeInterval.YEAR -> resourceReference(R.string.markets_selector_interval_1y_title) - PriceChangeInterval.ALL_TIME -> resourceReference(R.string.markets_selector_interval_all_title) - } -} - -// region Preview -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun MarketsTokenDetailsContent_Preview( - @PreviewParameter(MarketsTokenDetailsContentPreviewProvider::class) params: MarketsTokenDetailsUM, -) { - TangemThemePreview { - MarketsTokenDetailsContent( - state = params, - onHeaderSizeChange = {}, - onBackClick = {}, - backgroundColor = TangemTheme.colors.background.tertiary, - portfolioBlock = {}, - backButtonEnabled = true, - isAccountEnabled = true, - addTopBarStatusBarPadding = false, - ) - } -} - -private class MarketsTokenDetailsContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - MarketsTokenDetailsPreview.loadingState, - MarketsTokenDetailsPreview.contentState, - ) -} -// endregion \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt deleted file mode 100644 index fdbf284dbc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ExchangesBottomSheet.kt +++ /dev/null @@ -1,191 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.SpacerH12 -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.audits.AuditLabelUM -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.ui.state.ExchangesBottomSheetContent -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.toImmutableList - -/** - * Exchanges bottom sheet - * - * @param config bottom sheet config - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun ExchangesBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { Title(textResId = it.titleResId, onBackClick = config.onDismissRequest) }, - content = { content -> - Box { - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = bottomBarHeight), - ) { - item(key = "subtitle") { - Subtitle( - subtitleRes = content.subtitleResId, - volumeReference = content.volumeReference, - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing8, - ), - ) - } - - items( - items = content.exchangeItems, - key = TokenItemState::id, - itemContent = { TokenItem(state = it, isBalanceHidden = false) }, - ) - } - - if (content is ExchangesBottomSheetContent.Error) { - Error( - content = content, - modifier = Modifier.align(Alignment.Center), - ) - } - } - }, - ) -} - -@Composable -private fun Title(@StringRes textResId: Int, onBackClick: () -> Unit) { - TangemTopAppBar( - title = stringResourceSafe(id = textResId), - startButton = TopAppBarButtonUM.Back(onBackClicked = onBackClick), - ) -} - -@Composable -private fun Subtitle(@StringRes subtitleRes: Int, volumeReference: TextReference, modifier: Modifier = Modifier) { - Row( - modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - SubtitleText(textReference = resourceReference(id = subtitleRes)) - - SubtitleText(textReference = volumeReference) - } -} - -@Composable -private fun SubtitleText(textReference: TextReference) { - Text( - text = textReference.resolveReference(), - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) -} - -@Composable -private fun Error(content: ExchangesBottomSheetContent.Error, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResourceSafe(id = content.message), - color = TangemTheme.colors.text.tertiary, - textAlign = TextAlign.Center, - style = TangemTheme.typography.caption1, - ) - - SpacerH12() - - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(id = R.string.alert_button_try_again), - onClick = content.onRetryClick, - ), - ) - } -} - -@Preview -@Preview(name = "Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ExchangesBottomSheet( - @PreviewParameter(ExchangesBottomSheetContentProvider::class) content: ExchangesBottomSheetContent, -) { - TangemThemePreview { - ExchangesBottomSheet( - config = TangemBottomSheetConfig( - onDismissRequest = {}, - content = content, - isShown = true, - ), - ) - } -} - -private class ExchangesBottomSheetContentProvider : CollectionPreviewParameterProvider( - listOf( - ExchangesBottomSheetContent.Loading(exchangesCount = 13), - ExchangesBottomSheetContent.Error(onRetryClick = {}), - ExchangesBottomSheetContent.Content( - exchangeItems = List(size = 13) { index -> - TokenItemState.Content( - id = index.toString(), - iconState = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = R.drawable.ic_facebook_24, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "OKX")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "$67.52M"), - subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference(value = "CEX")), - subtitle2State = TokenItemState.Subtitle2State.LabelContent( - auditLabelUM = AuditLabelUM( - text = stringReference("Caution"), - type = AuditLabelUM.Type.Warning, - ), - ), - onItemClick = {}, - onItemLongClick = {}, - ) - } - .toImmutableList(), - ), - ), -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt deleted file mode 100644 index 234468b262..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoBottomSheet.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.components.notifications.Notification -import com.tangem.core.ui.components.notifications.NotificationConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.InfoBottomSheetContent -import com.tangem.features.markets.impl.R -import dev.jeziellago.compose.markdowntext.MarkdownText - -@Composable -internal fun InfoBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { TangemBottomSheetTitle(title = it.title) }, - content = { content -> - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - MarkdownText( - markdown = content.body.resolveReference(), - disableLinkMovementMethod = true, - linkifyMask = 0, - syntaxHighlightColor = TangemTheme.colors.text.secondary, - style = TangemTheme.typography.body2.copy( - color = TangemTheme.colors.text.secondary, - ), - ) - - if (content.generatedAINotificationUM != null) { - AdditionalInfoNotification( - onClick = content.generatedAINotificationUM.onClick, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing16) - .fillMaxWidth(), - ) - } - - SpacerH(bottomBarHeight) - } - }, - ) -} - -@Composable -private fun AdditionalInfoNotification(onClick: () -> Unit, modifier: Modifier = Modifier) { - Notification( - config = NotificationConfig( - subtitle = TextReference.Res(id = R.string.information_generated_with_ai), - iconResId = R.drawable.ic_magic_28, - onClick = onClick, - shouldShowArrowIcon = false, - ), - modifier = modifier, - subtitleColor = TangemTheme.colors.text.primary1, - containerColor = TangemTheme.colors.button.disabled, - iconTint = TangemTheme.colors.icon.accent, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt deleted file mode 100644 index 3ca286968f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InfoPoint.kt +++ /dev/null @@ -1,186 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.R -import com.tangem.core.ui.components.SpacerW4 -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.text.TooltipText -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM - -@Composable -internal fun InfoPoint(infoPointUM: InfoPointUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), - horizontalAlignment = Alignment.Start, - ) { - if (infoPointUM.onInfoClick != null) { - TooltipText( - text = infoPointUM.title, - onInfoClick = infoPointUM.onInfoClick, - textStyle = TangemTheme.typography.caption2, - ) - } else { - Text( - text = infoPointUM.title.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - Row { - Text( - text = infoPointUM.value, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - if (infoPointUM.change != null) { - SpacerW4() - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size8) - .align(Alignment.CenterVertically), - imageVector = ImageVector.vectorResource( - id = when (infoPointUM.change) { - InfoPointUM.ChangeType.UP -> R.drawable.ic_arrow_up_8 - InfoPointUM.ChangeType.DOWN -> R.drawable.ic_arrow_down_8 - }, - ), - tint = when (infoPointUM.change) { - InfoPointUM.ChangeType.UP -> TangemTheme.colors.icon.accent - InfoPointUM.ChangeType.DOWN -> TangemTheme.colors.icon.warning - }, - contentDescription = null, - ) - } - } - } -} - -@Composable -internal fun InfoPointShimmer(modifier: Modifier = Modifier, withTooltip: Boolean = false) { - Column( - modifier = modifier.padding(vertical = TangemTheme.dimens.spacing8), - horizontalAlignment = Alignment.Start, - ) { - if (withTooltip) { - Box( - modifier = Modifier - .requiredHeight(TangemTheme.dimens.size16) - .fillMaxWidth(), - contentAlignment = Alignment.CenterStart, - ) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.caption2, - textSizeHeight = false, - ) - } - } else { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.caption2, - textSizeHeight = true, - ) - } - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - style = TangemTheme.typography.body1, - textSizeHeight = true, - ) - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - Column( - modifier = Modifier - .width(150.dp) - .background(TangemTheme.colors.background.tertiary), - ) { - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000,000", - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000,000", - onInfoClick = { }, - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000", - change = InfoPointUM.ChangeType.UP, - onInfoClick = { }, - ), - ) - InfoPoint( - infoPointUM = InfoPointUM( - title = stringReference("Market Cap"), - value = "$1,000,000", - change = InfoPointUM.ChangeType.DOWN, - onInfoClick = { }, - ), - ) - } - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewShimmer() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - Column( - modifier = Modifier - .width(150.dp) - .background(TangemTheme.colors.background.tertiary), - ) { - InfoPointShimmer(modifier = Modifier.fillMaxWidth()) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - } - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt deleted file mode 100644 index d40e37b970..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/InsightsBlock.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.block.information.GridItems -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.text.TooltipText -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.InsightsUM -import com.tangem.features.markets.details.impl.ui.getText -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -@Composable -internal fun InsightsBlock(state: InsightsUM, modifier: Modifier = Modifier) { - var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } - - InformationBlock( - modifier = modifier, - title = { - TooltipText( - text = resourceReference(R.string.markets_token_details_insights), - textStyle = TangemTheme.typography.subtitle2, - onInfoClick = state.onInfoClick, - ) - }, - action = { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.WEEK, - PriceChangeInterval.MONTH, - ), - initialSelectedItem = PriceChangeInterval.H24, - onClick = { interval -> - currentInterval = interval - state.onIntervalChanged(interval) - }, - modifier = Modifier.width(IntrinsicSize.Min), - ) { interval -> - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - horizontal = 14.dp, - vertical = 4.dp, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = interval.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary1, - ) - } - } - }, - content = { - val infoPoints = when (currentInterval) { - PriceChangeInterval.H24 -> state.h24Info - PriceChangeInterval.WEEK -> state.weekInfo - PriceChangeInterval.MONTH -> state.monthInfo - else -> state.h24Info - } - - GridItems( - items = infoPoints, - itemContent = { infoPoint -> - InfoPoint( - modifier = Modifier.align(Alignment.CenterStart), - infoPointUM = infoPoint, - ) - }, - ) - }, - ) -} - -@Composable -internal fun InsightsBlockPlaceholder(modifier: Modifier = Modifier) { - val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } - val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } - val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 - - InformationBlock( - modifier = modifier, - title = { - RectangleShimmer( - modifier = Modifier - .height(headerHeight) - .fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - ) - }, - content = { - GridItems( - items = List(size = 4) { it }.toImmutableList(), - horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - itemContent = { - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - ) - }, - ) - }, - ) -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - InsightsBlock( - state = InsightsUM( - h24Info = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000 000 000", - ), - ), - weekInfo = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000 000", - ), - ), - monthInfo = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_experienced_buyers), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_buy_pressure), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_holders), - value = "1 000", - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_liquidity), - value = "1 000", - ), - ), - onInfoClick = {}, - onIntervalChanged = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - actualContent = { ContentPreview() }, - shimmerContent = { InsightsBlockPlaceholder() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt deleted file mode 100644 index 6d10ec8957..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/LinksBlock.kt +++ /dev/null @@ -1,219 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -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.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.ChipShimmer -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.chip.Chip -import com.tangem.core.ui.components.inputrow.inner.DividerContainer -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.LinksUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun LinksBlock(state: LinksUM, modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - contentHorizontalPadding = 0.dp, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_links), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - content = { - Column { - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_official_links), - links = state.officialLinks, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_social), - links = state.social, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_repository), - links = state.repository, - onLinkClick = state.onLinkClick, - ) - SubBlock( - title = stringResourceSafe(id = R.string.markets_token_details_blockchain_site), - links = state.blockchainSite, - onLinkClick = state.onLinkClick, - lastBlock = true, - ) - } - }, - ) -} - -@OptIn(ExperimentalLayoutApi::class) -@Composable -private fun SubBlock( - links: ImmutableList, - onLinkClick: (LinksUM.Link) -> Unit, - modifier: Modifier = Modifier, - lastBlock: Boolean = false, - title: String = "Official links", -) { - if (links.isEmpty()) return - - DividerContainer( - modifier = modifier, - showDivider = !lastBlock, - ) { - Column( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Text( - text = title, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - links.fastForEach { link -> - Chip( - text = stringReference(link.title), - iconResId = link.iconRes, - onClick = { onLinkClick(link) }, - ) - } - } - } - } -} - -@Composable -fun LinksBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - contentHorizontalPadding = 0.dp, - title = { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.subtitle2, - ) - }, - content = { - Column { - SubBlockPlaceholder() - SubBlockPlaceholder() - SubBlockPlaceholder(lastBlock = true) - } - }, - ) -} - -@Composable -private fun SubBlockPlaceholder(modifier: Modifier = Modifier, lastBlock: Boolean = false) { - DividerContainer( - modifier = modifier, - showDivider = !lastBlock, - ) { - Column( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - TextShimmer( - modifier = Modifier.width(78.dp), - style = TangemTheme.typography.caption2, - ) - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - repeat(times = 3) { - ChipShimmer( - modifier = Modifier.weight(1f), - ) - } - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - LinksBlock( - state = LinksUM( - officialLinks = persistentListOf( - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Website", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - social = persistentListOf( - LinksUM.Link( - title = "Twitter", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - LinksUM.Link( - title = "Facebook", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - repository = persistentListOf( - LinksUM.Link( - title = "Github", - iconRes = R.drawable.ic_plus_24, - url = "https://tangem.com", - ), - ), - blockchainSite = persistentListOf(), - onLinkClick = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PlaceholderPreview() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { LinksBlockPlaceholder() }, - actualContent = { ContentPreview() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt deleted file mode 100644 index 3d5714135e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ListedOnBlock.kt +++ /dev/null @@ -1,138 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.common.ui.R -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.ui.state.ListedOnUM -import kotlinx.coroutines.delay - -/** - * "Listed on" block - * - * @param state block state - * -[REDACTED_AUTHOR] - */ -@Composable -internal fun ListedOnBlock(state: ListedOnUM, modifier: Modifier = Modifier) { - Box(modifier = modifier) { - InformationBlock( - title = { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - modifier = Modifier - .clip(shape = TangemTheme.shapes.roundedCornersXMedium) - .clickable(enabled = state is ListedOnUM.Content) { - (state as? ListedOnUM.Content)?.onClick?.invoke() - }, - ) { - Description( - state = state, - modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing12), - ) - } - - if (state is ListedOnUM.Content) { - Icon( - painter = painterResource(id = R.drawable.ic_chevron_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - modifier = Modifier - .align(Alignment.CenterEnd) - .padding(end = TangemTheme.dimens.spacing12), - ) - } - } -} - -@Composable -internal fun ListedOnBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - title = { - TextShimmer( - style = TangemTheme.typography.subtitle2, - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - ) - }, - modifier = modifier, - ) { - TextShimmer( - style = TangemTheme.typography.body2, - modifier = Modifier - .fillMaxWidth(fraction = 0.3f) - .padding(bottom = TangemTheme.dimens.spacing12), - ) - } -} - -@Composable -private fun Description(state: ListedOnUM, modifier: Modifier = Modifier) { - Text( - text = state.description.resolveReference(), - modifier = modifier, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = TangemTheme.typography.body2, - ) -} - -@Preview(widthDp = 328, heightDp = 68) -@Preview(name = "Dark Theme", widthDp = 328, heightDp = 68, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview_ListedOnBlock(@PreviewParameter(ListenOnUMProvider::class) state: ListedOnUM?) { - TangemThemePreview { - if (state == null) { - ListedOnBlockPlaceholder() - } else { - ListedOnBlock(state = state) - } - } -} - -@Preview -@Composable -private fun Preview_ListedOnBlock_StateChanging() { - var state by remember { mutableStateOf(value = null) } - - Preview_ListedOnBlock(state = state) - - LaunchedEffect(key1 = null) { - delay(timeMillis = 3000) - - state = ListedOnUM.Empty - } -} - -private class ListenOnUMProvider : CollectionPreviewParameterProvider( - collection = listOf( - ListedOnUM.Empty, - ListedOnUM.Content(onClick = {}, amount = 5), - null, - ), -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt deleted file mode 100644 index 0ad6b22ee0..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MarketTokenDetailsChart.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.Color -import com.tangem.common.ui.charts.MarketChart -import com.tangem.common.ui.charts.getMarketChartBottomAxisHeight -import com.tangem.common.ui.charts.state.MarketChartLook -import com.tangem.common.ui.charts.state.rememberMarketChartState -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.core.ui.components.UnableToLoadData - -@Composable -internal fun MarketTokenDetailsChart( - state: MarketsTokenDetailsUM.ChartState, - backgroundColor: Color, - modifier: Modifier = Modifier, -) { - val growingColor = TangemTheme.colors.icon.accent - val fallingColor = TangemTheme.colors.icon.warning - val neutralColor = TangemTheme.colors.icon.informative - - val chartState = rememberMarketChartState( - dataProducer = state.dataProducer, - colorMapper = { chartType -> - when (chartType) { - MarketChartLook.Type.Growing -> growingColor - MarketChartLook.Type.Falling -> fallingColor - MarketChartLook.Type.Neutral -> neutralColor - } - }, - onMarkerShown = state.onMarkerPointSelected, - ) - - val bottomChartAxisHeight = getMarketChartBottomAxisHeight() - - Box(modifier) { - MarketChart( - modifier = Modifier.fillMaxWidth(), - state = chartState, - ) - - if (state.status != MarketsTokenDetailsUM.ChartState.Status.DATA) { - Box( - Modifier - .drawBehind { drawRect(backgroundColor) } - .matchParentSize() - .padding(bottom = bottomChartAxisHeight), - ) { - when (state.status) { - MarketsTokenDetailsUM.ChartState.Status.LOADING -> { - CircularProgressIndicator( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .align(Alignment.Center), - color = TangemTheme.colors.text.accent, - strokeWidth = TangemTheme.dimens.size2, - ) - } - MarketsTokenDetailsUM.ChartState.Status.ERROR -> { - UnableToLoadData( - modifier = Modifier - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing12, - ) - .align(Alignment.Center), - onRetryClick = state.onLoadRetryClick, - ) - } - else -> {} - } - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt deleted file mode 100644 index 98e74fd06f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/MetricsBlock.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.TextButton -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.GridItems -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.InfoPointUM -import com.tangem.features.markets.details.impl.ui.state.MetricsUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -const val MAX_METRICS_COUNT = 6 - -@Composable -internal fun MetricsBlock(state: MetricsUM, modifier: Modifier = Modifier) { - var isExpanded by remember { mutableStateOf(false) } - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_metrics), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - action = { - if (state.metrics.size > MAX_METRICS_COUNT) { - ShowLessMoreButton(expanded = isExpanded, onClick = { isExpanded = !isExpanded }) - } - }, - content = { - val metrics = if (isExpanded) { - state.metrics - } else { - state.metrics.take(MAX_METRICS_COUNT).toImmutableList() - } - - GridItems( - items = metrics, - itemContent = { - InfoPoint(infoPointUM = it) - }, - ) - }, - ) -} - -// TODO make TextButton clickable area smaller and remove paddings for an action in InformationBlock -@Composable -private fun ShowLessMoreButton(expanded: Boolean, onClick: () -> Unit) { - // FIXME add string resources - val text = if (expanded) { - "See less" - } else { - "See more" - } - - TextButton( - text = text, - onClick = onClick, - colors = TangemButtonsDefaults.positiveButtonColors, - textStyle = TangemTheme.typography.body2, - ) -} - -@Composable -internal fun MetricsBlockPlaceholder(modifier: Modifier = Modifier) { - InformationBlock( - modifier = modifier, - title = { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - style = TangemTheme.typography.subtitle2, - ) - }, - action = { - Box(Modifier) - }, - content = { - GridItems( - items = List(size = 6) { it }.toImmutableList(), - horizontalArragement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - itemContent = { - InfoPointShimmer( - modifier = Modifier.fillMaxWidth(), - withTooltip = true, - ) - }, - ) - }, - ) -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun BlockPreview() { - TangemThemePreview { - MetricsBlock( - state = MetricsUM( - metrics = persistentListOf( - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_capitalization), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_market_rating), - value = "A", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_trading_volume), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_fully_diluted_valuation), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_circulating_supply), - value = "1.2T", - onInfoClick = {}, - ), - InfoPointUM( - title = resourceReference(R.string.markets_token_details_total_supply), - value = "1.2T", - onInfoClick = {}, - ), - ), - ), - ) - } -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - actualContent = { BlockPreview() }, - shimmerContent = { MetricsBlockPlaceholder() }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt deleted file mode 100644 index 17e8d19e3f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/PricePerformanceBlock.kt +++ /dev/null @@ -1,256 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SpacerW8 -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons -import com.tangem.core.ui.components.progressbar.TangemLinearProgressIndicator -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemAnimations -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.getText -import com.tangem.features.markets.details.impl.ui.state.PricePerformanceUM -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun PricePerformanceBlock(state: PricePerformanceUM, modifier: Modifier = Modifier) { - var currentInterval by remember { mutableStateOf(PriceChangeInterval.H24) } - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(id = R.string.markets_token_details_price_performance), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - action = { - SegmentedButtons( - config = persistentListOf( - PriceChangeInterval.H24, - PriceChangeInterval.MONTH, - PriceChangeInterval.ALL_TIME, - ), - initialSelectedItem = PriceChangeInterval.H24, - onClick = { interval -> - currentInterval = interval - state.onIntervalChanged(interval) - }, - modifier = Modifier.width(IntrinsicSize.Min), - ) { interval -> - Box( - Modifier - .fillMaxSize() - .align(Alignment.Center) - .padding( - horizontal = 14.dp, - vertical = TangemTheme.dimens.spacing4, - ), - ) { - Text( - modifier = Modifier.align(Alignment.Center), - text = interval.getText().resolveReference(), - style = TangemTheme.typography.caption1, - color = TangemTheme.colors.text.primary1, - ) - } - } - }, - content = { - val value = when (currentInterval) { - PriceChangeInterval.H24 -> state.h24 - PriceChangeInterval.MONTH -> state.month - PriceChangeInterval.ALL_TIME -> state.all - else -> error("") - } - - Content( - modifier = Modifier.fillMaxWidth(), - state = value, - ) - }, - ) -} - -@Composable -private fun Content(state: PricePerformanceUM.Value, modifier: Modifier = Modifier) { - val animatedIndicatorFraction by TangemAnimations.horizontalIndicatorAsState( - targetFraction = state.indicatorFraction, - ) - - Column( - modifier = modifier - .padding(vertical = TangemTheme.dimens.spacing8), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = stringResourceSafe(R.string.markets_token_details_low), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - SpacerW8() - Text( - text = stringResourceSafe(R.string.markets_token_details_high), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - TangemLinearProgressIndicator( - modifier = Modifier - .height(TangemTheme.dimens.size6) - .fillMaxWidth(), - progress = { animatedIndicatorFraction }, - color = TangemTheme.colors.text.accent, - backgroundColor = TangemTheme.colors.background.tertiary, - strokeCap = StrokeCap.Round, - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8), - ) { - Text( - modifier = Modifier.weight(1f), - text = state.low, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - ) - Text( - modifier = Modifier.weight(1f), - text = state.high, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - textAlign = TextAlign.End, - ) - } - } -} - -@Composable -internal fun PricePerformanceBlockPlaceholder(modifier: Modifier = Modifier) { - val subtitle2dp = with(LocalDensity.current) { TangemTheme.typography.subtitle2.lineHeight.toDp() } - val caption1dp = with(LocalDensity.current) { TangemTheme.typography.caption1.lineHeight.toDp() } - val headerHeight = maxOf(subtitle2dp, caption1dp) + TangemTheme.dimens.spacing4 - - InformationBlock( - modifier = modifier, - title = { - RectangleShimmer( - modifier = Modifier - .height(headerHeight) - .fillMaxWidth(), - radius = TangemTheme.dimens.radius3, - ) - }, - content = { - Column( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing8), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.width(35.dp), - style = TangemTheme.typography.caption2, - ) - SpacerW8() - TextShimmer( - modifier = Modifier.width(35.dp), - style = TangemTheme.typography.caption2, - ) - } - RectangleShimmer( - modifier = Modifier - .height(TangemTheme.dimens.size6) - .fillMaxWidth(), - radius = 27.dp, - ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.width(TangemTheme.dimens.size56), - style = TangemTheme.typography.body1, - ) - SpacerW8() - TextShimmer( - modifier = Modifier.width(TangemTheme.dimens.size56), - style = TangemTheme.typography.body1, - ) - } - } - }, - ) -} - -@Preview -@Preview("Dark Theme", uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - PricePerformanceBlock( - modifier = Modifier, - state = PricePerformanceUM( - h24 = PricePerformanceUM.Value( - low = "\$38,5K", - high = "\$58,5K", - indicatorFraction = 0.5f, - ), - month = PricePerformanceUM.Value( - low = "\$500,5K", - high = "\$5800,5K", - indicatorFraction = 0.8f, - ), - all = PricePerformanceUM.Value( - low = "\$58,52", - high = "\$580,5M", - indicatorFraction = 0.2f, - ), - onIntervalChanged = {}, - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PlaceholderPreview() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - PricePerformanceBlockPlaceholder() - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt deleted file mode 100644 index d7d047c38b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/ScoreStarsBlock.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.annotation.FloatRange -import androidx.compose.foundation.layout.* -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.CompositingStrategy -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.impl.R -import kotlin.math.round - -private const val STARS_COUNT = 5 - -@Composable -internal fun ScoreStarsBlock( - score: Float, - horizontalSpacing: Dp, - scoreTextStyle: TextStyle, - modifier: Modifier = Modifier, -) { - val rounded = score.roundTo1decimal() - val percentage = rounded / STARS_COUNT - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(horizontalSpacing), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = rounded.toString(), - style = scoreTextStyle, - color = TangemTheme.colors.text.primary1, - ) - Stars(fraction = percentage) - } -} - -@Suppress("MagicNumber") -@Composable -private fun Stars(@FloatRange(0.0, 1.0) fraction: Float = 0f) { - val grayColor = TangemTheme.colors.icon.inactive - - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - ) { - repeat(times = 5) { i -> - Box( - modifier = Modifier.size(TangemTheme.dimens.size16), - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .requiredSize(16.dp) - .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) - .drawWithCache { - onDrawWithContent { - val starFraction = ((fraction - i * 0.2) / 0.2).coerceIn(0.0, 1.0) - val starFractionFloat = starFraction - .toFloat() - .roundTo1decimal() - - drawContent() - drawRect( - color = grayColor, - topLeft = Offset(x = size.width * starFractionFloat, y = 0f), - size = Size(size.width * (1 - starFractionFloat), size.height), - blendMode = BlendMode.SrcIn, - ) - } - }, - imageVector = ImageVector.vectorResource(R.drawable.ic_star_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - } - } -} - -@Suppress("MagicNumber") -private fun Float.roundTo1decimal(): Float { - return round(this * 10) / 10 -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt deleted file mode 100644 index 468b7f7b59..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBlock.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -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.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 com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.text.TooltipText -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.core.ui.utils.PreviewShimmerContainer -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreUM -import com.tangem.features.markets.impl.R - -@Composable -internal fun SecurityScoreBlock(state: SecurityScoreUM, modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.action) - .fillMaxWidth() - .heightIn(max = TangemTheme.dimens.size72) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - ) { - Column( - modifier = Modifier - .weight(1F) - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - TooltipText( - text = resourceReference(R.string.markets_token_details_security_score), - onInfoClick = state.onInfoClick, - textStyle = TangemTheme.typography.subtitle2, - ) - - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - ScoreStarsBlock( - score = state.score, - scoreTextStyle = TangemTheme.typography.body1, - horizontalSpacing = TangemTheme.dimens.spacing8, - ) - } -} - -@Composable -internal fun SecurityScoreBlockPlaceholder(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .clip(TangemTheme.shapes.roundedCornersXMedium) - .background(TangemTheme.colors.background.primary) - .fillMaxWidth() - .heightIn(max = TangemTheme.dimens.size72) - .padding(all = TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column( - modifier = Modifier - .fillMaxWidth(fraction = 0.4f) - .padding(vertical = TangemTheme.dimens.spacing2) - .fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween, - ) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.subtitle2, - textSizeHeight = true, - ) - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } - - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.5f), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } -} - -@Preview(widthDp = 328, showBackground = true) -@Preview(widthDp = 328, showBackground = true, locale = "ru") -@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ContentPreview() { - TangemThemePreview { - SecurityScoreBlock( - state = SecurityScoreUM( - score = 3.5f, - description = stringReference("Based on 3 ratings"), - onInfoClick = {}, - ), - ) - } -} - -@Preview(widthDp = 328, showBackground = true) -@Preview(widthDp = 328, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewPlaceholder() { - TangemThemePreview { - PreviewShimmerContainer( - shimmerContent = { - SecurityScoreBlockPlaceholder( - modifier = Modifier.fillMaxWidth(), - ) - }, - actualContent = { - ContentPreview() - }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt deleted file mode 100644 index b8c4242c9a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/SecurityScoreBottomSheet.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.ripple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.util.fastForEachIndexed -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.components.inputrow.inner.DividerContainer -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.ui.preview.SecurityScorePreviewData -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent - -@Composable -internal fun SecurityScoreBottomSheet(config: TangemBottomSheetConfig) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - TangemBottomSheet( - config = config, - addBottomInsets = false, - title = { TangemBottomSheetTitle(title = it.title) }, - content = { content -> - Column( - modifier = Modifier - .verticalScroll(rememberScrollState()) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - Text( - text = content.description.resolveReference(), - style = TangemTheme.typography.body2.copy( - color = TangemTheme.colors.text.secondary, - ), - ) - - SpacerH12() - content.providers.fastForEachIndexed { index, provider -> - DividerContainer( - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = content.providers.lastIndex, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action), - showDivider = index != content.providers.lastIndex, - ) { - SecurityScoreProviderRow( - providerUM = provider, - onLinkClick = { content.onProviderLinkClick(provider) }, - ) - } - } - - SpacerH16() - SpacerH(bottomBarHeight) - } - }, - ) -} - -@Composable -private fun SecurityScoreProviderRow( - providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM, - onLinkClick: () -> Unit, -) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = TangemTheme.dimens.spacing12) - .heightIn(min = TangemTheme.dimens.size68), - verticalAlignment = Alignment.CenterVertically, - ) { - SubcomposeAsyncImage( - modifier = Modifier - .size(size = TangemTheme.dimens.size40) - .clip(TangemTheme.shapes.roundedCorners8), - model = ImageRequest.Builder(context = LocalContext.current) - .data(providerUM.iconUrl) - .crossfade(enable = true) - .allowHardware(false) - .build(), - loading = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, - error = { RectangleShimmer(radius = TangemTheme.dimens.radius8) }, - contentDescription = null, - ) - - Column( - modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - Text( - text = providerUM.name, - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - if (providerUM.lastAuditDate != null) { - Text( - text = providerUM.lastAuditDate, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } - - SpacerWMax() - - Column( - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - modifier = Modifier.clickable( - enabled = providerUM.urlData != null, - indication = ripple(bounded = false), - interactionSource = remember { MutableInteractionSource() }, - onClick = onLinkClick, - ), - ) { - ScoreStarsBlock( - score = providerUM.score, - scoreTextStyle = TangemTheme.typography.body2, - horizontalSpacing = TangemTheme.dimens.spacing3, - ) - - UrlBlock(providerUM) - } - } -} - -@Composable -private fun UrlBlock(providerUM: SecurityScoreBottomSheetContent.SecurityScoreProviderUM) { - val urlData = providerUM.urlData - val rootHost = urlData?.rootHost - if (urlData != null && rootHost != null) { - Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4), - ) { - Text( - text = urlData.rootHost, - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - Icon( - modifier = Modifier - .size(TangemTheme.dimens.size16), - painter = painterResource(id = R.drawable.ic_arrow_top_right_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - } -} - -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SecurityScoreBottomSheetPreview() { - TangemThemePreview { - SecurityScoreBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = SecurityScorePreviewData.bottomSheetContent, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt deleted file mode 100644 index 13124c707d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/components/TokenMarketDetailsBody.kt +++ /dev/null @@ -1,198 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.components - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.tangem.core.ui.components.UnableToLoadData -import com.tangem.core.ui.components.items.DescriptionItem -import com.tangem.core.ui.components.items.DescriptionPlaceholder -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.impl.ui.state.MarketsTokenDetailsUM -import com.tangem.features.markets.impl.R - -@Suppress("CanBeNonNullable") -internal fun LazyListScope.tokenMarketDetailsBody( - state: MarketsTokenDetailsUM.Body, - isAccountEnabled: Boolean, - portfolioBlock: @Composable ((Modifier) -> Unit)?, -) { - when (state) { - MarketsTokenDetailsUM.Body.Loading -> { - item("description-loading") { - DescriptionPlaceholder(modifier = Modifier.blockPaddings()) - } - - if (portfolioBlock != null) { - item(key = "portfolio") { - portfolioBlock(Modifier.blockPaddings()) - } - } - - if (isAccountEnabled) { - aboutCoinHeader() - } - - loadingInfoBlocks() - } - is MarketsTokenDetailsUM.Body.Content -> { - if (state.description != null) { - description(state.description) - } - - if (portfolioBlock != null) { - item(key = "portfolio") { - portfolioBlock(Modifier.blockPaddings()) - } - } - - if (isAccountEnabled) { - aboutCoinHeader() - } - - infoBlocksList(state.infoBlocks) - } - is MarketsTokenDetailsUM.Body.Error -> { - error(state) - } - MarketsTokenDetailsUM.Body.Nothing -> { - // Do nothing - } - } -} - -private fun LazyListScope.error(state: MarketsTokenDetailsUM.Body.Error) { - item("body-error") { - Box(Modifier.fillMaxWidth()) { - UnableToLoadData( - modifier = Modifier - .align(Alignment.Center) - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing40, - ), - onRetryClick = state.onLoadRetryClick, - ) - } - } -} - -private fun LazyListScope.aboutCoinHeader() { - item("aboutCoinHeader") { - Text( - modifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing20, - ), - text = stringResourceSafe(R.string.markets_about_coin_header), - color = TangemTheme.colors.text.primary1, - style = TangemTheme.typography.h3, - ) - } -} - -private fun LazyListScope.description(description: MarketsTokenDetailsUM.Description) { - item("description") { - DescriptionItem( - modifier = Modifier.blockPaddings(), - description = description.shortDescription, - hasFullDescription = description.fullDescription != null, - onReadMoreClick = description.onReadMoreClick, - ) - } -} - -internal fun LazyListScope.infoBlocksList(state: MarketsTokenDetailsUM.InformationBlocks) { - if (state.insights != null) { - item("insights") { - InsightsBlock( - modifier = Modifier.blockPaddings(), - state = state.insights, - ) - } - } - - if (state.securityScore != null) { - item("securityScore") { - SecurityScoreBlock( - modifier = Modifier.blockPaddings(), - state = state.securityScore, - ) - } - } - - if (state.metrics != null) { - item("metrics") { - MetricsBlock( - modifier = Modifier.blockPaddings(), - state = state.metrics, - ) - } - } - - if (state.pricePerformance != null) { - item("pricePerformance") { - PricePerformanceBlock( - modifier = Modifier.blockPaddings(), - state = state.pricePerformance, - ) - } - } - - item(key = "listedOn") { - ListedOnBlock( - state = state.listedOn, - modifier = Modifier.blockPaddings(), - ) - } - - if (state.links != null) { - item("links") { - LinksBlock( - modifier = Modifier.blockPaddings(), - state = state.links, - ) - } - } -} - -private fun LazyListScope.loadingInfoBlocks() { - item("insights-loading") { - InsightsBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("securityScore-loading") { - SecurityScoreBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("metrics-loading") { - MetricsBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("pricePerformance-loading") { - PricePerformanceBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item(key = "listedOn-loading") { - ListedOnBlockPlaceholder(modifier = Modifier.blockPaddings()) - } - - item("links-loading") { - LinksBlockPlaceholder(modifier = Modifier.blockPaddings()) - } -} - -@Composable -private fun Modifier.blockPaddings(): Modifier { - return this.padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing12, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt deleted file mode 100644 index 83be0ff031..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/MarketsTokenDetailsPreview.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.preview - -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.features.markets.details.impl.ui.state.* -import kotlinx.collections.immutable.persistentListOf - -internal object MarketsTokenDetailsPreview { - private val infoPoint = InfoPointUM( - title = stringReference("1"), - value = "2", - change = InfoPointUM.ChangeType.DOWN, - onInfoClick = {}, - ) - - val loadingState = MarketsTokenDetailsUM( - tokenName = "Token Name", - priceText = "$0.00000000324", - dateTimeText = stringReference("Today"), - priceChangePercentText = "52.00%", - iconUrl = "", - priceChangeType = PriceChangeType.UP, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = MarketChartDataProducer.build { }, - onLoadRetryClick = {}, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = { _, _ -> }, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = { }, - body = MarketsTokenDetailsUM.Body.Loading, - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - isMarkerSet = false, - triggerPriceChange = consumedEvent(), - ) - - val contentState = MarketsTokenDetailsUM( - tokenName = "Token Name", - priceText = "$0.00000000324", - dateTimeText = stringReference("Today"), - priceChangePercentText = "52.00%", - iconUrl = "", - priceChangeType = PriceChangeType.UP, - chartState = MarketsTokenDetailsUM.ChartState( - dataProducer = MarketChartDataProducer.build { }, - onLoadRetryClick = {}, - status = MarketsTokenDetailsUM.ChartState.Status.LOADING, - onMarkerPointSelected = { _, _ -> }, - ), - selectedInterval = PriceChangeInterval.H24, - onSelectedIntervalChange = { }, - body = MarketsTokenDetailsUM.Body.Content( - description = MarketsTokenDetailsUM.Description( - shortDescription = stringReference("markets_token_details_description_short"), - fullDescription = stringReference("markets_token_details_description_full"), - onReadMoreClick = {}, - ), - infoBlocks = MarketsTokenDetailsUM.InformationBlocks( - insights = InsightsUM( - h24Info = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - weekInfo = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - monthInfo = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - onInfoClick = {}, - onIntervalChanged = {}, - ), - securityScore = SecurityScoreUM( - score = 2.3f, - description = stringReference("markets_token_details_security_score_description"), - onInfoClick = {}, - ), - metrics = MetricsUM( - metrics = persistentListOf( - infoPoint, - infoPoint, - infoPoint, - ), - ), - pricePerformance = PricePerformanceUM( - h24 = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - month = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - all = PricePerformanceUM.Value( - low = "1", - high = "2", - indicatorFraction = 0.3f, - ), - onIntervalChanged = {}, - ), - listedOn = ListedOnUM.Empty, - links = null, - ), - ), - bottomSheetConfig = TangemBottomSheetConfig( - isShown = false, - onDismissRequest = {}, - content = TangemBottomSheetConfigContent.Empty, - ), - isMarkerSet = true, - triggerPriceChange = consumedEvent(), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt deleted file mode 100644 index d80e49af6c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/preview/SecurityScorePreviewData.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.preview - -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.details.impl.ui.state.SecurityScoreBottomSheetContent - -internal object SecurityScorePreviewData { - - val bottomSheetContent = SecurityScoreBottomSheetContent( - title = stringReference("Security score"), - description = stringReference( - "Security score of a token is a metric that assesses the " + - "security level of a blockchain or token based on various factors and is compiled from " + - "the sources listed below.", - ), - providers = listOf( - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Moralis", - lastAuditDate = "21.10.2024", - score = 4.9F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://moralis.com/", - rootHost = "moralis.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Certik", - lastAuditDate = "10.07.2024", - score = 4.6F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://certik.com/", - rootHost = "certik.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "Cyberscope", - lastAuditDate = "25.06.2023", - score = 4.5F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://cyberscope.com/", - rootHost = "cyberscope.com", - ), - iconUrl = "", - ), - SecurityScoreBottomSheetContent.SecurityScoreProviderUM( - name = "TokenInsight", - lastAuditDate = "17.01.2022", - score = 4.0F, - urlData = SecurityScoreBottomSheetContent.SecurityScoreProviderUM.UrlData( - fullUrl = "https://tokeninsight.com/", - rootHost = "tokeninsight.com", - ), - iconUrl = "", - ), - - ), - onProviderLinkClick = {}, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt deleted file mode 100644 index cccbf85d31..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ExchangesBottomSheetContent.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.StringRes -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.plus -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.impl.R -import com.tangem.utils.StringsSigns.DOT -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList - -/** - * Exchanges bottom sheet content - * -[REDACTED_AUTHOR] - */ -internal sealed interface ExchangesBottomSheetContent : TangemBottomSheetConfigContent { - - /** Title of bottom sheet. Like, app bar. */ - @get:StringRes - val titleResId: Int - get() = R.string.markets_token_details_exchanges_title - - /** Subtitle */ - @get:StringRes - val subtitleResId: Int - get() = R.string.markets_token_details_exchange - - /** Volume info */ - @get:StringRes - val volumeReference: TextReference - get() = resourceReference(id = R.string.markets_token_details_volume) + - stringReference(value = " $DOT ") + - resourceReference(id = R.string.markets_selector_interval_24h_title) - - /** Exchange items */ - val exchangeItems: ImmutableList - - /** - * Loading state - * - * @property exchangesCount count of exchanges - */ - data class Loading(val exchangesCount: Int) : ExchangesBottomSheetContent { - - override val exchangeItems: ImmutableList - get() = List(size = exchangesCount) { index -> TokenItemState.Loading(id = "loading#$index") } - .toImmutableList() - } - - /** - * Content state - * - * @property exchangeItems exchanges - */ - data class Content( - override val exchangeItems: ImmutableList, - ) : ExchangesBottomSheetContent - - /** Error state */ - data class Error( - val onRetryClick: () -> Unit, - ) : ExchangesBottomSheetContent { - override val exchangeItems: ImmutableList = persistentListOf() - - @StringRes - val message: Int = R.string.markets_loading_error_title - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt deleted file mode 100644 index d6af118609..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoBottomSheetContent.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference - -internal data class InfoBottomSheetContent( - val title: TextReference, - val body: TextReference, - val generatedAINotificationUM: GeneratedAINotificationUM? = null, -) : TangemBottomSheetConfigContent { - - data class GeneratedAINotificationUM(val onClick: () -> Unit) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt deleted file mode 100644 index 383db4e627..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InfoPointUM.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.extensions.TextReference - -internal data class InfoPointUM( - val title: TextReference, - val value: String, - val change: ChangeType? = null, - val onInfoClick: (() -> Unit)? = null, -) { - enum class ChangeType { - UP, DOWN - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt deleted file mode 100644 index e2d0b3270b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/InsightsUM.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.domain.markets.PriceChangeInterval -import kotlinx.collections.immutable.ImmutableList - -internal data class InsightsUM( - val h24Info: ImmutableList, - val weekInfo: ImmutableList, - val monthInfo: ImmutableList, - val onInfoClick: () -> Unit, - val onIntervalChanged: (PriceChangeInterval) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt deleted file mode 100644 index 5b4ea87e18..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/LinksUM.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.DrawableRes -import kotlinx.collections.immutable.ImmutableList - -internal data class LinksUM( - val officialLinks: ImmutableList, - val social: ImmutableList, - val repository: ImmutableList, - val blockchainSite: ImmutableList, - val onLinkClick: (Link) -> Unit, -) { - data class Link( - @DrawableRes val iconRes: Int, - val title: String, - val url: String, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt deleted file mode 100644 index a853675b10..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/ListedOnUM.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.pluralReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.markets.impl.R - -/** - * "Listed on" block UI model - * -[REDACTED_AUTHOR] - */ -internal sealed interface ListedOnUM { - - /** Title */ - val title: TextReference - get() = resourceReference(id = R.string.markets_token_details_listed_on) - - /** Description */ - val description: TextReference - - /** Empty state. No exchanges found */ - data object Empty : ListedOnUM { - override val description = resourceReference(id = R.string.markets_token_details_empty_exchanges) - } - - /** - * Content with number of exchanges - * - * @property onClick lambda be invoked when button is clicked - * @property amount amount of exchanges - */ - data class Content( - val onClick: () -> Unit, - private val amount: Int, - ) : ListedOnUM { - override val description: TextReference = pluralReference( - id = R.plurals.markets_token_details_amount_exchanges, - count = amount, - formatArgs = wrappedList(amount), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt deleted file mode 100644 index 94f581efd7..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MarketsTokenDetailsUM.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.charts.state.MarketChartDataProducer -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.markets.PriceChangeInterval -import java.math.BigDecimal - -internal data class MarketsTokenDetailsUM( - val tokenName: String, - val priceText: String, - val iconUrl: String?, - val dateTimeText: TextReference, - val priceChangePercentText: String?, - val priceChangeType: PriceChangeType, - val selectedInterval: PriceChangeInterval, - val isMarkerSet: Boolean, - val chartState: ChartState, - val onSelectedIntervalChange: (PriceChangeInterval) -> Unit, - val bottomSheetConfig: TangemBottomSheetConfig, - val triggerPriceChange: StateEvent, - val body: Body, -) { - - data class ChartState( - val status: Status, - val dataProducer: MarketChartDataProducer, - val onLoadRetryClick: () -> Unit, - val onMarkerPointSelected: (time: BigDecimal?, price: BigDecimal?) -> Unit, - ) { - enum class Status { - LOADING, ERROR, DATA - } - } - - data class InformationBlocks( - val insights: InsightsUM?, - val securityScore: SecurityScoreUM?, - val metrics: MetricsUM?, - val pricePerformance: PricePerformanceUM?, - val listedOn: ListedOnUM, - val links: LinksUM?, - ) - - @Immutable - sealed interface Body { - - data class Error( - val onLoadRetryClick: () -> Unit, - ) : Body - - data object Loading : Body - - data class Content( - val description: Description?, - val infoBlocks: InformationBlocks, - ) : Body - - data object Nothing : Body - } - - data class Description( - val shortDescription: TextReference, - val fullDescription: TextReference?, - val onReadMoreClick: () -> Unit, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt deleted file mode 100644 index 8b28533fb3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/MetricsUM.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import kotlinx.collections.immutable.ImmutableList - -internal data class MetricsUM( - val metrics: ImmutableList, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt deleted file mode 100644 index 9448472a0d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/PricePerformanceUM.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.FloatRange -import com.tangem.domain.markets.PriceChangeInterval - -internal data class PricePerformanceUM( - val h24: Value, - val month: Value, - val all: Value, - val onIntervalChanged: (PriceChangeInterval) -> Unit, -) { - data class Value( - val low: String, - val high: String, - @FloatRange(from = 0.0, to = 1.0) val indicatorFraction: Float, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt deleted file mode 100644 index ceb100bdfb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreBottomSheetContent.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference - -internal data class SecurityScoreBottomSheetContent( - val title: TextReference, - val description: TextReference, - val providers: List, - val onProviderLinkClick: (SecurityScoreProviderUM) -> Unit, -) : TangemBottomSheetConfigContent { - - data class SecurityScoreProviderUM( - val name: String, - val lastAuditDate: String?, - val score: Float, - val urlData: UrlData?, - val iconUrl: String?, - ) { - data class UrlData( - val fullUrl: String, - val rootHost: String?, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt deleted file mode 100644 index d4ebd3a9ee..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/ui/state/SecurityScoreUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.details.impl.ui.state - -import androidx.annotation.FloatRange -import com.tangem.core.ui.extensions.TextReference - -internal data class SecurityScoreUM( - @FloatRange(from = 0.0, to = 5.0) val score: Float, - val description: TextReference, - val onInfoClick: () -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt deleted file mode 100644 index 9f441298d5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/DefaultMarketsEntryComponent.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.tangem.features.markets.entry.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.State -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.ExperimentalDecomposeApi -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.popWhile -import com.arkivanov.decompose.value.Value -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.navigation.inner.InnerRouter -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.MarketsEntryComponent -import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory.Child -import com.tangem.features.markets.entry.impl.ui.EntryBottomSheetContent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Stable -internal class DefaultMarketsEntryComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - private val marketsEntryChildFactory: MarketsEntryChildFactory, -) : MarketsEntryComponent, AppComponentContext by context { - - private val stackNavigation = StackNavigation() - - private val innerRouter = InnerRouter( - stackNavigation = stackNavigation, - popCallback = { onChildBack() }, - ) - - private val stack: Value> = childStack( - key = "main", - source = stackNavigation, - serializer = Child.serializer(), - initialConfiguration = Child.TokenList, - handleBackButton = false, - childFactory = { configuration, factoryContext -> - marketsEntryChildFactory.createChild( - child = configuration, - appComponentContext = childByContext( - componentContext = factoryContext, - router = innerRouter, - ), - onTokenClick = ::marketsListTokenSelected, - ) - }, - ) - - @Suppress("LongMethod") - @Composable - override fun BottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - modifier: Modifier, - ) { - EntryBottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - stackState = stack.subscribeAsState(), - modifier = modifier, - ) - } - - @OptIn(ExperimentalDecomposeApi::class) - private fun marketsListTokenSelected(token: TokenMarketParams, appCurrency: AppCurrency) { - innerRouter.push( - route = Child.TokenDetails( - params = MarketsTokenDetailsComponent.Params( - token = token, - appCurrency = appCurrency, - shouldShowPortfolio = true, - analyticsParams = MarketsTokenDetailsComponent.AnalyticsParams( - blockchain = null, - source = "Market", - ), - ), - ), - ) - } - - private fun onChildBack() { - if (stack.value.active.configuration !is Child.TokenList) { - stackNavigation.popWhile { it != Child.TokenList } - } - } - - @AssistedFactory - interface Factory : MarketsEntryComponent.Factory { - override fun create(context: AppComponentContext): DefaultMarketsEntryComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt deleted file mode 100644 index 785ef74cdb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/MarketsEntryChildFactory.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.features.markets.entry.impl - -import androidx.compose.runtime.Immutable -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.navigation.Route -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.tokenlist.MarketsTokenListComponent -import kotlinx.serialization.Serializable -import javax.inject.Inject - -internal class MarketsEntryChildFactory @Inject constructor( - private val tokenListComponentFactory: MarketsTokenListComponent.FactoryBottomSheet, - private val tokenDetailsComponentFactory: MarketsTokenDetailsComponent.Factory, -) { - - @Serializable - @Immutable - sealed interface Child : Route { - - @Serializable - @Immutable - data object TokenList : Child - - @Serializable - @Immutable - data class TokenDetails(val params: MarketsTokenDetailsComponent.Params) : Child - } - - fun createChild( - child: Child, - appComponentContext: AppComponentContext, - onTokenClick: (TokenMarketParams, AppCurrency) -> Unit, - ): Any { - return when (child) { - is Child.TokenDetails -> { - tokenDetailsComponentFactory.create( - context = appComponentContext, - params = child.params, - ) - } - is Child.TokenList -> { - tokenListComponentFactory.create( - context = appComponentContext, - params = Unit, - onTokenClick = onTokenClick, - ) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt deleted file mode 100644 index 4603041300..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/di/ComponentModule.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.entry.impl.di - -import com.tangem.features.markets.entry.MarketsEntryComponent -import com.tangem.features.markets.entry.impl.DefaultMarketsEntryComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface ComponentModule { - - @Binds - @Singleton - fun bindMarketsEntryComponent(factory: DefaultMarketsEntryComponent.Factory): MarketsEntryComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt deleted file mode 100644 index 7814672735..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/entry/impl/ui/EntryBottomSheetContent.kt +++ /dev/null @@ -1,128 +0,0 @@ -package com.tangem.features.markets.entry.impl.ui - -import androidx.compose.animation.Animatable -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.AnimationVector4D -import androidx.compose.animation.core.tween -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.slide -import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation -import com.arkivanov.decompose.router.stack.ChildStack -import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.markets.details.MarketsTokenDetailsComponent -import com.tangem.features.markets.entry.impl.MarketsEntryChildFactory -import com.tangem.features.markets.tokenlist.MarketsTokenListComponent - -@Composable -internal fun EntryBottomSheetContent( - bottomSheetState: State, - onHeaderSizeChange: (Dp) -> Unit, - stackState: State>, - modifier: Modifier = Modifier, -) { - val primary = TangemTheme.colors.background.primary - val backgroundColor = remember { Animatable(primary) } - - LocalMainBottomSheetColor.current.value = backgroundColor.value - - Children( - stack = stackState.value, - animation = stackAnimation(slide()), - modifier = modifier, - ) { child -> - when (child.configuration) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - (child.instance as MarketsTokenDetailsComponent).BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = Modifier, - ) - } - is MarketsEntryChildFactory.Child.TokenList -> { - (child.instance as MarketsTokenListComponent).BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = Modifier, - ) - } - } - } - - val activeChild = stackState.value.active.configuration - - BackgroundColorEffects( - activeChild = activeChild, - backgroundColor = backgroundColor, - bottomSheetState = bottomSheetState, - ) -} - -@Composable -private fun BackgroundColorEffects( - activeChild: MarketsEntryChildFactory.Child, - backgroundColor: Animatable, - bottomSheetState: State, -) { - val primary = TangemTheme.colors.background.primary - val tertiary = TangemTheme.colors.background.tertiary - - // Order of LaunchedEffects is important here - - LaunchedEffect(activeChild) { - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.animateTo( - tertiary, - animationSpec = tween(durationMillis = 500), - ) - } - is MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 500), - ) - } - } - } - - LaunchedEffect(bottomSheetState.value) { - if (activeChild is MarketsEntryChildFactory.Child.TokenDetails) { - when (bottomSheetState.value) { - BottomSheetState.EXPANDED -> { - backgroundColor.animateTo( - tertiary, - animationSpec = tween(durationMillis = 100), - ) - } - BottomSheetState.COLLAPSED -> { - backgroundColor.animateTo( - primary, - animationSpec = tween(durationMillis = 100), - ) - } - } - } - } - - LaunchedEffect(primary, tertiary) { - if (backgroundColor.isRunning) return@LaunchedEffect - - when (activeChild) { - is MarketsEntryChildFactory.Child.TokenDetails -> { - backgroundColor.snapTo(tertiary) - } - is MarketsEntryChildFactory.Child.TokenList -> { - backgroundColor.snapTo(primary) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt deleted file mode 100644 index f35084b04a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioComponent.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent - -internal interface AddToPortfolioComponent : ComposableBottomSheetComponent { - - data class Params( - val addToPortfolioManager: AddToPortfolioManager, - val callback: Callback, - ) - - interface Callback { - fun onDismiss() - } - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt deleted file mode 100644 index 28daa1389c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AddToPortfolioManager.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent.AnalyticsParams -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow - -internal interface AddToPortfolioManager { - - val token: TokenMarketParams - val analyticsParams: AnalyticsParams? - val portfolioFetcher: PortfolioFetcher - - val state: StateFlow - - val allAvailableNetworks: Flow> - fun setTokenNetworks(networks: List) - - sealed interface State { - data object Init : State - data class AvailableToAdd( - val availableToAddData: AvailableToAddData, - ) : State - - data object NothingToAdd : State - } - - interface Factory { - fun create( - scope: CoroutineScope, - token: TokenMarketParams, - analyticsParams: AnalyticsParams?, - ): AddToPortfolioManager - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt deleted file mode 100644 index 976ad0a6c4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/api/AvailableToAddData.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.features.markets.portfolio.add.api - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.serialization.Serializable - -internal data class AvailableToAddData( - val availableToAddWallets: Map, -) { - val isAvailableToAdd: Boolean = availableToAddWallets.values.any { item -> item.isAvailableToAdd } - val isSinglePortfolio: Boolean - get() = availableToAddWallets.size == 1 && availableToAddWallets.values.first().accounts.size == 1 -} - -internal data class AvailableToAddWallet( - val userWallet: UserWallet, - val accounts: List, - val availableNetworks: Set, - val availableToAddAccounts: Map, -) { - val isAvailableToAdd: Boolean = availableToAddAccounts.values.any { item -> item.isAvailableToAdd } -} - -@Serializable -internal data class AvailableToAddAccount( - val account: AccountStatus, - val availableNetworks: Set, - val addedNetworks: Set, -) { - - val isSingleNetwork: Boolean - get() = availableNetworks.size == 1 - - val availableToAddNetworks: Set = availableNetworks - .filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } } - .toSet() - - val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty() - - val addedMarketNetworks: Set = availableNetworks - .filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } } - .toSet() -} - -@Serializable -internal data class SelectedPortfolio( - val userWallet: UserWallet, - val account: AvailableToAddAccount, - val isAccountMode: Boolean, - val hasMorePortfoliosAvailable: Boolean, -) - -internal data class SelectedNetwork( - val selectedNetwork: TokenMarketInfo.Network, - val cryptoCurrency: CryptoCurrency, - val hasMoreNetworksAvailable: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt deleted file mode 100644 index ed74502649..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/AddTokenComponent.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.features.markets.portfolio.add.api.SelectedNetwork -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel -import com.tangem.common.ui.addtoken.AddTokenContent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow - -internal class AddTokenComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: AddTokenModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state = model.uiState.collectAsStateWithLifecycle() - val um = state.value ?: return - AddTokenContent( - modifier = modifier, - state = um, - ) - } - - data class Params( - val eventBuilder: PortfolioAnalyticsEvent.EventBuilder, - val selectedPortfolio: Flow, - val selectedNetwork: Flow, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onChangeNetworkClick() - fun onChangePortfolioClick() - fun onTokenAdded(status: CryptoCurrencyStatus) - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): AddTokenComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt deleted file mode 100644 index e954b972bd..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ChooseNetworkComponent.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.model.ChooseNetworkModel -import com.tangem.features.markets.portfolio.add.impl.ui.ChooseNetworkContent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class ChooseNetworkComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: ChooseNetworkModel = getOrCreateModel(params) - - @Composable - override fun Content(modifier: Modifier) { - val state by model.uiState.collectAsStateWithLifecycle() - ChooseNetworkContent(state) - } - - data class Params( - val selectedPortfolio: SelectedPortfolio, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onNetworkSelected(network: TokenMarketInfo.Network) - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): ChooseNetworkComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt deleted file mode 100644 index 81c2f24a63..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/DefaultAddToPortfolioComponent.kt +++ /dev/null @@ -1,210 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.stack.ChildStack -import com.arkivanov.decompose.router.stack.backStack -import com.arkivanov.decompose.router.stack.childStack -import com.arkivanov.decompose.router.stack.pop -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.child -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.account.PortfolioSelectorComponent -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent.Params -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioModel -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioRoutes -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -internal class DefaultAddToPortfolioComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, - portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, - addTokenComponentFactory: AddTokenComponent.Factory, - tokenActionsComponentFactory: TokenActionsComponent.Factory, - private val chooseNetworkComponentFactory: ChooseNetworkComponent.Factory, -) : AppComponentContext by context, AddToPortfolioComponent { - - private val model: AddToPortfolioModel = getOrCreateModel(params) - - private val portfolioSelectorComponent: PortfolioSelectorComponent = portfolioSelectorComponentFactory.create( - context = child("portfolioSelectorComponent"), - params = PortfolioSelectorComponent.Params( - portfolioFetcher = model.portfolioFetcher, - controller = model.portfolioSelectorController, - ), - ) - - private val addTokenComponent: AddTokenComponent = addTokenComponentFactory.create( - context = child("addTokenComponent"), - params = AddTokenComponent.Params( - eventBuilder = model.eventBuilder, - callbacks = model, - selectedPortfolio = model.selectedPortfolio, - selectedNetwork = model.selectedNetwork, - ), - ) - - private val tokenActionsComponent: TokenActionsComponent = tokenActionsComponentFactory.create( - context = child("tokenActionsComponent"), - params = TokenActionsComponent.Params( - eventBuilder = model.eventBuilder, - callbacks = model, - data = model.tokenActionsData, - ), - ) - - private val childStack = childStack( - key = "addToPortfolioStack", - handleBackButton = true, - source = model.navigation, - serializer = AddToPortfolioRoutes.serializer(), - initialStack = { model.currentStack }, - childFactory = ::contentChild, - ) - - private fun onBack() { - if (childStack.backStack.isNotEmpty()) model.navigation.pop() else dismiss() - } - - override fun dismiss() { - params.callback.onDismiss() - } - - @Composable - override fun BottomSheet() { - val stack by childStack.subscribeAsState() - val contentStack = remember { mutableStateOf(stack) } - val currentRoute = stack.active.configuration - val isNotEmpty = currentRoute != AddToPortfolioRoutes.Empty - if (isNotEmpty) { - contentStack.value = stack - } - - TangemModalBottomSheet( - scrollableContent = false, - onBack = ::onBack, - config = TangemBottomSheetConfig( - isShown = isNotEmpty, - onDismissRequest = ::dismiss, - content = TangemBottomSheetConfigContent.Empty, - ), - containerColor = TangemTheme.colors.background.tertiary, - title = { state -> - AnimatedContent(targetState = contentStack.value) { stack -> - BottomSheetTitle( - stack = stack, - onBackClick = ::onBack, - modifier = Modifier.fillMaxWidth(), - ) - } - }, - content = { state -> - AnimatedContent(targetState = contentStack.value) { stack -> - val paddingModifier = Modifier.padding( - start = 16.dp, - end = 16.dp, - bottom = 16.dp, - ) - val isScrollableContent = when (stack.active.configuration) { - AddToPortfolioRoutes.PortfolioSelector -> false - AddToPortfolioRoutes.AddToken, - AddToPortfolioRoutes.Empty, - is AddToPortfolioRoutes.NetworkSelector, - AddToPortfolioRoutes.TokenActions, - -> true - } - if (isScrollableContent) { - Column( - modifier = paddingModifier.verticalScroll(rememberScrollState()), - ) { - stack.active.instance.Content(modifier = Modifier) - } - } else { - stack.active.instance.Content(modifier = paddingModifier) - } - } - }, - ) - } - - @Composable - private fun BottomSheetTitle( - stack: ChildStack, - onBackClick: (() -> Unit), - modifier: Modifier = Modifier, - ) { - val title: TextReference = when (stack.active.configuration) { - AddToPortfolioRoutes.AddToken -> resourceReference(R.string.common_add_token) - AddToPortfolioRoutes.Empty -> TextReference.EMPTY - is AddToPortfolioRoutes.NetworkSelector -> resourceReference(R.string.common_choose_network) - AddToPortfolioRoutes.TokenActions -> resourceReference(R.string.common_get_token) - AddToPortfolioRoutes.PortfolioSelector -> (stack.active.instance as PortfolioSelectorComponent) - .title.collectAsStateWithLifecycle().value - } - val startIconRes: Int? - val endIconRes: Int? - if (stack.backStack.isNotEmpty()) { - startIconRes = R.drawable.ic_back_24 - endIconRes = null - } else { - startIconRes = null - endIconRes = R.drawable.ic_close_24 - } - TangemModalBottomSheetTitle( - modifier = modifier, - title = title, - startIconRes = startIconRes, - endIconRes = endIconRes, - onStartClick = onBackClick, - onEndClick = onBackClick, - ) - } - - private fun contentChild( - config: AddToPortfolioRoutes, - componentContext: ComponentContext, - ): ComposableContentComponent = when (config) { - AddToPortfolioRoutes.AddToken -> addTokenComponent - AddToPortfolioRoutes.PortfolioSelector -> portfolioSelectorComponent - AddToPortfolioRoutes.TokenActions -> tokenActionsComponent - AddToPortfolioRoutes.Empty -> ComposableContentComponent.EMPTY - is AddToPortfolioRoutes.NetworkSelector -> chooseNetworkComponentFactory.create( - context = childByContext(componentContext), - params = ChooseNetworkComponent.Params( - selectedPortfolio = config.selectedPortfolio, - callbacks = model, - ), - ) - } - - @AssistedFactory - interface Factory : AddToPortfolioComponent.Factory { - override fun create(context: AppComponentContext, params: Params): DefaultAddToPortfolioComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt deleted file mode 100644 index 93bf26a76f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/TokenActionsComponent.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.features.markets.portfolio.add.impl.model.TokenActionsModel -import com.tangem.features.markets.portfolio.add.impl.ui.TokenActionsContent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.tokenreceive.TokenReceiveComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.Flow - -internal class TokenActionsComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: Params, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, -) : AppComponentContext by context, ComposableContentComponent { - - private val model: TokenActionsModel = getOrCreateModel(params) - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = TokenReceiveConfig.serializer(), - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - @Composable - override fun Content(modifier: Modifier) { - val state = model.uiState.collectAsStateWithLifecycle() - val bottomSheet by bottomSheetSlot.subscribeAsState() - val tokenActionsUM = state.value ?: return - TokenActionsContent( - modifier = modifier, - state = tokenActionsUM, - ) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: TokenReceiveConfig, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - - data class Params( - val eventBuilder: PortfolioAnalyticsEvent.EventBuilder, - val data: Flow, - val callbacks: Callbacks, - ) - - interface Callbacks { - fun onLaterClick() - } - - @AssistedFactory - interface Factory : ComponentFactory { - override fun create(context: AppComponentContext, params: Params): TokenActionsComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt deleted file mode 100644 index 3a311b6854..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.converter - -import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase -import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase -import com.tangem.domain.markets.GetTokenMarketCryptoCurrency -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.account.AccountStatus -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.add.api.AvailableToAddAccount -import com.tangem.features.markets.portfolio.add.api.AvailableToAddData -import com.tangem.features.markets.portfolio.add.api.AvailableToAddWallet -import javax.inject.Inject - -internal class AvailableToAddDataConverter @Inject constructor( - private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, - private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, - private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, -) { - - suspend fun convert( - balances: Map, - availableNetworks: Set, - marketParams: TokenMarketParams, - ): AvailableToAddData { - suspend fun AccountStatus.getAvailableToAddAccount(wallet: UserWallet): AvailableToAddAccount? { - val currencies = availableNetworks - .mapNotNull { network -> - createCryptoCurrency( - userWallet = wallet, - network = network, - marketParams = marketParams, - account = this.account, - ) - } - - if (currencies.isEmpty()) return null - - val addedNetworks = getAccountCurrencyStatusUseCase.invokeSync(wallet.walletId, currencies) - .fold( - ifEmpty = { emptySet() }, - ifSome = { map -> - map.values.flatMapTo(hashSetOf()) { statuses -> - statuses.map { it.currency.network } - } - }, - ) - - return AvailableToAddAccount( - account = this, - availableNetworks = availableNetworks, - addedNetworks = addedNetworks, - ) - } - - suspend fun getAvailableToAddWallet( - entry: Map.Entry, - ): AvailableToAddWallet { - val (_, balance) = entry - val wallet = balance.userWallet - val filteredNetworks = wallet.filteredAvailableNetworks(availableNetworks) - val accounts = balance.accountsBalance.accountStatuses - val availableToAddAccounts: Map = accounts - .mapNotNull { accountStatus -> - val availableToAddAccount = accountStatus.getAvailableToAddAccount(wallet) ?: return@mapNotNull null - accountStatus.account.accountId to availableToAddAccount - } - .toMap() - return AvailableToAddWallet( - userWallet = wallet, - accounts = accounts, - availableNetworks = filteredNetworks, - availableToAddAccounts = availableToAddAccounts, - ) - } - - val availableToAddWallets: Map = balances - .map { entry -> - val (walletId, _) = entry - val availableToAddWallet = getAvailableToAddWallet(entry) - walletId to availableToAddWallet - } - .filter { (_, wallet) -> wallet.availableToAddAccounts.isNotEmpty() } - .toMap() - - return AvailableToAddData( - availableToAddWallets = availableToAddWallets, - ) - } - - private fun UserWallet.filteredAvailableNetworks(networks: Set) = - filterAvailableNetworksForWalletUseCase( - userWalletId = this.walletId, - networks = networks, - ) - - private suspend fun createCryptoCurrency( - userWallet: UserWallet, - network: TokenMarketInfo.Network, - marketParams: TokenMarketParams, - account: Account, - ): CryptoCurrency? { - val derivationIndex = when (account) { - is Account.CryptoPortfolio -> account.derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - return getTokenMarketCryptoCurrency( - userWalletId = userWallet.walletId, - tokenMarketParams = marketParams, - network = network, - accountIndex = derivationIndex, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt deleted file mode 100644 index 39e8de3e0b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioComponentModule.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.di - -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager -import com.tangem.features.markets.portfolio.add.impl.DefaultAddToPortfolioComponent -import com.tangem.features.markets.portfolio.add.impl.ui.DefaultAddToPortfolioManager -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal interface AddToPortfolioComponentModule { - - @Binds - fun bindAddToPortfolioComponent(factory: DefaultAddToPortfolioComponent.Factory): AddToPortfolioComponent.Factory - - @Binds - fun bindAddToPortfolioManagerFactory(factory: DefaultAddToPortfolioManager.Factory): AddToPortfolioManager.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt deleted file mode 100644 index b093d471f6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/di/AddToPortfolioModelModule.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.portfolio.add.impl.model.AddToPortfolioModel -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenModel -import com.tangem.features.markets.portfolio.add.impl.model.ChooseNetworkModel -import com.tangem.features.markets.portfolio.add.impl.model.TokenActionsModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface AddToPortfolioModelModule { - - @Binds - @IntoMap - @ClassKey(AddTokenModel::class) - fun addTokenModel(model: AddTokenModel): Model - - @Binds - @IntoMap - @ClassKey(AddToPortfolioModel::class) - fun addToPortfolioModel(model: AddToPortfolioModel): Model - - @Binds - @IntoMap - @ClassKey(TokenActionsModel::class) - fun tokenActionsModel(model: TokenActionsModel): Model - - @Binds - @IntoMap - @ClassKey(ChooseNetworkModel::class) - fun chooseNetworkModel(model: ChooseNetworkModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt deleted file mode 100644 index 142c66cf7a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioModel.kt +++ /dev/null @@ -1,381 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.arkivanov.decompose.router.stack.StackNavigation -import com.arkivanov.decompose.router.stack.popToFirst -import com.arkivanov.decompose.router.stack.pushNew -import com.arkivanov.decompose.router.stack.replaceAll -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.markets.GetTokenMarketCryptoCurrency -import com.tangem.domain.markets.TokenMarketInfo -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.wallet.UserWallet -import com.tangem.features.account.PortfolioSelectorController -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.* -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import com.tangem.features.markets.portfolio.add.impl.ChooseNetworkComponent -import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.model.PortfolioTokenUMConverter.Companion.toQuickActions -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.Job -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject - -private const val TOKEN_ACTIONS_DELAY = 500L - -@ModelScoped -@Suppress("LongParameterList") -internal class AddToPortfolioModel @Inject constructor( - paramsContainer: ParamsContainer, - override val dispatchers: CoroutineDispatcherProvider, - private val callbackDelegate: AddToPortfolioCallbackDelegate, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, - private val getTokenMarketCryptoCurrency: GetTokenMarketCryptoCurrency, - private val messageSender: UiMessageSender, - private val analyticsEventHandler: AnalyticsEventHandler, - val portfolioSelectorController: PortfolioSelectorController, -) : Model(), - ChooseNetworkComponent.Callbacks by callbackDelegate, - TokenActionsComponent.Callbacks by callbackDelegate, - AddTokenComponent.Callbacks by callbackDelegate { - - private val params = paramsContainer.require() - val navigation = StackNavigation() - var currentStack = listOf(AddToPortfolioRoutes.Empty) - - /* Flows that hold state and provide it to child models */ - val selectedNetwork: MutableSharedFlow = replayMutableSharedFlow() - val selectedPortfolio: MutableSharedFlow = replayMutableSharedFlow() - val tokenActionsData: MutableSharedFlow = replayMutableSharedFlow() - - private val addToPortfolioManager = params.addToPortfolioManager - val portfolioFetcher = addToPortfolioManager.portfolioFetcher - val eventBuilder = PortfolioAnalyticsEvent.EventBuilder( - token = addToPortfolioManager.token, - source = addToPortfolioManager.analyticsParams?.source, - ) - - val featureData: Flow = combineFeatureData() - - init { - navigation.subscribe { currentStack = it.transformer.invoke(currentStack) } - startAddToPortfolioFlow() - } - - private fun replayMutableSharedFlow() = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - @Suppress("LongMethod") - private fun startAddToPortfolioFlow() { - channelFlow { - fun finishFlow() { - params.callback.onDismiss() - channel.close() - } - val featureDataFlow: StateFlow = featureData - .filterIsInstance() - .map { it.availableToAddData } - .distinctUntilChanged() - .stateIn(this) - val isAccountMode = portfolioSelectorController.isAccountModeSync() - - // use snapshot data, looks like we don’t need to remap at runtime - val data = featureDataFlow.value - - // you must control it via [AddToPortfolioManager.state] - if (!data.isAvailableToAdd) { - finishFlow() - return@channelFlow - } - - // init data flows, emits on user/code selection, updates state holder - val firstSelectedPortfolio = setupPortfolioFlow(data) - .onEach { selectedPortfolio.emit(it) } - val firstSelectedNetwork = setupNetworkFlow(firstSelectedPortfolio) - .onEach { selectedNetwork.emit(it) } - - val isSinglePortfolio = data.isSinglePortfolio - if (isSinglePortfolio) { - val accountId = data.availableToAddWallets.values.first() - .availableToAddAccounts.values.first() - .account.account.accountId - // force select a portfolio, triggers [selectedPortfolio] - portfolioSelectorController.selectAccount(accountId) - } else { - logAccountSelector(isAccountMode) - navigation.replaceAll(AddToPortfolioRoutes.PortfolioSelector) - } - - val firstPartOfNavigation: Job = firstSelectedPortfolio - .onEach { portfolio -> - val isSingleAvailableNetwork = portfolio.account.isSingleNetwork - when { - // force select a network, triggers [selectedNetwork] - isSingleAvailableNetwork -> { - val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) - } - // it's important to control root screen, UI depends on it(close/arrow icon) - isSinglePortfolio -> navigation.replaceAll(routeToNetworkSelector(portfolio)) - else -> navigation.pushNew(routeToNetworkSelector(portfolio)) - } - } - .launchIn(this) - - // main flow that combine all require data - val allRequireForAdd = combine( - flow = firstSelectedNetwork, - flow2 = firstSelectedPortfolio, - transform = { a, b -> a to b }, - ) - - // suspend until all required data is selected - allRequireForAdd.first() - // line of navigation to AddToken screen is finished; cancel the job, select a new root screen - firstPartOfNavigation.cancel() - navigation.replaceAll(AddToPortfolioRoutes.AddToken) - - var middleNavigationJob: Job? = null - // handle actions from AddToken screen - callbackDelegate.onChangeNetworkClick.receiveAsFlow() - .onEach { - middleNavigationJob?.cancel() - middleNavigationJob = changeNetworkNavigationFlow() - .launchIn(this) - val route = routeToNetworkSelector(selectedPortfolio.first()) - navigation.pushNew(route) - } - .launchIn(this) - // handle actions from AddToken screen - callbackDelegate.onChangePortfolioClick.receiveAsFlow() - .onEach { - middleNavigationJob?.cancel() - middleNavigationJob = changePortfolioNavigationFlow(data).launchIn(this) - logAccountSelector(isAccountMode) - navigation.pushNew(AddToPortfolioRoutes.PortfolioSelector) - } - .launchIn(this) - - // suspend until token is added - val addedToken = callbackDelegate.onTokenAdded.receiveAsFlow().first() - middleNavigationJob?.cancel() - val selectedPortfolio = selectedPortfolio.first() - - messageSender.send(ToastMessage(message = resourceReference(R.string.markets_token_added))) - - setupTokenActionsFlow(selectedPortfolio, addedToken) - .onEach { cryptoCurrencyData -> - tokenActionsData.emit(cryptoCurrencyData) - navigation.replaceAll(AddToPortfolioRoutes.TokenActions) - } - .onEmpty { finishFlow() } - .launchIn(this) - - callbackDelegate.onLaterClick.receiveAsFlow().first() - finishFlow() - } - .catch { error -> - Timber.e(error) - params.callback.onDismiss() - } - .launchIn(modelScope) - } - - private fun logAccountSelector(isAccountMode: Boolean) { - if (isAccountMode) { - analyticsEventHandler.send(eventBuilder.popupToChooseAccount()) - } - } - - private fun changeNetworkNavigationFlow(): Flow { - return setupNetworkFlow(selectedPortfolio) - .onEach { newNetwork -> - selectedNetwork.emit(newNetwork) - navigation.popToFirst() - } - } - - private fun changePortfolioNavigationFlow(data: AvailableToAddData): Flow = flow { - val selectedPortfolioValue = selectedPortfolio.first() - val selectedAccount = selectedPortfolioValue.account.account.account.accountId - portfolioSelectorController.selectAccount(selectedAccount) - val changedPortfolio = setupPortfolioFlow(data) - .drop(1) - .onEach { portfolio -> - val isSingleAvailableNetwork = portfolio.account.isSingleNetwork - if (isSingleAvailableNetwork) { - val singleNetwork = portfolio.account.availableToAddNetworks.first() - callbackDelegate.onNetworkSelected(singleNetwork) - } else { - navigation.pushNew(routeToNetworkSelector(portfolio)) - } - } - val changedNetwork = setupNetworkFlow(changedPortfolio) - combine( - flow = changedPortfolio, - flow2 = changedNetwork, - transform = { newPortfolio, newNetwork -> - selectedPortfolio.tryEmit(newPortfolio) - selectedNetwork.tryEmit(newNetwork) - navigation.popToFirst() - }, - ).collect { emit(it) } - } - - private fun setupTokenActionsFlow( - selectedPortfolio: SelectedPortfolio, - addedToken: CryptoCurrencyStatus, - ): Flow { - val timeFlow = channelFlow { - val timerJob = launch { delay(TOKEN_ACTIONS_DELAY) } - getCryptoCurrencyActionsUseCase( - currency = addedToken.currency, - accountId = selectedPortfolio.account.account.account.accountId, - ).onEach { state -> - val requestedQuickActions = toQuickActions(state.states) - when { - requestedQuickActions.isNotEmpty() -> { - timerJob.cancel() - send(state) - } - // wait any requestedQuickActions while timer active - timerJob.isActive -> Unit - else -> close() - } - }.collect() - } - return timeFlow.map { actionsState -> - PortfolioData.CryptoCurrencyData( - userWallet = selectedPortfolio.userWallet, - status = actionsState.cryptoCurrencyStatus, - actions = actionsState.states, - ) - } - } - - private fun setupPortfolioFlow(data: AvailableToAddData): Flow = combine( - flow = portfolioSelectorController.isAccountMode, - flow2 = portfolioSelectorController.selectedAccount, - transform = { isAccountMode, selectedAccountId -> - selectedAccountId ?: return@combine null - val availableToAddWallets = - data.availableToAddWallets[selectedAccountId.userWalletId] ?: return@combine null - val availableToAddAccount = - availableToAddWallets.availableToAddAccounts[selectedAccountId] ?: return@combine null - if (!isAccountMode) analyticsEventHandler.send(eventBuilder.addToPortfolioWalletChanged()) - SelectedPortfolio( - isAccountMode = isAccountMode, - userWallet = availableToAddWallets.userWallet, - account = availableToAddAccount, - hasMorePortfoliosAvailable = !data.isSinglePortfolio, - ) - }, - ) - .filterNotNull() - - private fun setupNetworkFlow(selectedPortfolioFlow: Flow): Flow = combine( - flow = selectedPortfolioFlow, - flow2 = callbackDelegate.onNetworkSelected.receiveAsFlow(), - transform = transform@{ selectedPortfolio, selectedNetwork -> - SelectedNetwork( - cryptoCurrency = createCryptoCurrency( - userWallet = selectedPortfolio.userWallet, - network = selectedNetwork, - account = selectedPortfolio.account, - ) ?: return@transform null, - selectedNetwork = selectedNetwork, - hasMoreNetworksAvailable = !selectedPortfolio.account.isSingleNetwork, - ) - }, - ) - .filterNotNull() - - private suspend fun createCryptoCurrency( - userWallet: UserWallet, - network: TokenMarketInfo.Network, - account: AvailableToAddAccount, - ): CryptoCurrency? { - val accountIndex = when (account.account) { - is AccountStatus.CryptoPortfolio -> account.account.account.derivationIndex - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } - return getTokenMarketCryptoCurrency( - userWalletId = userWallet.walletId, - tokenMarketParams = addToPortfolioManager.token, - network = network, - accountIndex = accountIndex, - ) - } - - private fun routeToNetworkSelector(portfolio: SelectedPortfolio): AddToPortfolioRoutes.NetworkSelector { - return AddToPortfolioRoutes.NetworkSelector(selectedPortfolio = portfolio) - } - - private fun combineFeatureData() = addToPortfolioManager.state.onEach { state -> - when (state) { - is AddToPortfolioManager.State.AvailableToAdd -> - portfolioSelectorController.isEnabled.value = isEnabled@{ userWallet, accountStatus -> - val availableWallet = state.availableToAddData.availableToAddWallets[userWallet.walletId] - ?: return@isEnabled false - val isAvailableAccount = - availableWallet.availableToAddAccounts[accountStatus.account.accountId] - ?.isAvailableToAdd == true - return@isEnabled isAvailableAccount - } - AddToPortfolioManager.State.Init, - AddToPortfolioManager.State.NothingToAdd, - -> Unit - } - } -} - -@ModelScoped -internal class AddToPortfolioCallbackDelegate @Inject constructor() : - ChooseNetworkComponent.Callbacks, - TokenActionsComponent.Callbacks, - AddTokenComponent.Callbacks { - - val onNetworkSelected = Channel() - val onLaterClick = Channel() - val onChangeNetworkClick = Channel() - val onChangePortfolioClick = Channel() - val onTokenAdded = Channel() - - override fun onNetworkSelected(network: TokenMarketInfo.Network) { - onNetworkSelected.trySend(network) - } - - override fun onLaterClick() { - onLaterClick.trySend(Unit) - } - - override fun onChangeNetworkClick() { - onChangeNetworkClick.trySend(Unit) - } - - override fun onChangePortfolioClick() { - onChangePortfolioClick.trySend(Unit) - } - - override fun onTokenAdded(status: CryptoCurrencyStatus) { - onTokenAdded.trySend(status) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt deleted file mode 100644 index 4f46fac1d3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddToPortfolioRoutes.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import androidx.compose.runtime.Immutable -import com.tangem.core.decompose.navigation.Route -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import kotlinx.serialization.Serializable - -@Serializable -@Immutable -internal sealed interface AddToPortfolioRoutes : Route { - - @Serializable - data object Empty : AddToPortfolioRoutes - - @Serializable - data object PortfolioSelector : AddToPortfolioRoutes - - @Serializable - data class NetworkSelector( - val selectedPortfolio: SelectedPortfolio, - ) : AddToPortfolioRoutes - - @Serializable - data object AddToken : AddToPortfolioRoutes - - @Serializable - data object TokenActions : AddToPortfolioRoutes -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt deleted file mode 100644 index edc27eb65f..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenModel.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.common.ui.addtoken.AddTokenUM -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase -import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase -import com.tangem.domain.models.account.Account -import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.SelectedNetwork -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import com.tangem.features.markets.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class AddTokenModel @Inject constructor( - paramsContainer: ParamsContainer, - private val uiBuilder: AddTokenUiBuilder, - private val messageSender: UiMessageSender, - private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - override val dispatchers: CoroutineDispatcherProvider, - private val analyticsEventHandler: AnalyticsEventHandler, - private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, - private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val checkCurrencyUnsupportedDelegate: CheckCurrencyUnsupportedDelegate, -) : Model() { - - private val params = paramsContainer.require() - private val analyticsEventBuilder = params.eventBuilder - private val addTokenJob = JobHolder() - - val uiState: StateFlow - field = MutableStateFlow(value = null) - - init { - combine( - flow = params.selectedNetwork.distinctUntilChanged(), - flow2 = params.selectedPortfolio.distinctUntilChanged(), - transform = { selectedNetwork, selectedPortfolio -> - addTokenJob.join() - val isTangemIconVisible = needColdWalletInteraction(selectedNetwork, selectedPortfolio) - uiBuilder.updateContent( - selectedPortfolio = selectedPortfolio, - selectedNetwork = selectedNetwork, - isTangemIconVisible = isTangemIconVisible, - onConfirmClick = { onAddClick(selectedNetwork, selectedPortfolio).saveIn(addTokenJob) }, - ) - }, - ) - .onEach { newUI -> uiState.value = newUI } - .flowOn(dispatchers.default) - .launchIn(modelScope) - } - - private fun onAddClick(selectedNetwork: SelectedNetwork, selectedPortfolio: SelectedPortfolio) = - modelScope.launch(dispatchers.default) { - val um = uiState.value ?: return@launch - - val cryptoCurrency = selectedNetwork.cryptoCurrency - val account = selectedPortfolio.account.account.account - val accountId = account.accountId - val isMainNetwork = selectedNetwork.selectedNetwork.contractAddress == null - - val unsupportedCurrency = checkCurrencyUnsupportedDelegate.checkCurrencyUnsupportedState( - userWalletId = accountId.userWalletId, - rawNetworkId = selectedNetwork.selectedNetwork.networkId, - isMainNetwork = isMainNetwork, - ) - - if (unsupportedCurrency != null) return@launch - - uiState.value = um.toggleProgress(true) - val blockchainNames = listOf(selectedNetwork.selectedNetwork) - .mapNotNull { BlockchainUtils.getNetworkInfo(it.networkId)?.name } - analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioContinue(blockchainNames)) - - manageCryptoCurrenciesUseCase(accountId = accountId, add = cryptoCurrency) - .onLeft { error -> - processError(error = error) - uiState.value = um.toggleProgress(false) - return@launch - } - - val status = getAccountCurrencyStatusUseCase( - userWalletId = accountId.userWalletId, - currencyId = cryptoCurrency.id, - network = cryptoCurrency.network, - ).firstOrNull() - if (status == null) { - processError(error = null) - } else { - when (account) { - is Account.CryptoPortfolio -> if (!account.isMainAccount) { - analyticsEventHandler.send(analyticsEventBuilder.addToNotMainAccount()) - } - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - params.callbacks.onTokenAdded(status.status) - } - uiState.value = um.toggleProgress(false) - } - - private suspend fun needColdWalletInteraction( - selectedNetwork: SelectedNetwork, - selectedPortfolio: SelectedPortfolio, - ): Boolean = coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = selectedPortfolio.userWallet.walletId, - networksWithDerivationPath = mapOf( - selectedNetwork.selectedNetwork.networkId to selectedNetwork.cryptoCurrency.network.derivationPath.value, - ), - ) - - private fun processError(error: Throwable?) { - val message = error?.message?.let { stringReference(it) } - ?: resourceReference(R.string.common_something_went_wrong) - messageSender.send(ToastMessage(message = message)) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt deleted file mode 100644 index ec88d88216..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/AddTokenUiBuilder.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.common.ui.account.AccountIconUM -import com.tangem.common.ui.account.CryptoPortfolioIconConverter -import com.tangem.common.ui.account.PortfolioSelectUM -import com.tangem.common.ui.account.toUM -import com.tangem.common.ui.addtoken.AddTokenUM -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.ParamsContainer -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.iconResId -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.account.AccountStatus.* -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.SelectedNetwork -import com.tangem.features.markets.portfolio.add.api.SelectedPortfolio -import com.tangem.features.markets.portfolio.add.impl.AddTokenComponent -import javax.inject.Inject - -@ModelScoped -internal class AddTokenUiBuilder @Inject constructor( - paramsContainer: ParamsContainer, -) { - private val params = paramsContainer.require() - - private fun createNetwork(selectedNetwork: SelectedNetwork): AddTokenUM.Network { - return AddTokenUM.Network( - icon = selectedNetwork.cryptoCurrency.network.iconResId, - name = stringReference(selectedNetwork.cryptoCurrency.network.name), - editable = selectedNetwork.hasMoreNetworksAvailable, - onClick = { params.callbacks.onChangeNetworkClick() }, - ) - } - - private fun createPortfolio(selectedPortfolio: SelectedPortfolio): PortfolioSelectUM { - val accountIcon: AccountIconUM? - val portfolioName: TextReference - when (selectedPortfolio.isAccountMode) { - false -> { - accountIcon = null - portfolioName = stringReference(selectedPortfolio.userWallet.name) - } - true -> { - val accountStatus = selectedPortfolio.account.account - portfolioName = accountStatus.account.accountName.toUM().value - accountIcon = when (accountStatus) { - is CryptoPortfolio -> CryptoPortfolioIconConverter.convert(accountStatus.account.icon) - is Payment -> AccountIconUM.Payment - } - } - } - return PortfolioSelectUM( - icon = accountIcon, - name = portfolioName, - isAccountMode = selectedPortfolio.isAccountMode, - isMultiChoice = selectedPortfolio.hasMorePortfoliosAvailable, - onClick = { params.callbacks.onChangePortfolioClick() }, - ) - } - - fun updateContent( - selectedPortfolio: SelectedPortfolio, - selectedNetwork: SelectedNetwork, - isTangemIconVisible: Boolean, - onConfirmClick: () -> Unit, - ): AddTokenUM { - // its may happens when change portfolio after selected both params in line navigation - val isAvailableNetwork = selectedPortfolio.account.availableToAddNetworks - .any { selectedNetwork.selectedNetwork.networkId == it.networkId } - val button = AddTokenUM.Button( - isEnabled = isAvailableNetwork, - showProgress = false, - isTangemIconVisible = isTangemIconVisible, - text = resourceReference(R.string.common_add), - onConfirmClick = onConfirmClick, - ) - val networkUM = createNetwork(selectedNetwork) - val portfolioUM = createPortfolio(selectedPortfolio) - val currency = selectedNetwork.cryptoCurrency - val tokenToAdd = TokenItemState.Content( - id = currency.id.value, - iconState = CryptoCurrencyToIconStateConverter().convert(currency), - titleState = TokenItemState.TitleState.Content(stringReference(currency.name)), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = ""), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = ""), - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(currency.symbol)), - onItemClick = null, - onItemLongClick = null, - ) - return AddTokenUM( - tokenToAdd = tokenToAdd, - network = networkUM, - portfolio = portfolioUM, - button = button, - ) - } - - companion object { - - fun AddTokenUM.toggleProgress(showProgress: Boolean) = this.copy( - button = this.button.copy(showProgress = showProgress), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt deleted file mode 100644 index f6d5e737b6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import arrow.core.getOrElse -import com.tangem.core.decompose.ui.UiMessageSender -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.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.model.CurrencyUnsupportedState -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.markets.impl.R -import timber.log.Timber -import javax.inject.Inject - -class CheckCurrencyUnsupportedDelegate @Inject constructor( - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, - private val messageSender: UiMessageSender, -) { - - suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, - rawNetworkId: String, - isMainNetwork: Boolean, - ): CurrencyUnsupportedState? { - val result = checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, - networkId = rawNetworkId, - isMainNetwork = isMainNetwork, - ).getOrElse { throwable -> - Timber.e( - throwable, - """ - Failed to check currency unsupported state - |- User wallet ID: $userWalletId - |- Network ID: $rawNetworkId - |- Is main network: $isMainNetwork - """.trimIndent(), - ) - - val message = SnackbarMessage( - message = throwable.localizedMessage?.let(::stringReference) - ?: resourceReference(R.string.common_error), - ) - messageSender.send(message) - - null - } - - if (result != null) { - showUnsupportedWarning(result) - } - return result - } - - private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = DialogMessage( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_blockchain_by_card_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - }, - ) - - messageSender.send(message) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt deleted file mode 100644 index 4834f7cbbb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/ChooseNetworkModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.impl.ChooseNetworkComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.ChooseNetworkUM -import com.tangem.features.markets.portfolio.impl.model.BlockchainRowUMConverter -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class ChooseNetworkModel @Inject constructor( - paramsContainer: ParamsContainer, - private val checkCurrencyUnsupportedDelegate: CheckCurrencyUnsupportedDelegate, - override val dispatchers: CoroutineDispatcherProvider, -) : Model() { - - private val params = paramsContainer.require() - - val uiState: StateFlow = MutableStateFlow(buildUI()) - - private fun buildUI(): ChooseNetworkUM { - val allAvailable = params.selectedPortfolio.account.availableNetworks - val alreadyAdded = allAvailable - .subtract(params.selectedPortfolio.account.availableToAddNetworks) - val converter = BlockchainRowUMConverter( - alreadyAddedNetworks = alreadyAdded.mapTo(mutableSetOf()) { it.networkId }, - ) - val allAvailableNetworks = allAvailable.map { it to true } - return ChooseNetworkUM( - networks = converter.convertList(allAvailableNetworks).toPersistentList(), - onNetworkClick = onNetworkClick@{ row -> - val network = allAvailable - .find { it.networkId == row.id } - ?: return@onNetworkClick - checkNetwork(row, network) - }, - ) - } - - private fun checkNetwork(row: BlockchainRowUM, network: TokenMarketInfo.Network) = modelScope.launch { - val selectedWalletId = params.selectedPortfolio.userWallet.walletId - val unsupportedState = checkCurrencyUnsupportedDelegate.checkCurrencyUnsupportedState( - userWalletId = selectedWalletId, - rawNetworkId = row.id, - isMainNetwork = row.isMainNetwork, - ) - if (unsupportedState == null) { - params.callbacks.onNetworkSelected(network) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt deleted file mode 100644 index 466befc011..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsModel.kt +++ /dev/null @@ -1,76 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.TokenReceiveConfig -import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory -import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler.HandledQuickAction -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch -import javax.inject.Inject - -@ModelScoped -@Suppress("LongParameterList") -internal class TokenActionsModel @Inject constructor( - paramsContainer: ParamsContainer, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - tokenActionsIntentsFactory: TokenActionsHandler.Factory, - override val dispatchers: CoroutineDispatcherProvider, - private val uiBuilder: TokenActionsUiBuilder, - private val analyticsEventHandler: AnalyticsEventHandler, - private val receiveAddressesFactory: ReceiveAddressesFactory, -) : Model() { - - private val params = paramsContainer.require() - private val analyticsEventBuilder get() = params.eventBuilder - private val currentAppCurrency = getSelectedAppCurrencyUseCase.invokeOrDefault() - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - - private val tokenActionsHandler: TokenActionsHandler = - tokenActionsIntentsFactory.create( - currentAppCurrency = Provider { currentAppCurrency.value }, - onHandleQuickAction = { handledAction -> handledQuickAction(handledAction) }, - ) - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val uiState: StateFlow = params.data - .mapLatest { uiBuilder.build(it, tokenActionsHandler) } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = null, - ) - - private fun handledQuickAction(handledAction: HandledQuickAction) { - val event = analyticsEventBuilder.getTokenActionClick(actionUM = handledAction.action) - analyticsEventHandler.send(event) - val isReceive = handledAction.action == TokenActionsBSContentUM.Action.Receive - if (!isReceive) return - modelScope.launch { - val tokenConfig = receiveAddressesFactory.create( - status = handledAction.cryptoCurrencyData.status, - userWalletId = handledAction.cryptoCurrencyData.userWallet.walletId, - ) ?: return@launch - bottomSheetNavigation.activate(tokenConfig) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt deleted file mode 100644 index 6a1dcb993e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/model/TokenActionsUiBuilder.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.model - -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.ParamsContainer -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.stringReference -import com.tangem.features.markets.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.model.PortfolioTokenUMConverter -import com.tangem.features.markets.portfolio.impl.model.TokenActionsHandler -import javax.inject.Inject - -@ModelScoped -internal class TokenActionsUiBuilder @Inject constructor( - paramsContainer: ParamsContainer, - private val analyticsEventHandler: AnalyticsEventHandler, -) { - private val params = paramsContainer.require() - - fun build(data: PortfolioData.CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): TokenActionsUM { - val status = data.status - val tokenUM = TokenItemState.Content( - id = status.currency.id.value, - iconState = CryptoCurrencyToIconStateConverter().convert(status.currency), - titleState = TokenItemState.TitleState.Content(stringReference(status.currency.name)), - fiatAmountState = null, - subtitle2State = null, - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(status.currency.symbol)), - onItemClick = null, - onItemLongClick = null, - ) - return TokenActionsUM( - token = tokenUM, - onLaterClick = { - analyticsEventHandler.send(params.eventBuilder.getTokenLater()) - params.callbacks.onLaterClick() - }, - quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt deleted file mode 100644 index e2f26ae35b..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/ChooseNetworkContent.kt +++ /dev/null @@ -1,110 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.core.ui.components.label.Label -import com.tangem.core.ui.components.label.entity.LabelStyle -import com.tangem.core.ui.components.label.entity.LabelUM -import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -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.markets.impl.R -import com.tangem.features.markets.portfolio.add.impl.ui.state.ChooseNetworkUM -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -private const val DISABLED_ALPHA = 0.4f - -@Composable -internal fun ChooseNetworkContent(state: ChooseNetworkUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.action), - ) { - state.networks.fastForEachIndexed { index, model -> - key(model.id) { - BlockchainRow( - model = model, - itemPadding = PaddingValues( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing14, - ), - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = model.isEnabled, onClick = { state.onNetworkClick(model) }), - ) { - if (!model.isEnabled) { - Label( - modifier = Modifier.alpha(DISABLED_ALPHA), - state = LabelUM( - text = resourceReference(R.string.common_added), - style = LabelStyle.REGULAR, - ), - ) - } - } - } - } - } -} - -@Composable -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview(@PreviewParameter(ChooseNetworkContentProvider::class) content: ChooseNetworkUM) { - TangemThemePreview { - ChooseNetworkContent( - state = content, - ) - } -} - -internal class ChooseNetworkContentProvider : PreviewParameterProvider { - - private val blockchainRow = BlockchainRowUM( - id = UUID.randomUUID().toString(), - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.img_eth_22, - isMainNetwork = false, - isSelected = true, - isEnabled = true, - ) - - override val values: Sequence - get() = sequenceOf( - ChooseNetworkUM( - onNetworkClick = {}, - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - ), - blockchainRow.copy( - iconResId = R.drawable.ic_bsc_16, - isEnabled = false, - ), - blockchainRow.copy(iconResId = R.drawable.img_polygon_22), - blockchainRow.copy(iconResId = R.drawable.img_optimism_22), - ), - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt deleted file mode 100644 index e396ef008e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager.State -import com.tangem.features.markets.portfolio.add.impl.converter.AvailableToAddDataConverter -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* - -internal class DefaultAddToPortfolioManager @AssistedInject constructor( - private val availableToAddDataConverter: AvailableToAddDataConverter, - @Assisted override val token: TokenMarketParams, - @Assisted override val analyticsParams: MarketsPortfolioComponent.AnalyticsParams?, - @Assisted val scope: CoroutineScope, - dispatchers: CoroutineDispatcherProvider, - portfolioFetcherFactory: PortfolioFetcher.Factory, -) : AddToPortfolioManager { - - private val _allAvailableNetworks = MutableSharedFlow>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - override val allAvailableNetworks: Flow> = _allAvailableNetworks.asSharedFlow() - override val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), - scope = scope, - ) - - override val state: StateFlow = - combine( - flow = portfolioFetcher.data.map { it.balances }.distinctUntilChanged(), - flow2 = allAvailableNetworks.map { it.toSet() }.distinctUntilChanged(), - ) { balances, availableNetworks -> - val data = availableToAddDataConverter.convert( - balances = balances, - availableNetworks = availableNetworks, - marketParams = token, - ) - if (data.isAvailableToAdd) { - State.AvailableToAdd(data) - } else { - State.NothingToAdd - } - } - .flowOn(dispatchers.default) - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = State.Init, - ) - - override fun setTokenNetworks(networks: List) { - _allAvailableNetworks.tryEmit(networks) - } - - @AssistedFactory - interface Factory : AddToPortfolioManager.Factory { - override fun create( - scope: CoroutineScope, - token: TokenMarketParams, - analyticsParams: MarketsPortfolioComponent.AnalyticsParams?, - ): DefaultAddToPortfolioManager - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt deleted file mode 100644 index e0b24bf672..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/TokenActionsContent.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.SecondaryButton -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.SpacerH16 -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.icons.badge.drawBadge -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemColorPalette -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -@Composable -internal fun TokenActionsContent(state: TokenActionsUM, modifier: Modifier = Modifier) { - Column( - modifier = modifier.fillMaxWidth(), - ) { - TokenItem( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(color = TangemTheme.colors.background.action), - state = state.token, - isBalanceHidden = false, - ) - - SpacerH(TangemTheme.dimens.spacing14) - Column( - modifier = Modifier - .clip(RoundedCornerShape(TangemTheme.dimens.radius14)) - .background(TangemTheme.colors.background.action), - ) { - state.quickActions.actions.fastForEach { action -> - key(action.title) { - ActionRow( - state = action, - onClick = { state.quickActions.onQuickActionClick(action) }, - onLongClick = { state.quickActions.onQuickActionLongClick(action) }, - ) - } - } - } - - SpacerH16() - - SecondaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.common_later), - onClick = state.onLaterClick, - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun ActionRow( - state: QuickActionUM, - onClick: () -> Unit, - onLongClick: (() -> Unit), - modifier: Modifier = Modifier, -) { - val hapticManager = LocalHapticManager.current - val onLongClickInternal = { - hapticManager.perform(TangemHapticEffect.View.LongPress) - onLongClick() - } - - Row( - modifier = modifier - .fillMaxWidth() - .combinedClickable( - onLongClick = onLongClickInternal.takeIf { state.isLongClickAvailable }, - onClick = { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - }, - ) - .padding(horizontal = TangemTheme.dimens.spacing12, vertical = TangemTheme.dimens.spacing15), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - val containerColor = TangemTheme.colors.background.action - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .background( - color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), - shape = CircleShape, - ) - .size(36.dp) - .drawWithContent { - drawContent() - if (state is QuickActionUM.Exchange && state.shouldShowBadge) { - drawBadge(containerColor = containerColor, offset = 4.dp) - } - }, - ) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size16), - imageVector = ImageVector.vectorResource(id = state.icon), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - } - Column( - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Preview(widthDp = 360, showBackground = true) -@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(TokenActionsContentPreviewProvider::class) state: TokenActionsUM) { - TangemThemePreview { - TokenActionsContent( - state = state, - ) - } -} - -private class TokenActionsContentPreviewProvider : PreviewParameterProvider { - private val tokenState - get() = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.TokenIcon( - url = null, - topBadgeIconResId = R.drawable.img_eth_22, - fallbackTint = TangemColorPalette.Black, - fallbackBackground = TangemColorPalette.Meadow, - isGrayscale = false, - shouldShowCustomBadge = false, - ), - titleState = TokenItemState.TitleState.Content( - text = stringReference(value = "Tether"), - ), - fiatAmountState = null, - subtitle2State = null, - subtitleState = TokenItemState.SubtitleState.TextContent(value = stringReference("USDT")), - onItemClick = {}, - onItemLongClick = {}, - ) - - override val values: Sequence - get() = sequenceOf( - TokenActionsUM( - quickActions = PortfolioTokenUM.QuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - onQuickActionClick = {}, - onQuickActionLongClick = {}, - ), - token = tokenState, - onLaterClick = {}, - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt deleted file mode 100644 index 8e27218757..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/ChooseNetworkUM.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui.state - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import kotlinx.collections.immutable.ImmutableList - -data class ChooseNetworkUM( - val networks: ImmutableList, - val onNetworkClick: (BlockchainRowUM) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt deleted file mode 100644 index cb2466e02a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/add/impl/ui/state/TokenActionsUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.portfolio.add.impl.ui.state - -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM - -internal data class TokenActionsUM( - val token: TokenItemState, - val quickActions: PortfolioTokenUM.QuickActions, - val onLaterClick: () -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt deleted file mode 100644 index 62babbdc62..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/api/MarketsPortfolioComponent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.tangem.features.markets.portfolio.api - -import androidx.compose.runtime.Stable -import com.tangem.core.decompose.factory.ComponentFactory -import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import kotlinx.serialization.Serializable - -@Stable -interface MarketsPortfolioComponent : ComposableContentComponent { - - @Serializable - data class Params( - val token: TokenMarketParams, - val analyticsParams: AnalyticsParams?, - ) - - @Serializable - data class AnalyticsParams( - val source: String, - ) - - fun setTokenNetworks(networks: List) - - fun setNoNetworksAvailable() - - interface Factory : ComponentFactory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt deleted file mode 100644 index 200d6916bb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.tangem.features.markets.portfolio.impl - -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.arkivanov.decompose.ComponentContext -import com.arkivanov.decompose.extensions.compose.subscribeAsState -import com.arkivanov.decompose.router.slot.childSlot -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.context.childByContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioRoute -import com.tangem.features.markets.portfolio.impl.ui.MyPortfolio -import com.tangem.features.tokenreceive.TokenReceiveComponent -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject - -@Stable -internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( - @Assisted context: AppComponentContext, - @Assisted private val params: MarketsPortfolioComponent.Params, - private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, - private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, -) : AppComponentContext by context, MarketsPortfolioComponent { - - private val model: MarketsPortfolioModel = getOrCreateModel(params) - - private val bottomSheetSlot = childSlot( - source = model.bottomSheetNavigation, - serializer = MarketsPortfolioRoute.serializer(), - handleBackButton = false, - childFactory = ::bottomSheetChild, - ) - - override fun setTokenNetworks(networks: List) { - model.setTokenNetworks(networks) - } - - override fun setNoNetworksAvailable() { - model.setNoNetworksAvailable() - } - - @Composable - override fun Content(modifier: Modifier) { - val state by model.state.collectAsStateWithLifecycle() - val bottomSheet by bottomSheetSlot.subscribeAsState() - - MyPortfolio(modifier = modifier, state = state) - bottomSheet.child?.instance?.BottomSheet() - } - - private fun bottomSheetChild( - config: MarketsPortfolioRoute, - componentContext: ComponentContext, - ): ComposableBottomSheetComponent = when (config) { - MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create( - context = childByContext(componentContext), - params = AddToPortfolioComponent.Params( - addToPortfolioManager = requireNotNull(model.newAddToPortfolioManager) { - "newAddToPortfolioManager must be initialized" - }, - callback = model.addToPortfolioCallback, - ), - ) - is MarketsPortfolioRoute.TokenReceive -> tokenReceiveComponentFactory.create( - context = childByContext(componentContext), - params = TokenReceiveComponent.Params( - config = config.config, - onDismiss = model.bottomSheetNavigation::dismiss, - ), - ) - } - - @AssistedFactory - interface Factory : MarketsPortfolioComponent.Factory { - override fun create( - context: AppComponentContext, - params: MarketsPortfolioComponent.Params, - ): DefaultMarketsPortfolioComponent - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt deleted file mode 100644 index ac6125d3c0..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.analytics - -import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM - -internal class PortfolioAnalyticsEvent( - event: String, - params: Map = emptyMap(), -) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { - - data class EventBuilder( - val token: TokenMarketParams, - val source: String?, - ) { - - fun addToPortfolioClicked() = PortfolioAnalyticsEvent( - event = "Button - Add To Portfolio", - params = mapOf( - "Token" to token.symbol, - ), - ) - - fun popupToChooseAccount() = PortfolioAnalyticsEvent( - event = "Choose Account Opened", - ) - - fun addToNotMainAccount() = PortfolioAnalyticsEvent( - event = "Button - Add To Account", - ) - - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent(event = "Wallet Selected") - - fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( - event = "Token Network Selected", - params = mapOf( - "Count" to blockchainNames.size.toString(), - "Token" to token.symbol, - "blockchain" to blockchainNames.joinToString(separator = ", "), - ), - ) - - fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = - PortfolioAnalyticsEvent( - event = when (actionUM) { - TokenActionsBSContentUM.Action.Buy -> "Button - Buy" - TokenActionsBSContentUM.Action.Receive -> "Button - Receive" - TokenActionsBSContentUM.Action.Exchange -> "Button - Swap" - TokenActionsBSContentUM.Action.Stake -> "Button - Stake" - TokenActionsBSContentUM.Action.YieldMode -> "Button - Yield Mode" - else -> "error" - }, - params = buildMap { - put("Token", token.symbol) - if (source != null) put("Source", source) - put("blockchain", blockchainName) - }, - ) - - fun getTokenActionClick(actionUM: TokenActionsBSContentUM.Action) = PortfolioAnalyticsEvent( - event = when (actionUM) { - TokenActionsBSContentUM.Action.Buy -> "Popup Get token - Button Buy" - TokenActionsBSContentUM.Action.Receive -> "Popup Get token - Button Receive" - TokenActionsBSContentUM.Action.Exchange -> "Popup Get token - Button Exchange" - TokenActionsBSContentUM.Action.Stake -> "Popup Get token - Button Stake" - else -> "error" - }, - ) - - fun getTokenLater() = PortfolioAnalyticsEvent( - event = "Popup Get token - Button Later", - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt deleted file mode 100644 index d011fbf799..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ComponentModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.di - -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.DefaultMarketsPortfolioComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal interface ComponentModule { - - @Binds - @Singleton - fun bindMarketsPortfolioComponent( - factory: DefaultMarketsPortfolioComponent.Factory, - ): MarketsPortfolioComponent.Factory -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt deleted file mode 100644 index 2b35fe3c10..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/di/ModelModule.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.di - -import com.tangem.core.decompose.di.ModelComponent -import com.tangem.core.decompose.model.Model -import com.tangem.features.markets.portfolio.impl.model.MarketsPortfolioModel -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.multibindings.ClassKey -import dagger.multibindings.IntoMap - -@Module -@InstallIn(ModelComponent::class) -internal interface ModelModule { - - @Binds - @IntoMap - @ClassKey(MarketsPortfolioModel::class) - fun provideMarketsPortfolioModel(model: MarketsPortfolioModel): Model -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt deleted file mode 100644 index ac34715c4d..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioData.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.loader - -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance -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.error.TokenListError -import com.tangem.domain.tokens.model.TokenActionsState - -/** - * Portfolio data. Combined data from all flows that required to setup portfolio - * - * @property walletsWithCurrencies wallets with crypto currency statuses - * @property appCurrency app currency - * @property isBalanceHidden flag that indicates if balance should be hidden - * @property walletsWithBalance wallets with total balance - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioData( - val walletsWithCurrencies: Map>, - val appCurrency: AppCurrency, - val isBalanceHidden: Boolean, - val walletsWithBalance: Map>, -) { - data class CryptoCurrencyData( - val userWallet: UserWallet, - val status: CryptoCurrencyStatus, - val actions: List, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt deleted file mode 100644 index 0dadd3bcf4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.loader - -import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.models.YieldSupplyAvailability -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** - * Loader of portfolio data - * - * @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses - * @property getSelectedAppCurrencyUseCase use case for getting selected app currency - * @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings - * @property getWalletTotalBalanceUseCase use case for getting wallet total balance - * -[REDACTED_AUTHOR] - */ -internal class PortfolioDataLoader @Inject constructor( - private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, - private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, -) { - - /** Load data by [currencyRawId] */ - @OptIn(ExperimentalCoroutinesApi::class) - fun load(currencyRawId: CryptoCurrency.RawID): Flow { - return combine( - flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(), - ) { walletsWithCurrencies, appCurrency, isBalanceHidden -> - PortfolioData( - walletsWithCurrencies = walletsWithCurrencies, - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - walletsWithBalance = emptyMap(), - ) - } - // setup balances for wallets from walletsWithCurrencyStatuses - .flatMapLatest { portfolioData -> - getWalletsWithTotalBalanceFlow( - ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId), - ) - .map { portfolioData.copy(walletsWithBalance = it) } - .onEmpty { emit(portfolioData) } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun getAllWalletsCryptoCurrenciesData( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) - .distinctUntilChanged() - .map { walletsWithMaybeStatuses -> - walletsWithMaybeStatuses.mapValues { entry -> - entry.value.mapNotNull { it.getOrNull() } - } - } - .flatMapLatest { walletsWithStatuses -> - val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> - statuses.map { status -> - val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse { - YieldSupplyAvailability.Unavailable - } - getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability) - .map { actionStates -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = actionStates.states, - ) - } - } - } - - combine(actionsFlows) { actions -> - walletsWithStatuses.mapValues { entry -> - entry.value.mapNotNull { status -> - actions.firstOrNull { data -> - data.userWallet == entry.key && data.status == status - } - } - } - }.onEmpty { - emit( - walletsWithStatuses.mapValues { (wallet, statuses) -> - statuses.map { status -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = emptyList(), - ) - } - }, - ) - } - }.onEmpty { - emit(emptyMap()) - } - .distinctUntilChanged() - } - - private fun getWalletsWithTotalBalanceFlow( - ids: List, - ): Flow>> { - return combine( - flows = ids - .map { userWalletId -> - getWalletTotalBalanceUseCase(userWalletId) - .map { userWalletId to it } - .distinctUntilChanged() - }, - transform = { it.toMap() }, - ) - .distinctUntilChanged() - .onEmpty { ids.associateWith { Lce.Loading(partialContent = null) } } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt deleted file mode 100644 index bf294a2895..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM -import kotlinx.collections.immutable.toImmutableList - -/** - * Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig] - * - * @property token token params - * @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed - * @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed - * @property onNetworkSwitchClick callback is invoked when network switch is clicked - * @property onAnotherWalletSelect callback is invoked when wallet is selected - * @property onContinueClick callback is invoked when continue button is clicked - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class AddToPortfolioBSContentUMFactory( - private val addToPortfolioManager: AddToPortfolioManager, - private val token: TokenMarketParams, - private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit, - private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, - private val onAnotherWalletSelect: (UserWalletId) -> Unit, - private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set) -> Unit, -) { - - /** - * Create [TangemBottomSheetConfig] - * - - * @param portfolioData portfolio data - * @param portfolioUIData portfolio bottom sheet visibility model - * @param selectedWallet selected wallet - * @param alreadyAddedNetworks already added networks - */ - @Suppress("LongParameterList") - fun create( - currentState: TangemBottomSheetConfig?, - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - selectedWallet: UserWallet?, - alreadyAddedNetworks: Set?, - artworks: Map, - ): TangemBottomSheetConfig { - return (currentState ?: TangemBottomSheetConfig.Empty).copy( - isShown = portfolioUIData.portfolioBSVisibilityModel.isAddToPortfolioBSVisible, - onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, - content = if (selectedWallet != null && alreadyAddedNetworks != null) { - AddToPortfolioBSContentUM( - selectedWallet = selectedWallet.toSelectedUserWalletItemUM( - portfolioData = portfolioData, - balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(), - artwork = artworks[selectedWallet.walletId], - ), - selectNetworkUM = SelectNetworkUMConverter( - networksWithToggle = addToPortfolioManager.associateWithToggle( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - addToPortfolioData = portfolioUIData.addToPortfolioData, - ), - alreadyAddedNetworks = alreadyAddedNetworks, - onNetworkSwitchClick = onNetworkSwitchClick, - ).convert(value = token), - isScanCardNotificationVisible = portfolioUIData.shouldRequireColdWalletInteraction, - isContinueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks( - userWalletId = selectedWallet.walletId, - ), - onContinueButtonClick = { - onContinueClick( - selectedWallet.walletId, - portfolioUIData.addToPortfolioData.getAddedNetworks( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - ), - ) - }, - walletSelectorConfig = createWalletSelectorBSConfig( - isShow = portfolioUIData.portfolioBSVisibilityModel.isWalletSelectorBSVisible, - portfolioData = portfolioData, - selectedWalletId = selectedWallet.walletId, - artworks = artworks, - ), - isWalletBlockVisible = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency).size > 1, - ) - } else { - TangemBottomSheetConfigContent.Empty - }, - ) - } - - private fun UserWallet.toSelectedUserWalletItemUM( - artwork: UserWalletItemUM.ImageState? = null, - portfolioData: PortfolioData, - balance: TotalFiatBalance?, - ): UserWalletItemUM { - return UserWalletItemUMConverter( - onClick = { onWalletSelectorVisibilityChange(true) }, - endIcon = UserWalletItemUM.EndIcon.Arrow, - balance = balance, - artwork = artwork, - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - ).convert(value = this) - } - - private fun createWalletSelectorBSConfig( - isShow: Boolean, - portfolioData: PortfolioData, - selectedWalletId: UserWalletId, - artworks: Map, - ): TangemBottomSheetConfig { - return TangemBottomSheetConfig( - isShown = isShow, - onDismissRequest = { onWalletSelectorVisibilityChange(false) }, - content = WalletSelectorBSContentUM( - userWallets = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency) - .map { it.key } - .map { userWallet -> - val balance = portfolioData.walletsWithBalance[userWallet.walletId] - - UserWalletItemUMConverter( - onClick = { walletId -> - if (walletId != selectedWalletId) { - onAnotherWalletSelect(walletId) - onWalletSelectorVisibilityChange(false) - } - }, - appCurrency = portfolioData.appCurrency, - balance = balance?.getOrNull(), - isBalanceHidden = portfolioData.isBalanceHidden, - endIcon = if (userWallet.walletId == selectedWalletId) { - UserWalletItemUM.EndIcon.Checkmark - } else { - UserWalletItemUM.EndIcon.None - }, - artwork = artworks[userWallet.walletId], - ).convert(userWallet) - } - .toImmutableList(), - onBack = { onWalletSelectorVisibilityChange(false) }, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt deleted file mode 100644 index 15718128de..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/AddToPortfolioManager.kt +++ /dev/null @@ -1,190 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.update -import timber.log.Timber -import javax.inject.Inject - -internal typealias WalletsWithNetworks = Map> - -/** - * Manager for tracking changing networks in AddToPortfolio - * -[REDACTED_AUTHOR] - */ -internal class AddToPortfolioManager @Inject constructor( - private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, -) { - - val availableNetworks = MutableStateFlow?>(value = null) - private val addedNetworks = MutableStateFlow(value = emptyMap()) - private val removedNetworks = MutableStateFlow(value = emptyMap()) - - /** Get [AddToPortfolioData] as flow */ - fun getAddToPortfolioData(): Flow { - return combine( - flow = availableNetworks, - flow2 = addedNetworks, - flow3 = removedNetworks, - transform = ::AddToPortfolioData, - ) - } - - /** Set available networks [networks] */ - fun setAvailableNetworks(networks: List) { - availableNetworks.value = networks.toSet() - } - - /** Add network [networkId] to [userWalletId] */ - fun addNetwork(userWalletId: UserWalletId, networkId: String) { - addedNetworks.add(userWalletId, networkId) - - removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId) - } - - /** Remove network [networkId] from [userWalletId] */ - fun removeNetwork(userWalletId: UserWalletId, networkId: String) { - removedNetworks.add(userWalletId, networkId) - - addedNetworks.cancelPrevChangeIfExist( - userWalletId = userWalletId, - networkId = networkId, - ) - } - - /** Remove all networks by [userWalletId] */ - fun removeAllChanges(userWalletId: UserWalletId) { - addedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - - removedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - } - - fun associateWithToggle( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - addToPortfolioData: AddToPortfolioData, - ): Map { - val filteredNetworks = filterAvailableNetworksForWalletUseCase( - userWalletId = userWalletId, - networks = addToPortfolioData.availableNetworks.orEmpty(), - ) - // Use user choice or check already added networks - return filteredNetworks.associateWith { availableNetwork -> - val isAddedByUser = addToPortfolioData.addedNetworks[userWalletId]?.contains(availableNetwork) - - if (isAddedByUser == true) return@associateWith true - - val isRemovedByUser = addToPortfolioData.removedNetworks[userWalletId]?.contains(availableNetwork) - - if (isRemovedByUser == true) return@associateWith false - - val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId } - - isAddedBefore - } - } - - private fun MutableStateFlow.cancelPrevChangeIfExist( - userWalletId: UserWalletId, - networkId: String, - ) { - if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId) - } - - private fun MutableStateFlow.add(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = true) - } - - private fun MutableStateFlow.remove(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = false) - } - - private fun MutableStateFlow.change( - userWalletId: UserWalletId, - networkId: String, - isAddAction: Boolean, - ) { - val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId } - - if (network == null) { - Timber.d( - "Network [$networkId] doesn't contain in available networks [%s]", - availableNetworks.value?.joinToString { it.networkId }, - ) - - return - } - - update { currentMap -> - currentMap.toMutableMap().apply { - this[userWalletId] = if (isAddAction) { - this[userWalletId].orEmpty() + network - } else { - this[userWalletId].orEmpty() - network - } - } - } - } - - /** - * Add to portfolio data - * - * @property availableNetworks available networks that user can add to portfolio - * @property addedNetworks networks that user toggled on, but it might have already been added to the wallet - * @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet - * - * Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just - * toggle it. But when we will save user changes, we will check what tokens have already been added or - * haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks] - */ - data class AddToPortfolioData( - val availableNetworks: Set?, - val addedNetworks: WalletsWithNetworks, - val removedNetworks: WalletsWithNetworks, - ) { - - fun isUserAddedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() || - removedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - /** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */ - fun getAddedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val addedNetworksByUser = addedNetworks[userWalletId].orEmpty() - - return addedNetworksByUser.map { it.networkId } - .minus(alreadyAddedNetworkIds) - .mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - - /** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */ - fun getRemovedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val removedNetworksByUser = removedNetworks[userWalletId].orEmpty() - - return alreadyAddedNetworkIds - .minus(removedNetworksByUser.map { it.networkId }.toSet()) - .mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt deleted file mode 100644 index c9ec3f67cb..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/BlockchainRowUMConverter.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.getActiveIconRes -import com.tangem.core.ui.extensions.getGreyedOutIconRes -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.utils.converter.Converter - -/** - * Converter from [TokenMarketInfo.Network] to [BlockchainRowUM] - * - * @property alreadyAddedNetworks set of already added networks - * -[REDACTED_AUTHOR] - */ -internal class BlockchainRowUMConverter( - private val alreadyAddedNetworks: Set, -) : Converter, BlockchainRowUM> { - - override fun convert(value: Pair): BlockchainRowUM { - val (network, isSelected) = value - - val blockchainInfo = BlockchainUtils.getNetworkInfo(networkId = network.networkId) - ?: error("Can't find blockchain info for ${network.networkId}") - - val isMainNetwork = network.contractAddress == null - - val isEnabled = !alreadyAddedNetworks.contains(network.networkId) - - return BlockchainRowUM( - id = network.networkId, - name = blockchainInfo.name, - type = getNetworkType(network, blockchainInfo), - iconResId = if (isEnabled) { - if (isSelected) { - getActiveIconRes(blockchainInfo.blockchainId) - } else { - getGreyedOutIconRes(blockchainInfo.blockchainId) - } - } else { - getGreyedOutIconRes(blockchainInfo.blockchainId) - }, - isMainNetwork = isMainNetwork, - isSelected = isSelected, - isEnabled = isEnabled, - ) - } - - private fun getNetworkType( - network: TokenMarketInfo.Network, - blockchainInfo: BlockchainUtils.BlockchainInfo, - ): String { - val isMainNetwork = network.contractAddress == null - return when { - BlockchainUtils.isL2Network(networkId = network.networkId) -> MAIN_NETWORK_L2_TYPE_NAME - isMainNetwork -> MAIN_NETWORK_TYPE_NAME - else -> blockchainInfo.protocolName - } - } - - private companion object { - const val MAIN_NETWORK_TYPE_NAME = "MAIN" - const val MAIN_NETWORK_L2_TYPE_NAME = "MAIN L2" - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt deleted file mode 100644 index 0157de78d9..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ /dev/null @@ -1,426 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import androidx.compose.runtime.Stable -import arrow.core.getOrElse -import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -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.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.model.CurrencyUnsupportedState -import com.tangem.domain.markets.SaveMarketTokensUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.currency.CryptoCurrency -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.transaction.usecase.ReceiveAddressesFactory -import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioComponent -import com.tangem.features.markets.portfolio.api.MarketsPortfolioComponent -import com.tangem.features.markets.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.loader.PortfolioDataLoader -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.features.wallet.utils.UserWalletImageFetcher -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.operations.attestation.ArtworkSize -import com.tangem.utils.Provider -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch -import timber.log.Timber -import javax.inject.Inject -import com.tangem.features.markets.portfolio.add.api.AddToPortfolioManager as NewAddToPortfolioManager - -@Suppress("LongParameterList", "LargeClass") -@Stable -@ModelScoped -internal class MarketsPortfolioModel @Inject constructor( - paramsContainer: ParamsContainer, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - tokenActionsIntentsFactory: TokenActionsHandler.Factory, - override val dispatchers: CoroutineDispatcherProvider, - private val messageSender: UiMessageSender, - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, - private val portfolioDataLoader: PortfolioDataLoader, - private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - private val saveMarketTokensUseCase: SaveMarketTokensUseCase, - private val addToPortfolioManager: AddToPortfolioManager, - private val analyticsEventHandler: AnalyticsEventHandler, - private val userWalletImageFetcher: UserWalletImageFetcher, - private val receiveAddressesFactory: ReceiveAddressesFactory, - accountsFeatureToggles: AccountsFeatureToggles, - newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory, - newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory, -) : Model() { - - private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) - val state: StateFlow get() = _state - - private val params = paramsContainer.require() - private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( - token = params.token, - source = params.analyticsParams?.source, - ) - - val newAddToPortfolioManager: NewAddToPortfolioManager? - val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate? - - /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ - private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) - - private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) - - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { - override fun onDismiss() = bottomSheetNavigation.dismiss() - } - - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - - private val tokenActionsHandler = tokenActionsIntentsFactory.create( - currentAppCurrency = Provider { currentAppCurrency.value }, - onHandleQuickAction = { handledAction -> - val currencyNetwork = handledAction.cryptoCurrencyData.status.currency.network - analyticsEventHandler.send( - analyticsEventBuilder.quickActionClick( - actionUM = handledAction.action, - blockchainName = currencyNetwork.name, - ), - ) - configureReceiveAddresses(handledAction) - }, - ) - - private val factory = MyPortfolioUMFactory( - onAddClick = { - onAddToPortfolioBSVisibilityChange(isShow = true) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioClicked(), - ) - }, - addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( - addToPortfolioManager = addToPortfolioManager, - token = params.token, - onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, - onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, - onNetworkSwitchClick = ::onNetworkSwitchClick, - onAnotherWalletSelect = { walletId -> - onWalletSelect(walletId) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioWalletChanged(), - ) - }, - onContinueClick = { selectedWalletId, addedNetworks -> - onContinueClick(selectedWalletId, addedNetworks) - - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioContinue( - blockchainNames = addedNetworks.mapNotNull { - BlockchainUtils.getNetworkInfo(it.networkId)?.name - }, - ), - ) - }, - ), - currentState = Provider { _state.value }, - tokenActionsHandler = tokenActionsHandler, - updateTokens = { updateBlock -> - updateTokensState { state -> - state.copy(tokens = updateBlock(state.tokens)) - } - }, - ) - - init { - if (accountsFeatureToggles.isFeatureEnabled) { - newAddToPortfolioManager = newAddToPortfolioManagerFactory - .create( - modelScope, - params.token, - params.analyticsParams, - ) - newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create( - scope = modelScope, - token = params.token, - tokenActionsHandler = tokenActionsHandler, - buttonState = newAddToPortfolioManager.state.map { managerState -> - when (managerState) { - is NewAddToPortfolioManager.State.AvailableToAdd -> AddButtonState.Available - NewAddToPortfolioManager.State.Init -> AddButtonState.Loading - NewAddToPortfolioManager.State.NothingToAdd -> AddButtonState.Unavailable - } - }, - onAddClick = { - analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) - bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) - }, - ) - newMarketsPortfolioDelegate.combineData() - .onEach { _state.value = it } - .flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - newAddToPortfolioManager = null - newMarketsPortfolioDelegate = null - // Subscribe on selected wallet flow to support actual selected wallet - subscribeOnSelectedMultiWalletUpdates() - - subscribeOnStateUpdates() - } - } - - fun setTokenNetworks(networks: List) { - addToPortfolioManager.setAvailableNetworks(networks) - newAddToPortfolioManager?.setTokenNetworks(networks) - newMarketsPortfolioDelegate?.setTokenNetworks(networks) - } - - fun setNoNetworksAvailable() { - addToPortfolioManager.setAvailableNetworks(emptyList()) - newAddToPortfolioManager?.setTokenNetworks(emptyList()) - newMarketsPortfolioDelegate?.setTokenNetworks(emptyList()) - } - - private fun subscribeOnSelectedMultiWalletUpdates() { - getSelectedWalletUseCase() - .getOrElse { e -> - Timber.e("Failed to load selected wallet: $e") - error("Failed to load selected wallet") - } - .onEach { wallet -> - selectedMultiWalletIdFlow.value = wallet.takeIf { it.isMultiCurrency }?.walletId - } - .launchIn(modelScope) - } - - private fun subscribeOnStateUpdates() { - combine( - flow = loadPortfolioDataWithArtworks(params.token.id), - flow2 = getPortfolioUIDataFlow(), - transform = { pair, portfolioUIData -> - val (portfolioData, artworks) = pair - factory.create(portfolioData, portfolioUIData, artworks) - }, - ) - .onEach { _state.value = it } - .launchIn(modelScope) - } - - private fun loadPortfolioDataWithArtworks( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - val wallets = Channel>() - val portfolioFlow = portfolioDataLoader - .load(currencyRawId) - .onEach { wallets.trySend(it.walletsWithCurrencies.keys) } - - val artworksFlow = wallets.receiveAsFlow() - .distinctUntilChanged() - .flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) } - - return combine( - flow = portfolioFlow, - flow2 = artworksFlow, - ) { portfolioData, artworks -> portfolioData to artworks } - } - - private fun getPortfolioUIDataFlow(): Flow { - return combine( - flow = portfolioBSVisibilityModelFlow, - flow2 = selectedMultiWalletIdFlow, - flow3 = addToPortfolioManager.getAddToPortfolioData(), - transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData -> - PortfolioUIData( - portfolioBSVisibilityModel = portfolioBSVisibilityModel, - selectedWalletId = selectedWalletId, - addToPortfolioData = addToPortfolioData, - shouldRequireColdWalletInteraction = needColdWalletInteraction( - selectedWalletId, - addToPortfolioData, - ), - ) - }, - ) - } - - private suspend fun needColdWalletInteraction( - selectedWalletId: UserWalletId?, - addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - ): Boolean { - return if (selectedWalletId != null) { - coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = selectedWalletId, - networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty() - .associate { it.networkId to null }, - ) - } else { - false - } - } - - private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) { - val selectedWalletId = selectedMultiWalletIdFlow.value - - if (selectedWalletId == null) { - Timber.e("Impossible to switch network when selected wallet is null") - return - } - - if (isChecked) { - modelScope.launch { - val unsupportedState = checkCurrencyUnsupportedState( - userWalletId = selectedWalletId, - rawNetworkId = blockchainRowUM.id, - isMainNetwork = blockchainRowUM.isMainNetwork, - ) - if (unsupportedState != null) { - showUnsupportedWarning(unsupportedState) - } else { - addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) - } - } - } else { - addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) - } - } - - private suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, - rawNetworkId: String, - isMainNetwork: Boolean, - ): CurrencyUnsupportedState? { - return checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, - networkId = rawNetworkId, - isMainNetwork = isMainNetwork, - ).getOrElse { error -> - Timber.e( - error, - """ - Failed to check currency unsupported state - |- User wallet ID: $userWalletId - |- Network ID: $rawNetworkId - |- Is main network: $isMainNetwork - """.trimIndent(), - ) - - val message = SnackbarMessage( - message = error.localizedMessage - ?.let(::stringReference) - ?: resourceReference(R.string.common_error), - ) - messageSender.send(message) - - null - } - } - - private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = DialogMessage( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - }, - ) - - messageSender.send(message) - } - - private fun onWalletSelect(userWalletId: UserWalletId) { - selectedMultiWalletIdFlow.update { prevUserWalletId -> - prevUserWalletId?.let(addToPortfolioManager::removeAllChanges) - - userWalletId - } - } - - private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set) { - modelScope.launch { - saveMarketTokensUseCase( - userWalletId = userWalletId, - tokenMarketParams = params.token, - addedNetworks = addedNetworks, - removedNetworks = emptySet(), - ) - - onAddToPortfolioBSVisibilityChange(isShow = false) - - addToPortfolioManager.removeAllChanges(userWalletId) - } - } - - private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = isShow, isWalletSelectorBSVisible = false) - } - } - - private fun onWalletSelectorVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = true, isWalletSelectorBSVisible = isShow) - } - } - - private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) { - _state.update { stateToUpdate -> - val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate - block(tokensState) - } - } - - private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) { - val isNewReceive = quickAction.action == TokenActionsBSContentUM.Action.Receive - if (isNewReceive) { - modelScope.launch { - val tokenConfig = receiveAddressesFactory.create( - status = quickAction.cryptoCurrencyData.status, - userWalletId = quickAction.cryptoCurrencyData.userWallet.walletId, - ) ?: return@launch - bottomSheetNavigation.activate(MarketsPortfolioRoute.TokenReceive(tokenConfig)) - } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt deleted file mode 100644 index 576d9cda78..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioRoute.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.decompose.navigation.Route -import com.tangem.domain.models.TokenReceiveConfig -import kotlinx.serialization.Serializable - -@Serializable -sealed interface MarketsPortfolioRoute : Route { - - @Serializable - data object AddToPortfolio : MarketsPortfolioRoute - - @Serializable - data class TokenReceive( - val config: TokenReceiveConfig, - ) : MarketsPortfolioRoute -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt deleted file mode 100644 index d5c027adb8..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MyPortfolioUMFactory.kt +++ /dev/null @@ -1,150 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.markets.TokenMarketInfo -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.isMultiCurrency -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import kotlinx.collections.immutable.ImmutableList - -/** - * Factory for creating [MyPortfolioUM] - * - * @property onAddClick callback when user wants to add new token - * @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content - * @property tokenActionsHandler token actions handler - * @property currentState current state provider - * @property updateTokens callback for updating tokens - * -[REDACTED_AUTHOR] - */ -internal class MyPortfolioUMFactory( - private val onAddClick: () -> Unit, - private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, - private val tokenActionsHandler: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) { - - fun create( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): MyPortfolioUM { - val addToPortfolioData = portfolioUIData.addToPortfolioData - - val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true - if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable - - val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) { - portfolioData.walletsWithCurrencies - } else { - portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks) - } - - val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() - if (isPortfolioEmpty) { - val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() - - return if (hasMultiWallets) { - MyPortfolioUM.AddFirstToken( - addToPortfolioBSConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - ) - } else { - MyPortfolioUM.UnavailableForWallet - } - } - - return TokensPortfolioUMConverter( - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - addButtonState = walletsWithCurrencies.getAddButtonState( - availableNetworks = addToPortfolioData.availableNetworks, - ), - bsConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - quickActionsIntents = tokenActionsHandler, - currentState = currentState, - updateTokens = updateTokens, - ) - .convert(walletsWithCurrencies) - } - - private fun createAddToPortfolioBSConfig( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): TangemBottomSheetConfig { - val selectedWallet = portfolioData.walletsWithCurrencies.keys - .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } - ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency } - - val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty() - - val alreadyAddedNetworks = portfolioData.walletsWithCurrencies - .filterAvailableNetworks(availableNetworks)[selectedWallet] - ?.filter { !it.status.currency.isCustom } - ?.map { it.status.currency.network.backendId } - ?.toSet() - - return addToPortfolioBSContentUMFactory.create( - currentState = currentState().addToPortfolioBSConfig, - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - selectedWallet = selectedWallet, - alreadyAddedNetworks = alreadyAddedNetworks, - artworks = artworks, - ) - } - - private fun Map>.getAddButtonState( - availableNetworks: Set?, - ): AddButtonState { - if (availableNetworks == null) return AddButtonState.Loading - - val networkIds = availableNetworks.map { it.networkId } - - val isAllAvailableNetworksAdded = this - // User can add currencies only in multi-currency wallets - .filterKeys(UserWallet::isMultiCurrency) - .mapValues { entry -> entry.value.map { it.status.currency.network.backendId } } - // Each wallets contains all available networks? - .all { it.value.containsAll(networkIds) } - - return if (isAllAvailableNetworksAdded) AddButtonState.Unavailable else AddButtonState.Available - } - - /** Filter map values by available networks [networks] */ - private fun Map>.filterAvailableNetworks( - networks: Set, - ): Map> { - return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } - } - - /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ - private fun List.filterAvailableNetworks( - networks: Set, - ): List { - val networkIds = networks.map(TokenMarketInfo.Network::networkId) - - return mapNotNull { currencyData -> - currencyData.takeIf { networkIds.contains(it.status.currency.network.backendId) } - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt deleted file mode 100644 index 17acbaf94a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ /dev/null @@ -1,351 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import arrow.core.getOrElse -import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network -import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.common.ui.account.CryptoPortfolioIconConverter -import com.tangem.common.ui.account.toUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier -import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -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 -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -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.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.domain.yield.supply.models.YieldSupplyAvailability -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioHeader -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioListItem -import com.tangem.features.markets.portfolio.impl.ui.state.WalletHeader -import com.tangem.utils.extensions.isZero -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.* - -@OptIn(ExperimentalCoroutinesApi::class) -@Suppress("LongParameterList") -internal class NewMarketsPortfolioDelegate @AssistedInject constructor( - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val allAccountSupplier: MultiAccountStatusListSupplier, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCaseV2, - private val getUserWalletUseCase: GetUserWalletUseCase, - private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, - isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - @Assisted private val scope: CoroutineScope, - @Assisted private val token: TokenMarketParams, - @Assisted private val tokenActionsHandler: TokenActionsHandler, - @Assisted private val buttonState: Flow, - @Assisted private val onAddClick: () -> Unit, -) { - - private val currencyRawId: CryptoCurrency.RawID = token.id - private var expandedHolder: MutableStateFlow>>? = null - - private val settingsFlow: Flow = combine( - flow = getSelectedAppCurrencyUseCase.invokeOrDefault(), - flow2 = getBalanceHidingSettingsUseCase.isBalanceHidden(), - flow3 = isAccountsModeEnabledUseCase(), - transform = ::SettingsBox, - ).shareIn( - replay = 1, - started = SharingStarted.Eagerly, - scope = scope, - ).distinctUntilChanged() - - private val availableNetworks = MutableSharedFlow>( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - fun setTokenNetworks(networks: List) { - availableNetworks.tryEmit(networks) - } - - fun combineData(): Flow { - return availableNetworks.transformLatest { availableNetworks -> - when { - availableNetworks.isEmpty() -> emit(MyPortfolioUM.Unavailable) - else -> emitAll(onAvailableNetworksFlow().distinctUntilChanged()) - } - }.distinctUntilChanged() - } - - private fun onAvailableNetworksFlow(): Flow = - portfolioWithThisCurrencyFLow().transformLatest { portfolioWithCurrency -> - when (portfolioWithCurrency.flattenAddedCurrency.isEmpty()) { - false -> emitAll(contentFlow(portfolioWithCurrency).distinctUntilChanged()) - true -> when (portfolioWithCurrency.hasMultiWallets) { - true -> emitAll(addFirstTokenFlow()) - false -> emit(MyPortfolioUM.UnavailableForWallet) - } - } - } - - private fun addFirstTokenFlow(): Flow = buttonState.map { state -> - when (state) { - AddButtonState.Loading -> MyPortfolioUM.Loading - AddButtonState.Available -> MyPortfolioUM.AddFirstToken( - onAddClick = onAddClick, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - ) - AddButtonState.Unavailable -> MyPortfolioUM.Unavailable - } - } - - private fun contentFlow(portfolio: PortfoliosWithThisCurrency): Flow { - fun Portfolio.actionsFoAccountCurrencies(): List>> = - accountsWithAdded.map { account -> - fun CryptoCurrencyStatus.actionsFlow(): Flow> = flow { - val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(this@actionsFlow.currency) - .getOrElse { YieldSupplyAvailability.Unavailable } - emitAll( - getCryptoCurrencyActionsUseCase( - accountId = account.accountStatus.account.accountId, - currency = this@actionsFlow.currency, - yieldSupplyAvailability = yieldSupplyAvailability, - ).map { actionsState -> actionsState.cryptoCurrencyStatus.currency to actionsState }, - ) - } - account.addedCurrency.map { it.actionsFlow() } - }.flatten() - - val allAddedTokenActions = - portfolio.portfolios.map { portfolioItem -> portfolioItem.actionsFoAccountCurrencies() }.flatten() - - return combine( - flow = combine(allAddedTokenActions) { it.toMap() }.distinctUntilChanged(), - flow2 = buttonState.distinctUntilChanged(), - flow3 = getExpandedHolder(portfolio), - flow4 = settingsFlow.distinctUntilChanged(), - transform = { actions, addButtonState, expanded, settings -> - buildContentState( - portfolio = portfolio, - allActions = actions, - addButtonState = addButtonState, - expanded = expanded, - settings = settings, - ) - }, - ) - } - - private fun getExpandedHolder( - portfolio: PortfoliosWithThisCurrency, - ): StateFlow>> { - val expandedHolder = this.expandedHolder - if (expandedHolder != null) return expandedHolder - val allAddedCurrency = portfolio.flattenAddedCurrency - val shouldForceExpand = allAddedCurrency.size == 1 && - allAddedCurrency.first().value.amount?.isZero() == true - - val initValue = when { - shouldForceExpand -> { - val currency = allAddedCurrency.first() - // find userWallet than have this single added token - portfolio.portfolios - .find { it.accountsWithAdded.any { account -> account.addedCurrency.isNotEmpty() } } - ?.userWallet - ?.let { setOf(it.walletId to currency.currency.id) } - .orEmpty() - } - else -> emptySet() - } - return MutableStateFlow(initValue) - .also { this.expandedHolder = it } - } - - private fun portfolioWithThisCurrencyFLow(): Flow = - allAccountSupplier().map { list -> list.map { it.addedAccountsFlow() } }.flatMapLatest { flows -> - combine(flows) { portfolios -> - PortfoliosWithThisCurrency( - currencyRawId = currencyRawId, - portfolios = portfolios.toList(), - ) - } - }.distinctUntilChanged() - - private fun AccountStatusList.addedAccountsFlow(): Flow = - getUserWalletUseCase.invokeFlow(this.userWalletId).mapNotNull { it.getOrNull() }.map { wallet -> - Portfolio( - userWallet = wallet, - accountStatusList = this, - accountsWithAdded = this.filterByRawID(), - ) - }.distinctUntilChanged() - - private fun AccountStatusList.filterByRawID(): List { - fun AccountStatus.filterByRawID(): List = when (this) { - is AccountStatus.CryptoPortfolio -> this.tokenList.flattenCurrencies() - .filter { status -> - val currencyId = status.currency.id.rawCurrencyId ?: return@filter false - getTokenIdIfL2Network(currencyId.value) == currencyRawId.value - } - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } - return accountStatuses.map { accountStatus -> - AccountWithAdded( - accountStatus = accountStatus, - addedCurrency = accountStatus.filterByRawID(), - ) - } - } - - private fun buildContentState( - portfolio: PortfoliosWithThisCurrency, - allActions: Map, - addButtonState: AddButtonState, - expanded: Set>, - settings: SettingsBox, - ): MyPortfolioUM.Content { - val appCurrency = settings.appCurrency - val isBalanceHidden = settings.isBalanceHidden - val isAccountMode = settings.isAccountMode - val uiItems: MutableList = mutableListOf() - - fun toggleQuickActions(key: Pair) = expandedHolder?.update { expanded -> - val isExpand = expanded.contains(key) - if (isExpand) expanded.minus(key) else expanded.plus(key) - } - - val tokenUMConverter = PortfolioTokenUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - onTokenItemClick = { }, - tokenActionsHandler = tokenActionsHandler, - ) - - portfolio.portfolios.forEach { portfolioItem -> - if (portfolioItem.flattenAddedCurrency.isEmpty()) return@forEach - val userWallet = portfolioItem.userWallet - if (isAccountMode) { - uiItems.add(portfolioItem.userWallet.toWalletHeader()) - } else { - uiItems.add(portfolioItem.userWallet.toWalletPortfolioHeader()) - } - - portfolioItem.accountsWithAdded.forEach { accountWithAdded -> - if (accountWithAdded.addedCurrency.isEmpty()) return@forEach - if (isAccountMode) { - val account = accountWithAdded.accountStatus.account - uiItems.add(account.toAccountPortfolioHeader()) - } - - accountWithAdded.addedCurrency.forEach { currencyStatus -> - val actions = allActions[currencyStatus.currency]?.states.orEmpty() - val value = PortfolioData.CryptoCurrencyData( - userWallet = userWallet, - status = currencyStatus, - actions = actions, - ) - val expandedKey = portfolioItem.userWallet.walletId to currencyStatus.currency.id - val isExpand = expanded.contains(expandedKey) - - val tokenItem = tokenUMConverter.convertV2( - onTokenItemClick = { wallet, status -> - toggleQuickActions(wallet.walletId to status.currency.id) - }, - value = value, - isQuickActionsShown = isExpand, - ) - uiItems.add(tokenItem) - } - } - } - - return MyPortfolioUM.Content( - items = uiItems.toImmutableList(), - buttonState = addButtonState, - onAddClick = onAddClick, - ) - } - - private fun Account.toAccountPortfolioHeader(): PortfolioHeader = PortfolioHeader( - id = this.accountId.value, - state = AccountTitleUM.Account( - prefixText = TextReference.EMPTY, - name = this.accountName.toUM().value, - icon = when (this) { - is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(this.icon) - is Account.Payment -> TODO("[REDACTED_JIRA]") - }, - ), - ) - - private fun UserWallet.toWalletPortfolioHeader(): PortfolioHeader = PortfolioHeader( - id = this.walletId.stringValue, - state = AccountTitleUM.Text( - title = stringReference(this.name), - ), - ) - - private fun UserWallet.toWalletHeader(): WalletHeader = WalletHeader( - id = this.walletId.stringValue, - name = stringReference(this.name), - ) - - @Suppress("LongParameterList") - @AssistedFactory - interface Factory { - fun create( - scope: CoroutineScope, - token: TokenMarketParams, - tokenActionsHandler: TokenActionsHandler, - buttonState: Flow, - onAddClick: () -> Unit, - ): NewMarketsPortfolioDelegate - } -} - -private data class PortfoliosWithThisCurrency( - val currencyRawId: CryptoCurrency.RawID, - val portfolios: List, -) { - - val hasMultiWallets: Boolean = portfolios.any { it.userWallet.isMultiCurrency } - - val flattenAddedCurrency: List = - portfolios.map { portfolio -> portfolio.flattenAddedCurrency }.flatten() -} - -private data class Portfolio( - val userWallet: UserWallet, - val accountStatusList: AccountStatusList, - val accountsWithAdded: List, -) { - val flattenAddedCurrency: List = - accountsWithAdded.map { it.addedCurrency }.flatten() -} - -private data class AccountWithAdded( - val addedCurrency: List, - val accountStatus: AccountStatus, -) - -private data class SettingsBox( - val appCurrency: AppCurrency, - val isBalanceHidden: Boolean, - val isAccountMode: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt deleted file mode 100644 index acf1b7934c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioBSVisibilityModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -/** - * Model for portfolio bottom sheet visibility - * - * @property isAddToPortfolioBSVisible visibility of add to portfolio bottom sheet - * @property isWalletSelectorBSVisible visibility of wallet selector bottom sheet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioBSVisibilityModel( - val isAddToPortfolioBSVisible: Boolean = false, - val isWalletSelectorBSVisible: Boolean = false, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt deleted file mode 100644 index 17b0a6cdf5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ /dev/null @@ -1,126 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.ui.tokens.TokenItemStateConverter -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM] - * -[REDACTED_AUTHOR] - */ -internal class PortfolioTokenUMConverter( - private val appCurrency: AppCurrency, - private val isBalanceHidden: Boolean, - private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, - private val tokenActionsHandler: TokenActionsHandler, -) : Converter { - - fun convertV2( - value: PortfolioData.CryptoCurrencyData, - isQuickActionsShown: Boolean, - onTokenItemClick: (UserWallet, CryptoCurrencyStatus) -> Unit, - ): PortfolioTokenUM { - val tokenItemStateConverter = TokenItemStateConverter( - appCurrency = appCurrency, - onItemClick = { _, status -> onTokenItemClick(value.userWallet, status) }, - ) - return PortfolioTokenUM( - tokenItemState = tokenItemStateConverter.convert(value = value.status), - walletId = value.userWallet.walletId, - isBalanceHidden = isBalanceHidden, - isQuickActionsShown = isQuickActionsShown, - quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler), - ) - } - - override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM { - val tokenItemStateConverter = TokenItemStateConverter( - appCurrency = appCurrency, - titleStateProvider = { TokenItemState.TitleState.Content(text = stringReference(value.userWallet.name)) }, - subtitleStateProvider = { - TokenItemState.SubtitleState.TextContent(value = stringReference(value.status.currency.name)) - }, - onItemClick = { _, status -> onTokenItemClick(status) }, - ) - - return PortfolioTokenUM( - tokenItemState = tokenItemStateConverter.convert(value = value.status), - walletId = value.userWallet.walletId, - isBalanceHidden = isBalanceHidden, - isQuickActionsShown = false, - quickActions = quickActions(cryptoData = value, tokenActionsHandler = tokenActionsHandler), - ) - } - - companion object { - fun quickActions( - cryptoData: PortfolioData.CryptoCurrencyData, - tokenActionsHandler: TokenActionsHandler, - ): PortfolioTokenUM.QuickActions { - return PortfolioTokenUM.QuickActions( - actions = toQuickActions(cryptoData.actions), - onQuickActionClick = { quickActionUM -> - when (quickActionUM) { - QuickActionUM.Buy -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Buy, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.Exchange -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Exchange, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.Receive -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Receive, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.Stake -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Stake, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.YieldMode -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.YieldMode, - cryptoCurrencyData = cryptoData, - ) - } - }, - onQuickActionLongClick = { quickAction -> - if (quickAction == QuickActionUM.Receive) { - tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.CopyAddress, - cryptoCurrencyData = cryptoData, - ) - } - }, - ) - } - - fun toQuickActions(actions: List) = buildList { - actions.forEach { action -> - if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) { - when (action) { - is TokenActionsState.ActionState.Buy -> QuickActionUM.Buy - is TokenActionsState.ActionState.Swap -> QuickActionUM.Exchange( - shouldShowBadge = action.showBadge, - ) - is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive - is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake - is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(apy = action.apy) - else -> null - }?.let(::add) - } - } - }.toImmutableList() - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt deleted file mode 100644 index 855e596731..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/PortfolioUIData.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.domain.models.wallet.UserWalletId - -/** - * Portfolio UI data. Combined data from all UI flows that required to setup portfolio - * - * @property portfolioBSVisibilityModel portfolio bottom sheet visibility model - * @property selectedWalletId selected wallet id - * @property addToPortfolioData add to portfolio data - * @property shouldRequireColdWalletInteraction flag that indicates if user has missed derivations and has a cold wallet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioUIData( - val portfolioBSVisibilityModel: PortfolioBSVisibilityModel, - val selectedWalletId: UserWalletId?, - val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - val shouldRequireColdWalletInteraction: Boolean, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt deleted file mode 100644 index 8599ad97d5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/SelectNetworkUMConverter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [TokenMarketParams] to [SelectNetworkUM] - * - * @property networksWithToggle map of networks with toggles - * @property alreadyAddedNetworks already added networks - * @property onNetworkSwitchClick callback is called when network switch is clicked - * -[REDACTED_AUTHOR] - */ -internal class SelectNetworkUMConverter( - private val networksWithToggle: Map, - private val alreadyAddedNetworks: Set, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketParams): SelectNetworkUM { - return SelectNetworkUM( - tokenId = value.id.value, - iconUrl = value.imageUrl, - tokenName = value.name, - tokenCurrencySymbol = value.symbol, - networks = BlockchainRowUMConverter(alreadyAddedNetworks) - .convertList(networksWithToggle.toList()) - .toImmutableList(), - onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) }, - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt deleted file mode 100644 index 3db29adf59..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokenActionsHandler.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.common.routing.AppRoute -import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels -import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.core.analytics.models.AnalyticsParam -import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent -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.extensions.resourceReference -import com.tangem.core.ui.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.offramp.GetOfframpUrlUseCase -import com.tangem.domain.onramp.model.OnrampSource -import com.tangem.domain.tokens.model.TokenActionsState -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.utils.Provider -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlinx.collections.immutable.toImmutableList - -@Suppress("LongParameterList") -internal class TokenActionsHandler @AssistedInject constructor( - private val router: Router, - private val clipboardManager: ClipboardManager, - private val uiMessageSender: UiMessageSender, - private val getOfframpUrlUseCase: GetOfframpUrlUseCase, - private val urlOpener: UrlOpener, - private val analyticsEventHandler: AnalyticsEventHandler, - @Assisted private val currentAppCurrency: Provider, - @Assisted private val onHandleQuickAction: (HandledQuickAction) -> Unit, - private val isDemoCardUseCase: IsDemoCardUseCase, - private val messageSender: UiMessageSender, -) { - - private val disabledActionsInDemoMode = buildSet { - add(TokenActionsBSContentUM.Action.Sell) - } - - fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - onHandleQuickAction( - HandledQuickAction( - action = action, - cryptoCurrencyData = cryptoCurrencyData, - ), - ) - val userWallet = cryptoCurrencyData.userWallet - if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return - - when (action) { - TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Receive -> Unit - TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Send -> onSendClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Stake -> onStakeClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.YieldMode -> onYieldModeClick(cryptoCurrencyData) - } - } - - private fun handleDemoMode(action: TokenActionsBSContentUM.Action, userWallet: UserWallet.Cold): Boolean { - val isDemoCard = isDemoCardUseCase.invoke(userWallet.cardId) - val shouldShowDemoWarning = isDemoCard && disabledActionsInDemoMode.contains(action) - - if (shouldShowDemoWarning) { - showDemoModeWarning() - } - - return shouldShowDemoWarning - } - - private fun showDemoModeWarning() { - val message = DialogMessage( - message = resourceReference(R.string.alert_demo_feature_disabled), - ) - messageSender.send(message) - } - - private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val cryptoCurrencyStatus = cryptoCurrencyData.status - val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return - val addresses = networkAddress.availableAddresses - .mapToAddressModels(cryptoCurrencyStatus.currency) - .toImmutableList() - val defaultAddress = addresses.firstOrNull()?.value ?: return - - clipboardManager.setText(text = defaultAddress, isSensitive = true) - uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied))) - } - - private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - router.push( - AppRoute.Onramp( - userWalletId = cryptoCurrencyData.userWallet.walletId, - currency = cryptoCurrencyData.status.currency, - source = OnrampSource.MARKETS, - ), - ) - } - - private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - getOfframpUrlUseCase( - cryptoCurrencyStatus = cryptoCurrencyData.status, - appCurrencyCode = currentAppCurrency().code, - ).onRight { url -> - urlOpener.openUrl(url) - analyticsEventHandler.send(OfframpAnalyticsEvent.ScreenOpened) - } - } - - private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - router.push( - AppRoute.Swap( - currencyFrom = cryptoCurrencyData.status.currency, - userWalletId = cryptoCurrencyData.userWallet.walletId, - isInitialReverseOrder = true, - screenSource = AnalyticsParam.ScreensSources.Markets.value, - ), - ) - } - - private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val route = AppRoute.SendEntryPoint( - userWalletId = cryptoCurrencyData.userWallet.walletId, - currency = cryptoCurrencyData.status.currency, - ) - router.push(route) - } - - private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val option = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake } - ?.let { it as TokenActionsState.ActionState.Stake } - ?.option ?: return - - router.push( - AppRoute.Staking( - userWalletId = cryptoCurrencyData.userWallet.walletId, - cryptoCurrency = cryptoCurrencyData.status.currency, - integrationId = option.integrationId, - ), - ) - } - - private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { - val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance() - .firstOrNull()?.apy ?: return - - router.push( - AppRoute.YieldSupplyEntry( - userWalletId = cryptoCurrencyData.userWallet.walletId, - cryptoCurrency = cryptoCurrencyData.status.currency, - apy = yieldSupplyApy, - ), - ) - } - - @AssistedFactory - interface Factory { - fun create( - currentAppCurrency: Provider, - onHandleQuickAction: (HandledQuickAction) -> Unit, - ): TokenActionsHandler - } - - data class HandledQuickAction( - val action: TokenActionsBSContentUM.Action, - val cryptoCurrencyData: PortfolioData.CryptoCurrencyData, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt deleted file mode 100644 index 16f7f6554a..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.model - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.features.markets.portfolio.impl.loader.PortfolioData -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isZero -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens] - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class TokensPortfolioUMConverter( - private val appCurrency: AppCurrency, - private val isBalanceHidden: Boolean, - private val addButtonState: AddButtonState, - private val bsConfig: TangemBottomSheetConfig, - private val onAddClick: () -> Unit, - private val quickActionsIntents: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) : Converter>, MyPortfolioUM.Tokens> { - - override fun convert(value: Map>): MyPortfolioUM.Tokens { - val currentTokensState = currentState() as? MyPortfolioUM.Tokens - - return MyPortfolioUM.Tokens( - tokens = value - .flatMap { entry -> entry.value } - .map { cryptoData -> - PortfolioTokenUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - onTokenItemClick = { toggleQuickActions(cryptoData) }, - tokenActionsHandler = quickActionsIntents, - ).convert(value = cryptoData) to cryptoData - } - .setQuickActionsVisibility(currentState = currentTokensState) - .toImmutableList(), - buttonState = addButtonState, - addToPortfolioBSConfig = bsConfig, - onAddClick = onAddClick, - ) - } - - private fun List>.setQuickActionsVisibility( - currentState: MyPortfolioUM.Tokens?, - ): List { - return when { - // if there is only one token and it has empty balance, show quick actions for it - currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = true) - } - } - // if there is no previous state, hide quick actions for all tokens - currentState == null -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = false) - } - } - else -> { - val previousList = currentState.tokens - - // otherwise, keep previous state - this.map { (token, _) -> - token.copy( - isQuickActionsShown = previousList - .firstOrNull { it.matchWith(token) } - ?.isQuickActionsShown == true, - ) - } - } - } - } - - private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return cryptoData.status.value.amount?.isZero() == true - } - - private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) { - updateTokens { tokenList -> - tokenList.map { token -> - token.copy( - isQuickActionsShown = if (token.matchWith(cryptoData)) { - !token.isQuickActionsShown - } else { - false - }, - ) - }.toImmutableList() - } - } - - private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean { - return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id - } - - private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return this.walletId == cryptoData.userWallet.walletId && - this.tokenItemState.id == cryptoData.status.currency.id.value - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt deleted file mode 100644 index f2d2d22dd2..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ /dev/null @@ -1,383 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.vectorResource -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.util.fastForEachIndexed -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonSize -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.components.rows.ArrowRow -import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import kotlinx.coroutines.delay - -@Composable -internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - titleText = resourceReference(R.string.common_add_to_portfolio), - ) { contentState -> - Content( - modifier = Modifier.fillMaxWidth(), - state = contentState, - ) - - WalletSelectorBottomSheet(contentState.walletSelectorConfig) - } -} - -@Composable -private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { - var continueButtonAreaHeight by remember { mutableIntStateOf(0) } - val density = LocalDensity.current - val scrollState = rememberScrollState() - - Box(modifier = modifier) { - Column( - modifier = Modifier - .verticalScroll(state = scrollState) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - if (state.isWalletBlockVisible) { - UserWalletItem( - state = state.selectedWallet, - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) - SpacerH12() - } - - NetworkSelection( - modifier = Modifier.fillMaxWidth(), - state = state.selectNetworkUM, - ) - - SpacerH12() - - AnimatedVisibility( - visible = state.isScanCardNotificationVisible, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ScanWalletWarning(modifier = Modifier.fillMaxWidth()) - SpacerH12() - } - - // Scroll to the bottom when the notification appears and the scroll is at the bottom - LaunchedEffect(Unit) { - if (scrollState.canScrollForward.not()) { - delay(timeMillis = 500) - scrollState.animateScrollTo(scrollState.maxValue) - } - } - } - - SpacerH(with(density) { continueButtonAreaHeight.toDp() }) - } - - AnimatedVisibility( - visible = scrollState.canScrollForward, - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier.align(Alignment.BottomCenter), - ) { - BottomFade(Modifier.align(Alignment.BottomCenter)) - } - - ContinueButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .onGloballyPositioned { - continueButtonAreaHeight = it.size.height - }, - enabled = state.isContinueButtonEnabled, - isTangemIconVisible = state.isScanCardNotificationVisible, - onClick = state.onContinueButtonClick, - ) - } -} - -@Composable -private fun ContinueButton( - enabled: Boolean, - isTangemIconVisible: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TangemButton( - enabled = enabled, - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ) - .navigationBarsPadding() - .fillMaxWidth(), - text = stringResourceSafe(R.string.common_continue), - icon = if (enabled && isTangemIconVisible) { - TangemButtonIconPosition.End(R.drawable.ic_tangem_24) - } else { - TangemButtonIconPosition.None - }, - showProgress = false, - size = TangemButtonSize.Default, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - onClick = onClick, - animateContentChange = true, - ) -} - -@Suppress("LongMethod") -@Composable -private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { - val hapticManager = LocalHapticManager.current - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(R.string.markets_select_network), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, - ) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing14), - verticalAlignment = Alignment.CenterVertically, - ) { - CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - url = state.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - SpacerW12() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f, fill = false) - .alignByBaseline(), - text = state.tokenName, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - SpacerW6() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .alignByBaseline(), - text = state.tokenCurrencySymbol, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Visible, - maxLines = 1, - ) - } - - state.networks.fastForEachIndexed { index, network -> - ArrowRow( - isLastItem = index == state.networks.lastIndex, - content = { - BlockchainRow( - modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), - model = network, - action = { - TangemSwitch( - checked = network.isSelected, - checkedColor = if (network.isEnabled) { - TangemTheme.colors.control.checked - } else { - TangemTheme.colors.icon.inactive - }, - onCheckedChange = { checked -> - if (checked) { - hapticManager.perform(TangemHapticEffect.View.ToggleOn) - } else { - hapticManager.perform(TangemHapticEffect.View.ToggleOff) - } - - state.onNetworkSwitchClick(network, checked) - }, - enabled = network.isEnabled, - ) - }, - ) - }, - ) - } - } - } -} - -@Composable -private fun ScanWalletWarning(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .background( - color = TangemTheme.colors.button.disabled, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), - ) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size20), - imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Text( - text = stringResourceSafe(R.string.markets_generate_addresses_notification), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - content = content, - onDismissRequest = {}, - ), - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContent( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContentRtl( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview(rtl = true) { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -// For on device testing -@Composable -@Preview -private fun PreviewContentTestOnDevice( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview( - alwaysShowBottomSheets = false, - ) { - var isShow by remember { mutableStateOf(false) } - - var contentState by remember { - mutableStateOf(content) - } - - LaunchedEffect(Unit) { - contentState = content.copy( - onContinueButtonClick = { - contentState = contentState.copy( - isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible, - ) - }, - isContinueButtonEnabled = true, - selectedWallet = content.selectedWallet.copy( - onClick = { - contentState = contentState.copy( - isContinueButtonEnabled = !contentState.isContinueButtonEnabled, - ) - }, - ), - ) - } - - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = isShow, - content = contentState, - onDismissRequest = { isShow = false }, - ), - ) - - Button( - onClick = { isShow = !isShow }, - ) { - Text(text = "Toggle") - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt deleted file mode 100644 index 1ea37db49c..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/MyPortfolio.kt +++ /dev/null @@ -1,339 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.annotation.StringRes -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEachIndexed -import com.tangem.common.ui.account.AccountTitle -import com.tangem.core.ui.components.PrimaryButton -import com.tangem.core.ui.components.RectangleShimmer -import com.tangem.core.ui.components.SmallButtonShimmer -import com.tangem.core.ui.components.TextShimmer -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.buttons.SecondarySmallButton -import com.tangem.core.ui.components.buttons.SmallButtonConfig -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.* -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens.AddButtonState - -@Composable -internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { - if (state is MyPortfolioUM.Content) { - val contentModifier = Modifier.padding( - start = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing20, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing32, - ) - PortfolioList(state, contentModifier) - return - } - InformationBlock( - modifier = modifier, - contentHorizontalPadding = TangemTheme.dimens.spacing0, - title = { Title() }, - action = { - if (state !is MyPortfolioUM.Tokens) return@InformationBlock - - AddButton(state = state.buttonState, onClick = state.onAddClick) - }, - ) { - val contentModifier = Modifier.padding( - start = TangemTheme.dimens.spacing12, - end = TangemTheme.dimens.spacing12, - bottom = TangemTheme.dimens.spacing12, - ) - - when (state) { - is MyPortfolioUM.Tokens -> TokenList(state = state) - is MyPortfolioUM.AddFirstToken -> AddFirstTokenContent(state = state, modifier = contentModifier) - MyPortfolioUM.Loading -> LoadingPlaceholder(modifier = contentModifier) - MyPortfolioUM.Unavailable -> UnavailableAsset(modifier = contentModifier) - MyPortfolioUM.UnavailableForWallet -> UnavailableAssetForWallet(modifier = contentModifier) - is MyPortfolioUM.Content -> PortfolioList(state = state) - } - } - - val bsConfig = state.addToPortfolioBSConfig - if (bsConfig != null) { - AddToPortfolioBottomSheet(config = bsConfig) - } -} - -@Composable -private fun Title() { - Text( - text = stringResourceSafe(R.string.markets_common_my_portfolio), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun AddButton(state: AddButtonState, onClick: () -> Unit) { - when (state) { - AddButtonState.Loading -> { - Box { - SmallButtonShimmer( - modifier = Modifier.width(width = TangemTheme.dimens.size63), - shape = RoundedCornerShape(TangemTheme.dimens.radius3), - withIcon = true, - ) - - Box( - Modifier - .matchParentSize() - .background(TangemTheme.colors.background.action), - ) - - RectangleShimmer( - modifier = Modifier - .align(Alignment.Center) - .size(width = TangemTheme.dimens.size63, height = TangemTheme.dimens.size18), - radius = TangemTheme.dimens.radius3, - ) - } - } - AddButtonState.Available, - AddButtonState.Unavailable, - -> { - SecondarySmallButton( - config = SmallButtonConfig( - text = resourceReference(R.string.markets_add_token), - icon = TangemButtonIconPosition.Start(R.drawable.ic_plus_24), - onClick = onClick, - isEnabled = state == AddButtonState.Available, - ), - ) - } - } -} - -@Composable -private fun TokenList(state: MyPortfolioUM.Tokens, modifier: Modifier = Modifier) { - Column(modifier) { - state.tokens.fastForEachIndexed { index, token -> - key(token.tokenItemState.id) { - PortfolioItem( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - state = token, - lastInList = index == state.tokens.size - 1, - ) - } - } - } -} - -@Composable -private fun PortfolioList(state: MyPortfolioUM.Content, modifier: Modifier = Modifier) { - Column(modifier) { - key("PortfolioListHeader") { - Row( - modifier = Modifier.padding(horizontal = 4.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier.weight(1f), - text = stringResourceSafe(R.string.markets_common_my_portfolio), - style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, - ) - AddButton(state = state.buttonState, onClick = state.onAddClick) - } - } - - state.items.fastForEachIndexed { index, item -> - val previousItem = state.items.getOrNull(index.dec()) - val nextItem = state.items.getOrNull(index.inc()) - val itemModifier = Modifier - .fillMaxWidth() - .getOffsetModifier(item, previousItem) - .getBackgroundModifier(item, previousItem, nextItem) - - key(item.id) { - PortfolioItem( - item = item, - modifier = itemModifier, - lastInList = index == state.items.size - 1, - ) - } - } - } -} - -@Composable -private fun Modifier.getBackgroundModifier( - item: PortfolioListItem, - previousItem: PortfolioListItem?, - nextItem: PortfolioListItem?, -): Modifier { - val color = TangemTheme.colors.background.action - val radius = 14.dp - val topRound = RoundedCornerShape(topStart = radius, topEnd = radius) - val bottomRound = RoundedCornerShape(bottomStart = radius, bottomEnd = radius) - val allRound = RoundedCornerShape(size = radius) - val backgroundModifier = when (item) { - is WalletHeader -> this - is PortfolioHeader -> this - .clip(topRound) - .background(color = color) - is PortfolioTokenUM -> when { - previousItem is PortfolioHeader && nextItem !is PortfolioTokenUM -> this - .clip(bottomRound) - .background(color = color) - previousItem is WalletHeader && nextItem !is PortfolioTokenUM -> this - .clip(allRound) - .background(color = color) - previousItem is PortfolioTokenUM && nextItem !is PortfolioTokenUM -> this - .clip(bottomRound) - .background(color = color) - else -> this.background(color = color) - } - } - return backgroundModifier -} - -private fun Modifier.getOffsetModifier(item: PortfolioListItem, previousItem: PortfolioListItem?): Modifier = when { - item is WalletHeader -> this.padding(top = 20.dp, start = 4.dp, end = 4.dp) - item is PortfolioHeader && previousItem is PortfolioTokenUM -> this.padding(top = 12.dp) - item is PortfolioHeader && previousItem == null -> this.padding(top = 20.dp) - previousItem is WalletHeader -> this.padding(top = 12.dp) - previousItem is PortfolioTokenUM -> this - else -> this -} - -@Composable -private fun PortfolioItem(item: PortfolioListItem, lastInList: Boolean, modifier: Modifier = Modifier) { - when (item) { - is PortfolioHeader -> AccountTitle( - modifier = modifier.padding( - start = 12.dp, - top = 12.dp, - bottom = 8.dp, - ), - accountTitleUM = item.state, - textStyle = TangemTheme.typography.caption1, - textColor = TangemTheme.colors.text.primary1, - ) - is PortfolioTokenUM -> PortfolioItem( - state = item, - modifier = modifier, - lastInList = lastInList, - ) - is WalletHeader -> Text( - modifier = modifier, - text = item.name.resolveReference(), - style = TangemTheme.typography.subtitle1, - color = TangemTheme.colors.text.primary1, - ) - } -} - -@Composable -fun UnavailableAsset(modifier: Modifier = Modifier) { - UnavailableContent( - textId = R.string.markets_add_to_my_portfolio_unavailable_description, - modifier = modifier, - ) -} - -@Composable -fun UnavailableAssetForWallet(modifier: Modifier = Modifier) { - UnavailableContent( - textId = R.string.markets_add_to_my_portfolio_unavailable_for_wallet_description, - modifier = modifier, - ) -} - -@Composable -private fun UnavailableContent(@StringRes textId: Int, modifier: Modifier = Modifier) { - Text( - modifier = modifier, - text = stringResourceSafe(textId), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) -} - -@Composable -private fun AddFirstTokenContent(state: MyPortfolioUM.AddFirstToken, modifier: Modifier = Modifier) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), - ) { - Text( - text = stringResourceSafe(R.string.markets_add_to_my_portfolio_description), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.tertiary, - ) - - PrimaryButton( - modifier = Modifier.fillMaxWidth(), - text = stringResourceSafe(R.string.common_add_to_portfolio), - onClick = state.onAddClick, - ) - } -} - -@Composable -private fun LoadingPlaceholder(modifier: Modifier = Modifier) { - Column(modifier = modifier) { - TextShimmer( - modifier = Modifier.fillMaxWidth(), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - - TextShimmer( - modifier = Modifier.fillMaxWidth(fraction = 0.7f), - style = TangemTheme.typography.body2, - textSizeHeight = true, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { - TangemThemePreview { - Box( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary), - ) { - MyPortfolio(state) - } - } -} - -@Preview -@Composable -private fun PreviewRtl(@PreviewParameter(PreviewMyPortfolioUMProvider::class) state: MyPortfolioUM) { - TangemThemePreview(rtl = true) { - Box( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .padding(TangemTheme.dimens.spacing8), - ) { - MyPortfolio(state) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt deleted file mode 100644 index d7800514fc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioItem.kt +++ /dev/null @@ -1,155 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.icons.IconTint -import com.tangem.core.ui.components.token.TokenItem -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewMyPortfolioUMProvider -import com.tangem.features.markets.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.StringsSigns.DASH_SIGN -import kotlinx.collections.immutable.persistentListOf -import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState as TokenFiatAmountState - -@Composable -internal fun PortfolioItem(state: PortfolioTokenUM, lastInList: Boolean, modifier: Modifier = Modifier) { - Column(modifier) { - val hapticManager = LocalHapticManager.current - val tokenItemState = remember(state.tokenItemState) { - when (state.tokenItemState) { - is TokenItemState.Content -> state.tokenItemState.copy( - onItemClick = { cryptoCurrency -> - val onClick = state.tokenItemState.onItemClick - if (onClick != null) { - hapticManager.perform(TangemHapticEffect.View.ContextClick) - onClick.invoke(cryptoCurrency) - } - }, - ) - else -> state.tokenItemState - } - } - TokenItem( - state = tokenItemState, - isBalanceHidden = state.isBalanceHidden, - itemPaddingValues = PaddingValues( - start = TangemTheme.dimens.spacing10, - end = TangemTheme.dimens.spacing12, - ), - ) - - PortfolioQuickActions( - modifier = Modifier - .padding( - bottom = if (lastInList) { - TangemTheme.dimens.spacing12 - } else { - TangemTheme.dimens.spacing24 - }, - ), - actions = state.quickActions.actions, - isVisible = state.isQuickActionsShown, - onActionClick = state.quickActions.onQuickActionClick, - onActionLongClick = state.quickActions.onQuickActionLongClick, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview(@PreviewParameter(PortfolioTokenUMProvider::class) tokenUM: PortfolioTokenUM) { - TangemThemePreview { - var areQuickActionsShown by remember { mutableStateOf(value = false) } - - val onItemClick = { - areQuickActionsShown = areQuickActionsShown.not() - } - - PortfolioItem( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - state = tokenUM.copy( - tokenItemState = when (tokenUM.tokenItemState) { - is TokenItemState.Content -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() }) - is TokenItemState.Unreachable -> tokenUM.tokenItemState.copy(onItemClick = { onItemClick() }) - else -> tokenUM.tokenItemState - }, - isQuickActionsShown = areQuickActionsShown, - ), - lastInList = true, - ) - } -} - -private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider( - collection = listOf( - tokenUM.copy( - tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy( - fiatAmountState = contentFiatAmount.copy( - icons = persistentListOf( - TokenFiatAmountState.Content.IconUM( - iconRes = R.drawable.ic_staking_24, - tint = IconTint.Accent, - ), - ), - ), - ), - ), - tokenUM.copy( - tokenItemState = tokenUM.tokenItemState.copy( - fiatAmountState = contentFiatAmount.copy(text = DASH_SIGN), - subtitle2State = (tokenUM.tokenItemState.subtitle2State as? TokenItemState.Subtitle2State.TextContent - ?: error("subtitle2State must be TextContent for preview")) - .copy(text = DASH_SIGN), - ), - ), - tokenUM.copy(isBalanceHidden = true), - tokenUM.copy( - tokenItemState = TokenItemState.Unreachable( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState, - subtitleState = tokenUM.tokenItemState.subtitleState, - onItemClick = {}, - onItemLongClick = {}, - ), - ), - tokenUM.copy( - tokenItemState = TokenItemState.NoAddress( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState, - subtitleState = tokenUM.tokenItemState.subtitleState, - onItemLongClick = {}, - ), - ), - tokenUM.copy( - tokenItemState = TokenItemState.Loading( - id = tokenUM.tokenItemState.id, - iconState = tokenUM.tokenItemState.iconState, - titleState = tokenUM.tokenItemState.titleState as TokenItemState.TitleState.Content, - subtitleState = tokenUM.tokenItemState.subtitleState, - ), - ), - ), -) { - - companion object { - val tokenUM = PreviewMyPortfolioUMProvider().sampleToken - val contentFiatAmount = tokenUM.tokenItemState.fiatAmountState as? TokenFiatAmountState.Content - ?: error("fiatAmountState must be Content for preview") - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt deleted file mode 100644 index 22a377bfd2..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/PortfolioQuickActions.kt +++ /dev/null @@ -1,249 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.* -import androidx.compose.animation.core.Spring -import androidx.compose.animation.core.spring -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import com.tangem.core.ui.components.SpacerH4 -import com.tangem.core.ui.components.icons.badge.drawBadge -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.portfolio.impl.ui.state.QuickActionUM -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun PortfolioQuickActions( - actions: ImmutableList, - isVisible: Boolean, - onActionClick: (QuickActionUM) -> Unit, - onActionLongClick: (QuickActionUM) -> Unit, - modifier: Modifier = Modifier, -) { - if (actions.isEmpty()) return - - AnimatedVisibility( - visible = isVisible, - enter = expandVertically(expandFrom = Alignment.Top), - exit = shrinkVertically(shrinkTowards = Alignment.Top), - modifier = modifier, - ) { - Column(modifier = Modifier) { - actions.fastForEach { action -> - LineSeparator() - QuickActionItem( - state = action, - onClick = { onActionClick(action) }, - onLongClick = { onActionLongClick(action) }.takeIf { action.isLongClickAvailable }, - ) - } - } - } -} - -@Composable -private fun AnimatedVisibilityScope.LineSeparator(modifier: Modifier = Modifier) { - val lineColor = TangemTheme.colors.stroke.primary - val strokeWidth = TangemTheme.dimens.size1 - val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val startPadding = TangemTheme.dimens.spacing30 - - val height = TangemTheme.dimens.size16 - - Canvas( - modifier = modifier - .animateEnterExit( - enter = expandVertically( - animationSpec = spring( - stiffness = Spring.StiffnessLow, - ), - expandFrom = Alignment.Top, - ) + fadeIn(), - exit = shrinkVertically( - spring( - stiffness = Spring.StiffnessLow, - ), - shrinkTowards = Alignment.Top, - ) + fadeOut(), - ) - .fillMaxWidth() - .height(height), - ) { - val x = if (isLtr) startPadding.toPx() else size.width - startPadding.toPx() - - drawLine( - color = lineColor, - start = Offset(x, 0f), - end = Offset(x, size.height), - strokeWidth = strokeWidth.toPx(), - ) - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun AnimatedVisibilityScope.QuickActionItem( - state: QuickActionUM, - onClick: () -> Unit, - onLongClick: (() -> Unit)?, - modifier: Modifier = Modifier, -) { - val hapticManager = LocalHapticManager.current - val onLongClickInternal: (() -> Unit)? = if (onLongClick != null) { - { - hapticManager.perform(TangemHapticEffect.View.LongPress) - onLongClick() - } - } else { - null - } - - Row( - modifier = modifier - .fillMaxWidth() - .combinedClickable( - onLongClick = onLongClickInternal, - onClick = { - hapticManager.perform(TangemHapticEffect.View.SegmentTick) - onClick() - }, - ) - .padding(horizontal = TangemTheme.dimens.spacing14, vertical = TangemTheme.dimens.spacing4), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing18), - ) { - QuickActionIcon(state) - Column( - modifier = Modifier - .animateEnterExit( - enter = fadeIn(), - exit = fadeOut(), - ), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2), - ) { - Text( - text = state.title.resolveReference(), - style = TangemTheme.typography.body2, - color = TangemTheme.colors.text.primary1, - ) - Text( - text = state.description.resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } - } -} - -@Composable -private fun AnimatedVisibilityScope.QuickActionIcon(state: QuickActionUM) { - val containerColor = TangemTheme.colors.background.action - Box( - Modifier - .animateEnterExit( - enter = scaleIn(), - exit = scaleOut(), - ) - .background( - color = TangemTheme.colors.button.secondary, - shape = CircleShape, - ) - .size(TangemTheme.dimens.size32) - .drawWithContent { - drawContent() - if (state is QuickActionUM.Exchange && state.shouldShowBadge) { - drawBadge(containerColor = containerColor, offset = 4.dp) - } - }, - contentAlignment = Alignment.Center, - ) { - Icon( - modifier = Modifier - .requiredSize(TangemTheme.dimens.size16), - imageVector = ImageVector.vectorResource(id = state.icon), - contentDescription = null, - tint = TangemTheme.colors.button.primary, - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - var isVisible by remember { mutableStateOf(true) } - - Column( - modifier = Modifier - .fillMaxWidth() - .height(680.dp), - ) { - Button( - onClick = { isVisible = !isVisible }, - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - ) { - Text(text = "Toggle") - } - SpacerH4() - Box( - modifier = Modifier.background(color = TangemTheme.colors.background.action), - ) { - PortfolioQuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - isVisible = isVisible, - onActionClick = {}, - onActionLongClick = {}, - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewRtl() { - TangemThemePreview(rtl = true) { - Box(modifier = Modifier.background(color = TangemTheme.colors.background.action)) { - PortfolioQuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - isVisible = true, - onActionClick = {}, - onActionLongClick = {}, - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt deleted file mode 100644 index 8cb81049e5..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.portfolio.impl.ui.state.TokenActionsBSContentUM -import kotlinx.collections.immutable.toImmutableList - -@Composable -fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - title = { content -> - TangemBottomSheetTitle(content.title) - }, - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: TokenActionsBSContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - content.actions.forEachIndexed { index, action -> - Box( - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = content.actions.lastIndex, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action), - ) { - SimpleSettingsRow( - title = action.text.resolveReference(), - icon = action.iconRes, - redesign = true, - onItemsClick = { content.onActionClick(action) }, - ) - } - } - } -} - -@Preview(widthDp = 360, heightDp = 640) -@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview( - alwaysShowBottomSheets = true, - ) { - Box(Modifier.background(TangemTheme.colors.background.secondary)) { - TokenActionsBottomSheet( - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = TokenActionsBSContentUM( - title = "Wallet 1", - actions = TokenActionsBSContentUM.Action.entries.toImmutableList(), - onActionClick = {}, - ), - ), - ) - } - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt deleted file mode 100644 index 23e63c41f4..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ /dev/null @@ -1,140 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.block.BlockCard -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -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.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.markets.portfolio.impl.ui.state.WalletSelectorBSContentUM -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - title = { content -> - TangemTopAppBar( - title = resourceReference(R.string.common_choose_wallet), - titleAlignment = Alignment.CenterHorizontally, - startButton = TopAppBarButtonUM.Back(content.onBack), - height = TangemTopAppBarHeight.BOTTOM_SHEET, - ) - }, - ) { content -> - Content( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing8, - ), - state = content, - ) - } -} - -@Composable -private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - Column( - modifier = modifier - .verticalScroll(rememberScrollState()), - ) { - BlockCard( - modifier = Modifier.fillMaxSize(), - colors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) { - state.userWallets.forEach { state -> - key(state.id) { - UserWalletItem( - modifier = Modifier.fillMaxWidth(), - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - state = state, - ) - } - } - } - SpacerH(bottomBarHeight) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - WalletSelectorBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewContent() { - TangemThemePreview { - Content( - state = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ) - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt deleted file mode 100644 index bdc1b092e3..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.preview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.markets.impl.R -import com.tangem.features.markets.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.markets.portfolio.impl.ui.state.SelectNetworkUM -import kotlinx.collections.immutable.persistentListOf - -internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { - - private val blockchainRow = BlockchainRowUM( - id = "1", - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ) - - val userWallet = UserWalletItemUM( - id = "1", - name = stringReference("Wallet 1"), - information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), - balance = UserWalletItemUM.Balance.Loading, - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ) - - override val values: Sequence - get() = sequenceOf( - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ), - blockchainRow, - blockchainRow, - ), - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = true, - isContinueButtonEnabled = true, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium Etherium Etherium Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ).copy(name = "Etherium Etherium Etherium Etherium"), - *Array(25) { blockchainRow }, - ), - - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = false, - isContinueButtonEnabled = false, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt deleted file mode 100644 index e6deaad138..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.preview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.account.AccountIconPreviewData -import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.markets.portfolio.impl.ui.state.* -import com.tangem.features.markets.portfolio.impl.ui.state.MyPortfolioUM.Tokens -import kotlinx.collections.immutable.persistentListOf -import java.util.UUID - -internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider { - - val sampleToken - get() = PortfolioTokenUM( - tokenItemState = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "My wallet")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "486,65 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "733,71097 MATIC"), - subtitleState = TokenItemState.SubtitleState.TextContent( - value = stringReference(value = "XRP Ledger token"), - ), - onItemClick = {}, - onItemLongClick = {}, - ), - isQuickActionsShown = false, - quickActions = PortfolioTokenUM.QuickActions( - actions = persistentListOf( - QuickActionUM.Buy, - QuickActionUM.Exchange(shouldShowBadge = true), - QuickActionUM.Receive, - ), - onQuickActionClick = {}, - onQuickActionLongClick = {}, - ), - isBalanceHidden = false, - walletId = UserWalletId(""), - ) - - val walletHeader - get() = WalletHeader( - id = UUID.randomUUID().toString(), - name = stringReference("Wallet 1"), - ) - - val walletPortfolioHeader - get() = PortfolioHeader( - state = AccountTitleUM.Text(title = stringReference("Wallet 1")), - id = UUID.randomUUID().toString(), - ) - - val accountHeader - get() = PortfolioHeader( - state = AccountTitleUM.Account( - icon = AccountIconPreviewData.randomAccountIcon(), - name = stringReference("Main Account"), - prefixText = TextReference.EMPTY, - ), - id = UUID.randomUUID().toString(), - ) - val coinIconState - get() = CurrencyIconState.CoinIcon( - url = null, - fallbackResId = com.tangem.core.ui.R.drawable.img_polygon_22, - isGrayscale = false, - shouldShowCustomBadge = false, - ) - val accountToken - get() = sampleToken.copy( - tokenItemState = TokenItemState.Content( - id = UUID.randomUUID().toString(), - iconState = coinIconState, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), - fiatAmountState = FiatAmountState.Content(text = "321 $"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "5,412 MATIC"), - subtitleState = TokenItemState.SubtitleState.TextContent(stringReference(value = "Token")), - onItemClick = {}, - onItemLongClick = {}, - ), - ) - - override val values: Sequence - get() = sequenceOf( - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken, sampleToken), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Available, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken, sampleToken.copy(isQuickActionsShown = true)), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Unavailable, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Tokens( - tokens = persistentListOf(sampleToken.copy(isQuickActionsShown = true), sampleToken), - buttonState = MyPortfolioUM.Tokens.AddButtonState.Loading, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.AddFirstToken( - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletPortfolioHeader, - accountToken, - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletHeader, - accountHeader, - accountToken, - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Content( - items = persistentListOf( - walletHeader, - accountHeader, - accountToken.copy(isQuickActionsShown = true), - accountToken, - ), - buttonState = Tokens.AddButtonState.Available, - onAddClick = {}, - ), - MyPortfolioUM.Loading, - MyPortfolioUM.Unavailable, - MyPortfolioUM.UnavailableForWallet, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt deleted file mode 100644 index ff5c1567e6..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent - -internal data class AddToPortfolioBSContentUM( - val selectedWallet: UserWalletItemUM, - val selectNetworkUM: SelectNetworkUM, - val isWalletBlockVisible: Boolean, - val isScanCardNotificationVisible: Boolean, - val isContinueButtonEnabled: Boolean, - val onContinueButtonClick: () -> Unit, - val walletSelectorConfig: TangemBottomSheetConfig, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt deleted file mode 100644 index 0d1cd7c700..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/MyPortfolioUM.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal sealed class MyPortfolioUM { - - abstract val addToPortfolioBSConfig: TangemBottomSheetConfig? - - data class Tokens( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, - val tokens: ImmutableList, - val buttonState: AddButtonState, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() { - - enum class AddButtonState { - Loading, - Available, - Unavailable, - } - } - - data class Content( - val items: ImmutableList, - val buttonState: Tokens.AddButtonState, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() { - - override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty - } - - data class AddFirstToken( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, - val onAddClick: () -> Unit, - ) : MyPortfolioUM() - - data object Loading : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } - - data object Unavailable : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } - - data object UnavailableForWallet : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt deleted file mode 100644 index b6dd772abc..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/PortfolioTokenUM.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.compose.runtime.Immutable -import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.extensions.TextReference -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.collections.immutable.ImmutableList - -@Immutable -internal sealed interface PortfolioListItem { - val id: String -} - -internal data class WalletHeader( - override val id: String, - val name: TextReference, -) : PortfolioListItem - -internal data class PortfolioHeader( - override val id: String, - val state: AccountTitleUM, -) : PortfolioListItem - -internal data class PortfolioTokenUM( - val tokenItemState: TokenItemState, - val walletId: UserWalletId, - val isBalanceHidden: Boolean, - val isQuickActionsShown: Boolean, - val quickActions: QuickActions, -) : PortfolioListItem { - override val id: String = tokenItemState.id - - data class QuickActions( - val actions: ImmutableList, - val onQuickActionClick: (QuickActionUM) -> Unit, - val onQuickActionLongClick: (QuickActionUM) -> Unit, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt deleted file mode 100644 index 040f3d243e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/QuickActionUM.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.features.markets.impl.R - -@Immutable -internal sealed class QuickActionUM( - val title: TextReference, - val description: TextReference, - @DrawableRes val icon: Int, - val isLongClickAvailable: Boolean = false, -) { - data object Buy : QuickActionUM( - title = resourceReference(R.string.common_buy), - description = resourceReference(R.string.buy_token_description), - icon = R.drawable.ic_plus_24, - ) - - data class Exchange( - val shouldShowBadge: Boolean, - ) : QuickActionUM( - title = resourceReference(R.string.common_exchange), - description = resourceReference(R.string.exсhange_token_description), - icon = R.drawable.ic_exchange_vertical_24, - ) - - data object Receive : QuickActionUM( - title = resourceReference(R.string.common_receive), - description = resourceReference(R.string.receive_token_description), - icon = R.drawable.ic_arrow_down_24, - isLongClickAvailable = true, - ) - - data object Stake : QuickActionUM( - title = resourceReference(R.string.common_stake), - description = resourceReference(R.string.stake_token_description), - icon = R.drawable.ic_staking_24, - ) - - data class YieldMode( - private val apy: String, - ) : QuickActionUM( - title = resourceReference(R.string.common_yield_mode), - description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), - icon = R.drawable.ic_analytics_up_mini_24, - ) -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt deleted file mode 100644 index 90830679ca..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/SelectNetworkUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import kotlinx.collections.immutable.ImmutableList - -internal data class SelectNetworkUM( - val tokenId: String, - val iconUrl: String?, - val tokenName: String, - val tokenCurrencySymbol: String, - val networks: ImmutableList, - val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt deleted file mode 100644 index 20db6ec796..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import androidx.annotation.DrawableRes -import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.markets.impl.R -import kotlinx.collections.immutable.ImmutableList - -internal data class TokenActionsBSContentUM( - val title: String, - val actions: ImmutableList, - val onActionClick: (Action) -> Unit, -) : TangemBottomSheetConfigContent { - - @Immutable - enum class Action( - val text: TextReference, - @DrawableRes val iconRes: Int, - ) { - CopyAddress( - text = resourceReference(R.string.common_copy_address), - iconRes = R.drawable.ic_copy_24, - ), - Send( - text = resourceReference(R.string.common_send), - iconRes = R.drawable.ic_arrow_up_24, - ), - Receive( - text = resourceReference(R.string.common_receive), - iconRes = R.drawable.ic_arrow_down_24, - ), - Buy( - text = resourceReference(R.string.common_buy), - iconRes = R.drawable.ic_plus_24, - ), - Sell( - text = resourceReference(R.string.common_sell), - iconRes = R.drawable.ic_currency_24, - ), - Exchange( - text = resourceReference(R.string.common_exchange), - iconRes = R.drawable.ic_exchange_horizontal_24, - ), - Stake( - text = resourceReference(R.string.common_stake), - iconRes = R.drawable.ic_staking_24, - ), - YieldMode( - text = resourceReference(R.string.common_yield_mode), - iconRes = R.drawable.ic_analytics_up_mini_24, - ), - ; - - val order: Int = ordinal - } -} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt deleted file mode 100644 index fddc12c25e..0000000000 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.markets.portfolio.impl.ui.state - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import kotlinx.collections.immutable.ImmutableList - -internal data class WalletSelectorBSContentUM( - val userWallets: ImmutableList, - val onBack: () -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt new file mode 100644 index 0000000000..2db32b29db --- /dev/null +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/model/formatter/Formatters.kt @@ -0,0 +1,12 @@ +package com.tangem.features.markets.token.block.impl.model.formatter + +import com.tangem.common.ui.charts.state.MarketChartLook +import com.tangem.core.ui.components.marketprice.PriceChangeType + +internal fun PriceChangeType.toChartType(): MarketChartLook.Type { + return when (this) { + PriceChangeType.UP -> MarketChartLook.Type.Growing + PriceChangeType.DOWN -> MarketChartLook.Type.Falling + PriceChangeType.NEUTRAL -> MarketChartLook.Type.Neutral + } +} \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt index 8f233b53ab..362075a2e2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlock.kt @@ -23,8 +23,8 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.markets.details.impl.model.formatter.toChartType import com.tangem.features.markets.impl.R +import com.tangem.features.markets.token.block.impl.model.formatter.toChartType import com.tangem.features.markets.token.block.impl.ui.state.TokenMarketBlockUM import kotlinx.collections.immutable.toImmutableList import kotlin.random.Random diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 199094b424..6bcf590744 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -30,8 +30,6 @@ import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejec import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.features.biometry.AskBiometryComponent import com.tangem.features.feed.entry.components.FeedEntryComponent -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle -import com.tangem.features.markets.entry.MarketsEntryComponent import com.tangem.features.pushnotifications.api.PushNotificationsBottomSheetComponent import com.tangem.features.pushnotifications.api.PushNotificationsParams import com.tangem.features.tokenreceive.TokenReceiveComponent @@ -46,14 +44,12 @@ import kotlinx.coroutines.launch internal class WalletComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted navigate: (WalletRoute) -> Unit, - marketsEntryComponentFactory: MarketsEntryComponent.Factory, feedEntryComponentFactory: FeedEntryComponent.Factory, private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val askBiometryComponentFactory: AskBiometryComponent.Factory, private val pushNotificationsBottomSheetComponent: PushNotificationsBottomSheetComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyDepositedWarningComponent: YieldSupplyDepositedWarningComponent.Factory, - private val feedFeatureToggle: FeedFeatureToggle, private val designFeatureToggles: DesignFeatureToggles, ) : ComposableContentComponent, AppComponentContext by appComponentContext { @@ -65,9 +61,6 @@ internal class WalletComponent @AssistedInject constructor( entryRoute = null, ) } - private val marketsEntryComponent by lazy { - marketsEntryComponentFactory.create(child("marketsEntryComponent")) - } init { lifecycle.subscribe(model.screenLifecycleProvider) @@ -194,19 +187,11 @@ internal class WalletComponent @AssistedInject constructor( onHeaderSizeChange: (Dp) -> Unit, modifier: Modifier = Modifier, ) { - if (feedFeatureToggle.isFeedEnabled) { - feedEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, - ) - } else { - marketsEntryComponent.BottomSheetContent( - bottomSheetState = bottomSheetState, - onHeaderSizeChange = onHeaderSizeChange, - modifier = modifier, - ) - } + feedEntryComponent.BottomSheetContent( + bottomSheetState = bottomSheetState, + onHeaderSizeChange = onHeaderSizeChange, + modifier = modifier, + ) } @AssistedFactory 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 507b19eb6d..6b19da9a9f 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 @@ -45,7 +45,6 @@ import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSend import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.KycRejectedCallbacks import com.tangem.feature.wallet.presentation.wallet.utils.ScreenLifecycleProvider import com.tangem.features.biometry.AskBiometryComponent -import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.pushnotifications.api.PushNotificationsModelCallbacks import com.tangem.features.wallet.deeplink.WalletDeepLinkActionListener import com.tangem.utils.Provider @@ -96,7 +95,6 @@ internal class WalletModel @Inject constructor( private val trackingContextProxy: TrackingContextProxy, private val singleAccountListSupplier: SingleAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val feedFeatureToggle: FeedFeatureToggle, private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase, private val appsFlyerStore: AppsFlyerStore, val screenLifecycleProvider: ScreenLifecycleProvider, @@ -118,7 +116,6 @@ internal class WalletModel @Inject constructor( init { trackScreenOpened() - updateMarketToggle() suggestToOpenMarkets() maybeMigrateNames() @@ -155,12 +152,6 @@ internal class WalletModel @Inject constructor( } } - private fun updateMarketToggle() { - stateHolder.update { - it.copy(isNewMarketEnabled = feedFeatureToggle.isFeedEnabled) - } - } - private fun updateYieldSupplyApy() { modelScope.launch(dispatchers.default) { yieldSupplyApyUpdateUseCase() 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 78b73ebfa5..fb85ffc73d 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 @@ -224,7 +224,6 @@ internal object WalletScreenPreviewData { isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsTooltip = {}, - isNewMarketEnabled = false, ) internal val accountScreenState = diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt index 8f877c0751..b1ddbd67b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/WalletStateController.kt @@ -139,7 +139,6 @@ internal class WalletStateController @Inject constructor( isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsTooltip = {}, - isNewMarketEnabled = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt index fa655d1daf..fe7cb111e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletScreenState.kt @@ -14,6 +14,5 @@ internal data class WalletScreenState( val event: StateEvent, val isHidingMode: Boolean, val showMarketsOnboarding: Boolean, - val isNewMarketEnabled: Boolean, val onDismissMarketsTooltip: () -> Unit, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 6bdd528c1b..324c26f234 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -304,11 +304,7 @@ private inline fun BaseScaffoldWithMarkets( val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() - val background = if (state.isNewMarketEnabled) { - TangemTheme.colors.background.tertiary - } else { - TangemTheme.colors.background.primary - } + val background = TangemTheme.colors.background.tertiary val showMarketsHint by remember { derivedStateOf { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index 01a5c16c67..e368898477 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -176,11 +176,7 @@ private inline fun BaseScaffoldWithMarkets( val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() - val background = if (state.isNewMarketEnabled) { - TangemTheme.colors2.surface.level2 - } else { - TangemTheme.colors.background.primary - } + val background = TangemTheme.colors2.surface.level2 CompositionLocalProvider( LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) }, From 1626270ae9719cd93c369e77ee190bf133c1304f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 19:06:21 +0400 Subject: [PATCH 52/97] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../moonpay/MoonpayBlockchainMapping.kt | 2 ++ .../converter/BlockchainSDKConfigConverter.kt | 9 ++++++++ .../GeneratedEnvironmentConfigConverter.kt | 17 ++++++++++++++ .../models/EnvironmentConfigModel.kt | 5 ++++ .../core/ui/extensions/BlockchainIcons.kt | 3 +++ .../src/main/res/drawable/ic_berachain_22.xml | 15 ++++++++++++ .../main/res/drawable/img_berachain_22.xml | 22 ++++++++++++++++++ .../data/common/network/NetworkFactory.kt | 1 + .../data/common/network/NetworkFactoryTest.kt | 7 ++++++ .../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 ++++ .../derivation/AccountNodeRecognizer.kt | 2 ++ .../derivation/AccountNodeRecognizerTest.kt | 23 +++++++++++++++++++ 17 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_berachain_22.xml create mode 100644 core/ui/src/main/res/drawable/img_berachain_22.xml diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 2d18650f6d..e4fac168bd 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 2d18650f6d4286353046ccb841745cef107c4fa6 +Subproject commit e4fac168bd941fe90b0f081dfa70ea1b4148b49f 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 1ff346fa44..74d8432e61 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 @@ -164,4 +164,6 @@ internal val Blockchain.moonPaySupportedCurrency: MoonPaySupportedCurrency? ArbitrumNova -> null Plasma, PlasmaTestnet -> null Monad, MonadTestnet -> null + Berachain -> MoonPaySupportedCurrency(networkCode = "berachain", currencyCode = "bera_bera") + BerachainTestnet -> null } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt index fab336d555..57c2a3abe7 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/converter/BlockchainSDKConfigConverter.kt @@ -34,6 +34,14 @@ internal object BlockchainSDKConfigConverter : Converter R.drawable.img_arbitrum_nova_22 "plasma", "plasma/test" -> R.drawable.img_plasma_22 "monad", "monad/test" -> R.drawable.img_monad_22 + "berachain", "berachain/test" -> R.drawable.img_berachain_22 else -> R.drawable.ic_alert_24 } } @@ -198,6 +199,7 @@ fun getActiveIconResByCoinId(coinId: String): Int { "arbitrum-nova" -> R.drawable.img_arbitrum_nova_22 "plasma", "plasma/test" -> R.drawable.img_plasma_22 "monad", "monad/test" -> R.drawable.img_monad_22 + "berachain-bera" -> R.drawable.img_berachain_22 else -> R.drawable.ic_alert_24 } } @@ -299,6 +301,7 @@ fun getGreyedOutIconRes(blockchainId: String): Int { "arbitrum-nova" -> R.drawable.ic_arbitrum_nova_22 "plasma", "plasma/test" -> R.drawable.ic_plasma_22 "monad", "monad/test" -> R.drawable.ic_monad_22 + "berachain", "berachain/test" -> R.drawable.ic_berachain_22 else -> R.drawable.ic_alert_24 } } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_berachain_22.xml b/core/ui/src/main/res/drawable/ic_berachain_22.xml new file mode 100644 index 0000000000..18aee83244 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_berachain_22.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/ui/src/main/res/drawable/img_berachain_22.xml b/core/ui/src/main/res/drawable/img_berachain_22.xml new file mode 100644 index 0000000000..c17d595496 --- /dev/null +++ b/core/ui/src/main/res/drawable/img_berachain_22.xml @@ -0,0 +1,22 @@ + + + + + + + + + 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 bf9014a844..096282c5e9 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 @@ -377,6 +377,7 @@ class NetworkFactory @Inject constructor( Blockchain.ArbitrumNova, Blockchain.Plasma, Blockchain.PlasmaTestnet, Blockchain.Monad, Blockchain.MonadTestnet, + Blockchain.Berachain, Blockchain.BerachainTestnet, -> Network.TransactionExtrasType.NONE // endregion } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt index e0fc158abe..6d3a6467d5 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/network/NetworkFactoryTest.kt @@ -461,6 +461,13 @@ class NetworkFactoryTest { Blockchain.Scroll, Blockchain.ScrollTestnet, Blockchain.ZkLinkNova, Blockchain.ZkLinkNovaTestnet, Blockchain.Pepecoin, Blockchain.PepecoinTestnet, + Blockchain.Hyperliquid, Blockchain.HyperliquidTestnet, + Blockchain.Quai, Blockchain.QuaiTestnet, + Blockchain.Linea, Blockchain.LineaTestnet, + Blockchain.ArbitrumNova, + Blockchain.Plasma, Blockchain.PlasmaTestnet, + Blockchain.Monad, Blockchain.MonadTestnet, + Blockchain.Berachain, Blockchain.BerachainTestnet, ), expected = Network.TransactionExtrasType.NONE, ), 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 c1bf85879a..6aeecb1989 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 @@ -164,5 +164,6 @@ public val Blockchain.mercuryoNetwork: String? Blockchain.ArbitrumNova -> null Blockchain.Plasma, Blockchain.PlasmaTestnet -> null Blockchain.Monad, Blockchain.MonadTestnet -> null + Blockchain.Berachain, Blockchain.BerachainTestnet -> 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 0c4ba1c0b0..77f35df7fd 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 @@ -219,6 +219,8 @@ data object Wallet2CardConfig : CardConfig { Blockchain.PlasmaTestnet -> EllipticCurve.Secp256k1 Blockchain.Monad -> EllipticCurve.Secp256k1 Blockchain.MonadTestnet -> EllipticCurve.Secp256k1 + Blockchain.Berachain -> EllipticCurve.Secp256k1 + Blockchain.BerachainTestnet -> 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 9ef3985637..7ae6b64eec 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 @@ -175,6 +175,8 @@ class Wallet2CardConfigTest { Blockchain.PlasmaTestnet to EllipticCurve.Secp256k1, Blockchain.Monad to EllipticCurve.Secp256k1, Blockchain.MonadTestnet to EllipticCurve.Secp256k1, + Blockchain.Berachain to EllipticCurve.Secp256k1, + Blockchain.BerachainTestnet to EllipticCurve.Secp256k1, ) @Test diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index ae62f1e18f..c84c960be6 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 = "releases-5.34-1430" +tangemBlockchainSdk = "develop-1437" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-578" #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 216070afad..2d2f7abe65 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 @@ -176,6 +176,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? { "plasma/test" -> Blockchain.PlasmaTestnet "monad" -> Blockchain.Monad "monad/test" -> Blockchain.MonadTestnet + "berachain" -> Blockchain.Berachain + "berachain/test" -> Blockchain.BerachainTestnet else -> null } } @@ -349,6 +351,8 @@ fun Blockchain.toNetworkId(): String { Blockchain.PlasmaTestnet -> "plasma/test" Blockchain.Monad -> "monad" Blockchain.MonadTestnet -> "monad/test" + Blockchain.Berachain -> "berachain" + Blockchain.BerachainTestnet -> "berachain/test" } } @@ -458,6 +462,7 @@ fun Blockchain.toCoinId(): String { Blockchain.ArbitrumNova -> "arbitrum-nova-ethereum" Blockchain.Plasma, Blockchain.PlasmaTestnet -> "plasma" Blockchain.Monad, Blockchain.MonadTestnet -> "monad" + Blockchain.Berachain, Blockchain.BerachainTestnet -> "berachain-bera" } } 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 32e90c0d4b..ee1b92dc52 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 @@ -178,6 +178,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { Blockchain.Quai, Blockchain.Plasma, Blockchain.Monad, + Blockchain.Berachain, -> true Blockchain.Nexa, // unsupported network Blockchain.Chia, @@ -254,6 +255,7 @@ class AccountNodeRecognizer(private val blockchain: Blockchain) { Blockchain.LineaTestnet, Blockchain.PlasmaTestnet, Blockchain.MonadTestnet, + Blockchain.BerachainTestnet, -> false // endregion } diff --git a/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt index 4d004e0616..756ae1c644 100644 --- a/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt +++ b/libs/crypto/src/test/kotlin/com/tangem/lib/crypto/derivation/AccountNodeRecognizerTest.kt @@ -103,6 +103,29 @@ internal class AccountNodeRecognizerTest { expected = null, ), // endregion + + // region Berachain blockchain (EVM-like) + TestModel( + blockchain = Blockchain.Berachain, + derivationPath = "m/44'/60'/0'/0/1", + expected = 1, + ), + TestModel( + blockchain = Blockchain.Berachain, + derivationPath = "m/44'/60'/0'/0/0", + expected = 0, + ), + TestModel( + blockchain = Blockchain.Berachain, + derivationPath = "m/44'/60'/0'/0", + expected = null, + ), + TestModel( + blockchain = Blockchain.Berachain, + derivationPath = "m/44'/60'", + expected = null, + ), + // endregion ) private fun provideUTXOTestModels() = listOf( From b27dd4ffa513d6ee035abb53eab50e7b06c2a665 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 15:27:49 +0000 Subject: [PATCH 53/97] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 423909e1bc..6934faad61 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.34-1430" +tangemBlockchainSdk = "develop-1437" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-581" +tangemCardSdk = "develop-582" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 435e579a6122fd47b133700e984db25023465383 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 25 Feb 2026 19:06:24 +0400 Subject: [PATCH 54/97] Updated on 2026-08-14 --- .../impl/DefaultMarketsPortfolioComponent.kt | 3 +- .../impl/loader/PortfolioDataLoader.kt | 135 ------ .../model/AddToPortfolioBSContentUMFactory.kt | 161 ------- .../impl/model/AddToPortfolioManager.kt | 193 -------- ...elegate.kt => MarketsPortfolioDelegate.kt} | 10 +- .../impl/model/MarketsPortfolioModel.kt | 415 +++--------------- .../impl/model/MyPortfolioUMFactory.kt | 153 ------- .../impl/model/PortfolioBSVisibilityModel.kt | 14 - .../portfolio/impl/model/PortfolioUIData.kt | 20 - .../impl/model/SelectNetworkUMConverter.kt | 37 -- .../impl/model/TokensPortfolioUMConverter.kt | 112 ----- .../impl/ui/AddToPortfolioBottomSheet.kt | 383 ---------------- .../details/portfolio/impl/ui/MyPortfolio.kt | 5 - .../impl/ui/TokenActionsBottomSheet.kt | 86 ---- .../impl/ui/WalletSelectorBottomSheet.kt | 145 ------ .../PreviewAddToPortfolioBSContentProvider.kt | 86 ---- .../preview/PreviewMyPortfolioUMProvider.kt | 9 +- .../ui/state/AddToPortfolioBSContentUM.kt | 15 - .../portfolio/impl/ui/state/MyPortfolioUM.kt | 22 +- .../impl/ui/state/SelectNetworkUM.kt | 13 - .../ui/state/WalletSelectorBSContentUM.kt | 10 - 21 files changed, 78 insertions(+), 1949 deletions(-) delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioDataLoader.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioManager.kt rename features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/{NewMarketsPortfolioDelegate.kt => MarketsPortfolioDelegate.kt} (97%) delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MyPortfolioUMFactory.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioBSVisibilityModel.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioUIData.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/SelectNetworkUMConverter.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokensPortfolioUMConverter.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/AddToPortfolioBottomSheet.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/TokenActionsBottomSheet.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/WalletSelectorBottomSheet.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/AddToPortfolioBSContentUM.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/SelectNetworkUM.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/DefaultMarketsPortfolioComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/DefaultMarketsPortfolioComponent.kt index 6df78dda91..a1cf255e9b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/DefaultMarketsPortfolioComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/DefaultMarketsPortfolioComponent.kt @@ -58,7 +58,6 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( bottomSheet.child?.instance?.BottomSheet() } - @Suppress("UnsafeCallOnNullableType") private fun bottomSheetChild( config: MarketsPortfolioRoute, componentContext: ComponentContext, @@ -66,7 +65,7 @@ internal class DefaultMarketsPortfolioComponent @AssistedInject constructor( MarketsPortfolioRoute.AddToPortfolio -> addToPortfolioComponentFactory.create( context = childByContext(componentContext), params = AddToPortfolioComponent.Params( - addToPortfolioManager = model.newAddToPortfolioManager!!, + addToPortfolioManager = model.addToPortfolioManager, callback = model.addToPortfolioCallback, ), ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioDataLoader.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioDataLoader.kt deleted file mode 100644 index c22e143ea1..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioDataLoader.kt +++ /dev/null @@ -1,135 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.loader - -import arrow.core.getOrElse -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetAllWalletsCryptoCurrencyStatusesUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase -import com.tangem.domain.tokens.GetWalletTotalBalanceUseCase -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.yield.supply.models.YieldSupplyAvailability -import com.tangem.domain.yield.supply.usecase.YieldSupplyGetAvailabilityUseCase -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -/** - * Loader of portfolio data - * - * @property getAllWalletsCryptoCurrencyStatusesUseCase use case for getting all wallets crypto currency statuses - * @property getSelectedAppCurrencyUseCase use case for getting selected app currency - * @property getBalanceHidingSettingsUseCase use case for getting balance hiding settings - * @property getWalletTotalBalanceUseCase use case for getting wallet total balance - * -[REDACTED_AUTHOR] - */ -internal class PortfolioDataLoader @Inject constructor( - private val getAllWalletsCryptoCurrencyStatusesUseCase: GetAllWalletsCryptoCurrencyStatusesUseCase, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getWalletTotalBalanceUseCase: GetWalletTotalBalanceUseCase, - private val yieldSupplyGetAvailabilityUseCase: YieldSupplyGetAvailabilityUseCase, - private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, -) { - - /** Load data by [currencyRawId] */ - @OptIn(ExperimentalCoroutinesApi::class) - fun load(currencyRawId: CryptoCurrency.RawID): Flow { - return combine( - flow = getAllWalletsCryptoCurrenciesData(currencyRawId = currencyRawId), - flow2 = getSelectedAppCurrencyUseCase.invokeOrDefault(), - flow3 = getBalanceHidingSettingsUseCase.isBalanceHidden(), - ) { walletsWithCurrencies, appCurrency, isBalanceHidden -> - PortfolioData( - walletsWithCurrencies = walletsWithCurrencies, - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - walletsWithBalance = emptyMap(), - ) - } - // setup balances for wallets from walletsWithCurrencyStatuses - .flatMapLatest { portfolioData -> - getWalletsWithTotalBalanceFlow( - ids = portfolioData.walletsWithCurrencies.keys.map(UserWallet::walletId), - ) - .map { portfolioData.copy(walletsWithBalance = it) } - .onEmpty { emit(portfolioData) } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - private fun getAllWalletsCryptoCurrenciesData( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) - .distinctUntilChanged() - .map { walletsWithMaybeStatuses -> - walletsWithMaybeStatuses.mapValues { entry -> - entry.value.mapNotNull { it.getOrNull() } - } - } - .flatMapLatest { walletsWithStatuses -> - val actionsFlows = walletsWithStatuses.flatMap { (wallet, statuses) -> - statuses.map { status -> - val yieldSupplyAvailability = yieldSupplyGetAvailabilityUseCase(status.currency).getOrElse { - YieldSupplyAvailability.Unavailable - } - getCryptoCurrencyActionsUseCase(wallet, status, yieldSupplyAvailability) - .map { tokenActionsState -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = tokenActionsState.states, - ) - } - } - } - - combine(actionsFlows) { actions -> - walletsWithStatuses.mapValues { entry -> - entry.value.mapNotNull { status -> - actions.firstOrNull { - it.userWallet == entry.key && it.status == status - } - } - } - }.onEmpty { - emit( - walletsWithStatuses.mapValues { (wallet, statuses) -> - statuses.map { status -> - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = status, - actions = emptyList(), - ) - } - }, - ) - } - }.onEmpty { - emit(emptyMap()) - } - .distinctUntilChanged() - } - - private fun getWalletsWithTotalBalanceFlow( - ids: List, - ): Flow>> { - return combine( - flows = ids - .map { userWalletId -> - getWalletTotalBalanceUseCase(userWalletId) - .map { userWalletId to it } - .distinctUntilChanged() - }, - transform = { it.toMap() }, - ) - .distinctUntilChanged() - .onEmpty { ids.associateWith { Lce.Loading(partialContent = null) } } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt deleted file mode 100644 index e139659522..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioBSContentUMFactory.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.common.ui.userwallet.converter.UserWalletItemUMConverter -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM -import kotlinx.collections.immutable.toImmutableList - -/** - * Factory to create AddToPortfolio bottom sheet content [TangemBottomSheetConfig] - * - * @property token token params - * @property onAddToPortfolioVisibilityChange callback is invoked when add to portfolio visibility is changed - * @property onWalletSelectorVisibilityChange callback is invoked when wallet selector visibility is changed - * @property onNetworkSwitchClick callback is invoked when network switch is clicked - * @property onAnotherWalletSelect callback is invoked when wallet is selected - * @property onContinueClick callback is invoked when continue button is clicked - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class AddToPortfolioBSContentUMFactory( - private val addToPortfolioManager: AddToPortfolioManager, - private val token: TokenMarketParams, - private val onAddToPortfolioVisibilityChange: (Boolean) -> Unit, - private val onWalletSelectorVisibilityChange: (Boolean) -> Unit, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, - private val onAnotherWalletSelect: (UserWalletId) -> Unit, - private val onContinueClick: (selectedWalletId: UserWalletId, addedNetworks: Set) -> Unit, -) { - - /** - * Create [TangemBottomSheetConfig] - * - - * @param portfolioData portfolio data - * @param portfolioUIData portfolio bottom sheet visibility model - * @param selectedWallet selected wallet - * @param alreadyAddedNetworks already added networks - */ - @Suppress("LongParameterList") - fun create( - currentState: TangemBottomSheetConfig?, - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - selectedWallet: UserWallet?, - alreadyAddedNetworks: Set?, - artworks: Map, - ): TangemBottomSheetConfig { - return (currentState ?: TangemBottomSheetConfig.Empty).copy( - isShown = portfolioUIData.portfolioBSVisibilityModel.isAddToPortfolioBSVisible, - onDismissRequest = { onAddToPortfolioVisibilityChange(false) }, - content = if (selectedWallet != null && alreadyAddedNetworks != null) { - AddToPortfolioBSContentUM( - selectedWallet = selectedWallet.toSelectedUserWalletItemUM( - portfolioData = portfolioData, - balance = portfolioData.walletsWithBalance[selectedWallet.walletId]?.getOrNull(), - artwork = artworks[selectedWallet.walletId], - ), - selectNetworkUM = SelectNetworkUMConverter( - networksWithToggle = addToPortfolioManager.associateWithToggle( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - addToPortfolioData = portfolioUIData.addToPortfolioData, - ), - alreadyAddedNetworks = alreadyAddedNetworks, - onNetworkSwitchClick = onNetworkSwitchClick, - ).convert(value = token), - isScanCardNotificationVisible = portfolioUIData.isNeededColdWalletInteraction, - isContinueButtonEnabled = portfolioUIData.addToPortfolioData.isUserAddedNetworks( - userWalletId = selectedWallet.walletId, - ), - onContinueButtonClick = { - onContinueClick( - selectedWallet.walletId, - portfolioUIData.addToPortfolioData.getAddedNetworks( - userWalletId = selectedWallet.walletId, - alreadyAddedNetworkIds = alreadyAddedNetworks, - ), - ) - }, - walletSelectorConfig = createWalletSelectorBSConfig( - isShow = portfolioUIData.portfolioBSVisibilityModel.isWalletSelectorBSVisible, - portfolioData = portfolioData, - selectedWalletId = selectedWallet.walletId, - artworks = artworks, - ), - isWalletBlockVisible = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency).size > 1, - ) - } else { - TangemBottomSheetConfigContent.Empty - }, - ) - } - - private fun UserWallet.toSelectedUserWalletItemUM( - artwork: UserWalletItemUM.ImageState? = null, - portfolioData: PortfolioData, - balance: TotalFiatBalance?, - ): UserWalletItemUM { - return UserWalletItemUMConverter( - onClick = { onWalletSelectorVisibilityChange(true) }, - endIcon = UserWalletItemUM.EndIcon.Arrow, - balance = balance, - artwork = artwork, - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - ).convert(value = this) - } - - private fun createWalletSelectorBSConfig( - isShow: Boolean, - portfolioData: PortfolioData, - selectedWalletId: UserWalletId, - artworks: Map, - ): TangemBottomSheetConfig { - return TangemBottomSheetConfig( - isShown = isShow, - onDismissRequest = { onWalletSelectorVisibilityChange(false) }, - content = WalletSelectorBSContentUM( - userWallets = portfolioData.walletsWithCurrencies - .filterKeys(UserWallet::isMultiCurrency) - .map { it.key } - .map { userWallet -> - val balance = portfolioData.walletsWithBalance[userWallet.walletId] - - UserWalletItemUMConverter( - onClick = { id -> - if (id != selectedWalletId) { - onAnotherWalletSelect(id) - onWalletSelectorVisibilityChange(false) - } - }, - appCurrency = portfolioData.appCurrency, - balance = balance?.getOrNull(), - isBalanceHidden = portfolioData.isBalanceHidden, - endIcon = if (userWallet.walletId == selectedWalletId) { - UserWalletItemUM.EndIcon.Checkmark - } else { - UserWalletItemUM.EndIcon.None - }, - artwork = artworks[userWallet.walletId], - ).convert(userWallet) - } - .toImmutableList(), - onBack = { onWalletSelectorVisibilityChange(false) }, - ), - ) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioManager.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioManager.kt deleted file mode 100644 index d176536657..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/AddToPortfolioManager.kt +++ /dev/null @@ -1,193 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.update -import timber.log.Timber -import javax.inject.Inject -import kotlin.collections.firstOrNull -import kotlin.collections.orEmpty -import kotlin.collections.set - -internal typealias WalletsWithNetworks = Map> - -/** - * Manager for tracking changing networks in AddToPortfolio - * -[REDACTED_AUTHOR] - */ -internal class AddToPortfolioManager @Inject constructor( - private val filterAvailableNetworksForWalletUseCase: FilterAvailableNetworksForWalletUseCase, -) { - - val availableNetworks = MutableStateFlow?>(value = null) - private val addedNetworks = MutableStateFlow(value = emptyMap()) - private val removedNetworks = MutableStateFlow(value = emptyMap()) - - /** Get [AddToPortfolioData] as flow */ - fun getAddToPortfolioData(): Flow { - return combine( - flow = availableNetworks, - flow2 = addedNetworks, - flow3 = removedNetworks, - transform = ::AddToPortfolioData, - ) - } - - /** Set available networks [networks] */ - fun setAvailableNetworks(networks: List) { - availableNetworks.value = networks.toSet() - } - - /** Add network [networkId] to [userWalletId] */ - fun addNetwork(userWalletId: UserWalletId, networkId: String) { - addedNetworks.add(userWalletId, networkId) - - removedNetworks.cancelPrevChangeIfExist(userWalletId = userWalletId, networkId = networkId) - } - - /** Remove network [networkId] from [userWalletId] */ - fun removeNetwork(userWalletId: UserWalletId, networkId: String) { - removedNetworks.add(userWalletId, networkId) - - addedNetworks.cancelPrevChangeIfExist( - userWalletId = userWalletId, - networkId = networkId, - ) - } - - /** Remove all networks by [userWalletId] */ - fun removeAllChanges(userWalletId: UserWalletId) { - addedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - - removedNetworks.update { - it.toMutableMap().apply { remove(userWalletId) } - } - } - - fun associateWithToggle( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - addToPortfolioData: AddToPortfolioData, - ): Map { - val filteredNetworks = filterAvailableNetworksForWalletUseCase( - userWalletId = userWalletId, - networks = addToPortfolioData.availableNetworks.orEmpty(), - ) - // Use user choice or check already added networks - return filteredNetworks.associateWith { availableNetwork -> - val isAddedByUser = addToPortfolioData.addedNetworks[userWalletId]?.contains(availableNetwork) - - if (isAddedByUser == true) return@associateWith true - - val isRemovedByUser = addToPortfolioData.removedNetworks[userWalletId]?.contains(availableNetwork) - - if (isRemovedByUser == true) return@associateWith false - - val isAddedBefore = alreadyAddedNetworkIds.any { it == availableNetwork.networkId } - - isAddedBefore - } - } - - private fun MutableStateFlow.cancelPrevChangeIfExist( - userWalletId: UserWalletId, - networkId: String, - ) { - if (value[userWalletId].orEmpty().any { it.networkId == networkId }) remove(userWalletId, networkId) - } - - private fun MutableStateFlow.add(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = true) - } - - private fun MutableStateFlow.remove(userWalletId: UserWalletId, networkId: String) { - change(userWalletId = userWalletId, networkId = networkId, isAddAction = false) - } - - private fun MutableStateFlow.change( - userWalletId: UserWalletId, - networkId: String, - isAddAction: Boolean, - ) { - val network = availableNetworks.value.orEmpty().firstOrNull { it.networkId == networkId } - - if (network == null) { - Timber.d( - "Network [$networkId] doesn't contain in available networks [%s]", - availableNetworks.value?.joinToString { it.networkId }, - ) - - return - } - - update { walletsWithNetworks -> - walletsWithNetworks.toMutableMap().apply { - this[userWalletId] = if (isAddAction) { - this[userWalletId].orEmpty() + network - } else { - this[userWalletId].orEmpty() - network - } - } - } - } - - /** - * Add to portfolio data - * - * @property availableNetworks available networks that user can add to portfolio - * @property addedNetworks networks that user toggled on, but it might have already been added to the wallet - * @property removedNetworks networks that user toggled off, but it might haven't been added to the wallet - * - * Example for [addedNetworks] and [removedNetworks]. This lists will include new networks when user just - * toggle it. But when we will save user changes, we will check what tokens have already been added or - * haven't been added to the wallet. See [getAddedNetworks] and [getRemovedNetworks] - */ - data class AddToPortfolioData( - val availableNetworks: Set?, - val addedNetworks: WalletsWithNetworks, - val removedNetworks: WalletsWithNetworks, - ) { - - fun isUserAddedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - fun isUserChangedNetworks(userWalletId: UserWalletId): Boolean { - return addedNetworks[userWalletId].orEmpty().isNotEmpty() || - removedNetworks[userWalletId].orEmpty().isNotEmpty() - } - - /** Get new networks that user [userWalletId] added using [alreadyAddedNetworkIds] */ - fun getAddedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val addedNetworksByUser = addedNetworks[userWalletId].orEmpty() - - return addedNetworksByUser.map { it.networkId } - .minus(alreadyAddedNetworkIds) - .mapNotNull { networkId -> addedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - - /** Get networks that user [userWalletId] removed using [alreadyAddedNetworkIds] */ - fun getRemovedNetworks( - userWalletId: UserWalletId, - alreadyAddedNetworkIds: Set, - ): Set { - val removedNetworksByUser = removedNetworks[userWalletId].orEmpty() - - return alreadyAddedNetworkIds - .minus(removedNetworksByUser.map { it.networkId }.toSet()) - .mapNotNull { networkId -> removedNetworksByUser.firstOrNull { it.networkId == networkId } } - .toSet() - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt similarity index 97% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt rename to features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt index 2df91aab47..50cf8f9881 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/NewMarketsPortfolioDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt @@ -5,7 +5,6 @@ import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.account.models.AccountStatusList @@ -45,7 +44,7 @@ import kotlinx.coroutines.flow.* @OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList") -internal class NewMarketsPortfolioDelegate @AssistedInject constructor( +internal class MarketsPortfolioDelegate @AssistedInject constructor( getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val allAccountSupplier: MultiAccountStatusListSupplier, @@ -106,10 +105,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( private fun addFirstTokenFlow(): Flow = buttonState.map { state -> when (state) { MyPortfolioUM.Tokens.AddButtonState.Loading -> MyPortfolioUM.Loading - MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken( - onAddClick = onAddClick, - addToPortfolioBSConfig = TangemBottomSheetConfig.Empty, - ) + MyPortfolioUM.Tokens.AddButtonState.Available -> MyPortfolioUM.AddFirstToken(onAddClick = onAddClick) MyPortfolioUM.Tokens.AddButtonState.Unavailable -> MyPortfolioUM.Unavailable } } @@ -314,7 +310,7 @@ internal class NewMarketsPortfolioDelegate @AssistedInject constructor( tokenActionsHandler: TokenActionsHandler, buttonState: Flow, onAddClick: () -> Unit, - ): NewMarketsPortfolioDelegate + ): MarketsPortfolioDelegate } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt index 71d48270b1..7c7818ad79 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt @@ -5,91 +5,48 @@ import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss -import com.tangem.common.ui.userwallet.state.UserWalletItemUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -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.message.DialogMessage -import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.model.CurrencyUnsupportedState -import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.currency.CryptoCurrency -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.transaction.usecase.ReceiveAddressesFactory -import com.tangem.domain.wallets.usecase.ColdWalletAndHasMissedDerivationsUseCase -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioDataLoader import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.features.feed.impl.R -import com.tangem.features.wallet.utils.UserWalletImageFetcher -import com.tangem.lib.crypto.BlockchainUtils -import com.tangem.operations.attestation.ArtworkSize import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import timber.log.Timber import javax.inject.Inject -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager as NewAddToPortfolioManager -@Suppress("LongParameterList", "LargeClass") +@Suppress("LongParameterList") @Stable @ModelScoped internal class MarketsPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, - getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - tokenActionsIntentsFactory: TokenActionsHandler.Factory, - override val dispatchers: CoroutineDispatcherProvider, - private val messageSender: UiMessageSender, - private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, - private val portfolioDataLoader: PortfolioDataLoader, - private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - private val saveMarketTokensUseCase: SaveMarketTokensUseCase, - private val addToPortfolioManager: AddToPortfolioManager, - private val analyticsEventHandler: AnalyticsEventHandler, - private val userWalletImageFetcher: UserWalletImageFetcher, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, + private val marketsPortfolioDelegateFactory: MarketsPortfolioDelegate.Factory, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val tokenActionsHandlerFactory: TokenActionsHandler.Factory, private val receiveAddressesFactory: ReceiveAddressesFactory, - accountsFeatureToggles: AccountsFeatureToggles, - newAddToPortfolioManagerFactory: NewAddToPortfolioManager.Factory, - newMarketsPortfolioDelegateFactory: NewMarketsPortfolioDelegate.Factory, + private val analyticsEventHandler: AnalyticsEventHandler, + override val dispatchers: CoroutineDispatcherProvider, ) : Model() { - private val _state: MutableStateFlow = MutableStateFlow(value = MyPortfolioUM.Loading) - val state: StateFlow get() = _state + val state: StateFlow + field = MutableStateFlow(value = MyPortfolioUM.Loading) private val params = paramsContainer.require() - private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( - tokenSymbol = params.token.symbol, - source = params.analyticsParams?.source, - ) - val newAddToPortfolioManager: NewAddToPortfolioManager? - val newMarketsPortfolioDelegate: NewMarketsPortfolioDelegate? - - /** Multi-wallet [UserWalletId] that user uses to add new tokens in AddToPortfolio bottom sheet */ - private val selectedMultiWalletIdFlow = MutableStateFlow(value = null) - - private val portfolioBSVisibilityModelFlow = MutableStateFlow(value = PortfolioBSVisibilityModel()) + val addToPortfolioManager: AddToPortfolioManager = createAddToPortfolioManager() + private val marketsPortfolioDelegate: MarketsPortfolioDelegate = createMarketsPortfolioDelegate() val bottomSheetNavigation: SlotNavigation = SlotNavigation() val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { @@ -97,321 +54,87 @@ internal class MarketsPortfolioModel @Inject constructor( override fun onSuccess(addedToken: CryptoCurrency) = bottomSheetNavigation.dismiss() } - private val currentAppCurrency = getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( - scope = modelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) - - private val tokenActionsHandler = tokenActionsIntentsFactory.create( - currentAppCurrency = Provider { currentAppCurrency.value }, - onHandleQuickAction = { handledAction -> - analyticsEventHandler.send( - analyticsEventBuilder.quickActionClick( - actionUM = handledAction.action, - blockchainName = handledAction - .cryptoCurrencyData - .status - .currency - .network - .name, - ), - ) - configureReceiveAddresses(handledAction) - }, + private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( + tokenSymbol = params.token.symbol, + source = params.analyticsParams?.source, ) - private val factory = MyPortfolioUMFactory( - onAddClick = { - onAddToPortfolioBSVisibilityChange(isShow = true) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioClicked(), - ) - }, - addToPortfolioBSContentUMFactory = AddToPortfolioBSContentUMFactory( - addToPortfolioManager = addToPortfolioManager, - token = params.token, - onAddToPortfolioVisibilityChange = ::onAddToPortfolioBSVisibilityChange, - onWalletSelectorVisibilityChange = ::onWalletSelectorVisibilityChange, - onNetworkSwitchClick = ::onNetworkSwitchClick, - onAnotherWalletSelect = { walletId -> - onWalletSelect(walletId) - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioWalletChanged(), - ) - }, - onContinueClick = { selectedWalletId, addedNetworks -> - onContinueClick(selectedWalletId, addedNetworks) + private val currentAppCurrency = createAppCurrencyFlow() - // === Analytics === - analyticsEventHandler.send( - analyticsEventBuilder.addToPortfolioContinue( - blockchainNames = addedNetworks.mapNotNull { - BlockchainUtils.getNetworkInfo(it.networkId)?.name - }, - ), - ) - }, - ), - currentState = Provider { _state.value }, - tokenActionsHandler = tokenActionsHandler, - updateTokens = { updateBlock -> - updateTokensState { state -> - state.copy(tokens = updateBlock(state.tokens)) - } - }, - ) + private val tokenActionsHandler = createTokenActionsHandler() init { - if (accountsFeatureToggles.isFeatureEnabled) { - newAddToPortfolioManager = newAddToPortfolioManagerFactory - .create( - modelScope, - params.token, - params.analyticsParams?.source?.let { NewAddToPortfolioManager.AnalyticsParams(it) }, - ) - newMarketsPortfolioDelegate = newMarketsPortfolioDelegateFactory.create( - scope = modelScope, - token = params.token, - tokenActionsHandler = tokenActionsHandler, - buttonState = newAddToPortfolioManager.state.map { state -> - when (state) { - is NewAddToPortfolioManager.State.AvailableToAdd -> { - MyPortfolioUM.Tokens.AddButtonState.Available - } - NewAddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading - NewAddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable - } - }, - onAddClick = { - analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) - bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) - }, - ) - newMarketsPortfolioDelegate.combineData() - .onEach { _state.value = it } - .flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - newAddToPortfolioManager = null - newMarketsPortfolioDelegate = null - // Subscribe on selected wallet flow to support actual selected wallet - subscribeOnSelectedMultiWalletUpdates() - - subscribeOnStateUpdates() - } + marketsPortfolioDelegate.combineData() + .onEach { state.value = it } + .flowOn(dispatchers.default) + .launchIn(modelScope) } fun setTokenNetworks(networks: List) { - addToPortfolioManager.setAvailableNetworks(networks) - newAddToPortfolioManager?.setTokenNetworks(networks) - newMarketsPortfolioDelegate?.setTokenNetworks(networks) + addToPortfolioManager.setTokenNetworks(networks) + marketsPortfolioDelegate.setTokenNetworks(networks) } fun setNoNetworksAvailable() { - addToPortfolioManager.setAvailableNetworks(emptyList()) - newAddToPortfolioManager?.setTokenNetworks(emptyList()) - newMarketsPortfolioDelegate?.setTokenNetworks(emptyList()) + addToPortfolioManager.setTokenNetworks(emptyList()) + marketsPortfolioDelegate.setTokenNetworks(emptyList()) } - private fun subscribeOnSelectedMultiWalletUpdates() { - getSelectedWalletUseCase() - .getOrElse { e -> - Timber.e("Failed to load selected wallet: $e") - error("Failed to load selected wallet") - } - .onEach { userWallet -> - selectedMultiWalletIdFlow.value = userWallet.takeIf { it.isMultiCurrency }?.walletId - } - .launchIn(modelScope) - } - - private fun subscribeOnStateUpdates() { - combine( - flow = loadPortfolioDataWithArtworks(params.token.id), - flow2 = getPortfolioUIDataFlow(), - transform = { pair, portfolioUIData -> - val (portfolioData, artworks) = pair - factory.create(portfolioData, portfolioUIData, artworks) - }, - ) - .onEach { _state.value = it } - .launchIn(modelScope) - } - - private fun loadPortfolioDataWithArtworks( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - val wallets = Channel>() - val portfolioFlow = portfolioDataLoader - .load(currencyRawId) - .onEach { wallets.trySend(it.walletsWithCurrencies.keys) } - - val artworksFlow = wallets.receiveAsFlow() - .distinctUntilChanged() - .flatMapLatest { userWalletImageFetcher.walletsImage(wallets = it, size = ArtworkSize.SMALL) } - - return combine( - flow = portfolioFlow, - flow2 = artworksFlow, - ) { portfolioData, artworks -> portfolioData to artworks } - } - - private fun getPortfolioUIDataFlow(): Flow { - return combine( - flow = portfolioBSVisibilityModelFlow, - flow2 = selectedMultiWalletIdFlow, - flow3 = addToPortfolioManager.getAddToPortfolioData(), - transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData -> - PortfolioUIData( - portfolioBSVisibilityModel = portfolioBSVisibilityModel, - selectedWalletId = selectedWalletId, - addToPortfolioData = addToPortfolioData, - isNeededColdWalletInteraction = isNeededColdWalletInteraction(selectedWalletId, addToPortfolioData), - ) - }, + private fun createAddToPortfolioManager(): AddToPortfolioManager { + return addToPortfolioManagerFactory.create( + scope = modelScope, + token = params.token, + analyticsParams = params.analyticsParams?.source?.let { AddToPortfolioManager.AnalyticsParams(it) }, ) } - private suspend fun isNeededColdWalletInteraction( - selectedWalletId: UserWalletId?, - addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - ): Boolean { - return if (selectedWalletId != null) { - coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = selectedWalletId, - networksWithDerivationPath = addToPortfolioData.addedNetworks[selectedWalletId].orEmpty() - .associate { it.networkId to null }, - ) - } else { - false - } - } - - private fun onNetworkSwitchClick(blockchainRowUM: BlockchainRowUM, isChecked: Boolean) { - val selectedWalletId = selectedMultiWalletIdFlow.value - - if (selectedWalletId == null) { - Timber.e("Impossible to switch network when selected wallet is null") - return - } - - if (isChecked) { - modelScope.launch { - val unsupportedState = checkCurrencyUnsupportedState( - userWalletId = selectedWalletId, - rawNetworkId = blockchainRowUM.id, - isMainNetwork = blockchainRowUM.isMainNetwork, - ) - if (unsupportedState != null) { - showUnsupportedWarning(unsupportedState) - } else { - addToPortfolioManager.addNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) + private fun createMarketsPortfolioDelegate(): MarketsPortfolioDelegate { + return marketsPortfolioDelegateFactory.create( + scope = modelScope, + token = params.token, + tokenActionsHandler = tokenActionsHandler, + buttonState = addToPortfolioManager.state.map { state -> + when (state) { + is AddToPortfolioManager.State.AvailableToAdd -> { + MyPortfolioUM.Tokens.AddButtonState.Available + } + AddToPortfolioManager.State.Init -> MyPortfolioUM.Tokens.AddButtonState.Loading + AddToPortfolioManager.State.NothingToAdd -> MyPortfolioUM.Tokens.AddButtonState.Unavailable } - } - } else { - addToPortfolioManager.removeNetwork(userWalletId = selectedWalletId, networkId = blockchainRowUM.id) - } - } - - private suspend fun checkCurrencyUnsupportedState( - userWalletId: UserWalletId, - rawNetworkId: String, - isMainNetwork: Boolean, - ): CurrencyUnsupportedState? { - return checkCurrencyUnsupportedUseCase( - userWalletId = userWalletId, - networkId = rawNetworkId, - isMainNetwork = isMainNetwork, - ).getOrElse { throwable -> - Timber.e( - throwable, - """ - Failed to check currency unsupported state - |- User wallet ID: $userWalletId - |- Network ID: $rawNetworkId - |- Is main network: $isMainNetwork - """.trimIndent(), - ) - - val message = SnackbarMessage( - message = throwable.localizedMessage - ?.let(::stringReference) - ?: resourceReference(R.string.common_error), - ) - messageSender.send(message) - - null - } - } - - private fun showUnsupportedWarning(unsupportedState: CurrencyUnsupportedState) { - val message = DialogMessage( - message = when (unsupportedState) { - is CurrencyUnsupportedState.Token.NetworkTokensUnsupported -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.Token.UnsupportedCurve -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) - is CurrencyUnsupportedState.UnsupportedNetwork -> resourceReference( - id = R.string.alert_manage_tokens_unsupported_curve_message, - formatArgs = wrappedList(unsupportedState.networkName), - ) + }, + onAddClick = { + analyticsEventHandler.send(analyticsEventBuilder.addToPortfolioClicked()) + bottomSheetNavigation.activate(MarketsPortfolioRoute.AddToPortfolio) }, ) - - messageSender.send(message) } - private fun onWalletSelect(userWalletId: UserWalletId) { - selectedMultiWalletIdFlow.update { prevUserWalletId -> - prevUserWalletId?.let(addToPortfolioManager::removeAllChanges) - - userWalletId - } - } - - private fun onContinueClick(userWalletId: UserWalletId, addedNetworks: Set) { - modelScope.launch { - saveMarketTokensUseCase( - userWalletId = userWalletId, - tokenMarketParams = params.token, - addedNetworks = addedNetworks, - removedNetworks = emptySet(), + private fun createAppCurrencyFlow(): StateFlow { + return getSelectedAppCurrencyUseCase() + .map { maybeAppCurrency -> + maybeAppCurrency.getOrElse { AppCurrency.Default } + } + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, ) - - onAddToPortfolioBSVisibilityChange(isShow = false) - - addToPortfolioManager.removeAllChanges(userWalletId) - } } - private fun onAddToPortfolioBSVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = isShow, isWalletSelectorBSVisible = false) - } - } - - private fun onWalletSelectorVisibilityChange(isShow: Boolean) { - portfolioBSVisibilityModelFlow.update { - it.copy(isAddToPortfolioBSVisible = true, isWalletSelectorBSVisible = isShow) - } - } - - private fun updateTokensState(block: (MyPortfolioUM.Tokens) -> MyPortfolioUM) { - _state.update { stateToUpdate -> - val tokensState = stateToUpdate as? MyPortfolioUM.Tokens ?: return@update stateToUpdate - block(tokensState) - } + private fun createTokenActionsHandler(): TokenActionsHandler { + return tokenActionsHandlerFactory.create( + currentAppCurrency = Provider { currentAppCurrency.value }, + onHandleQuickAction = { handledAction -> + val currency = handledAction.cryptoCurrencyData.status.currency + analyticsEventHandler.send( + analyticsEventBuilder.quickActionClick( + actionUM = handledAction.action, + blockchainName = currency.network.name, + ), + ) + configureReceiveAddresses(handledAction) + }, + ) } private fun configureReceiveAddresses(quickAction: TokenActionsHandler.HandledQuickAction) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MyPortfolioUMFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MyPortfolioUMFactory.kt deleted file mode 100644 index 72ff24db98..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MyPortfolioUMFactory.kt +++ /dev/null @@ -1,153 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.markets.TokenMarketInfo -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.isMultiCurrency -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import kotlinx.collections.immutable.ImmutableList - -/** - * Factory for creating [MyPortfolioUM] - * - * @property onAddClick callback when user wants to add new token - * @property addToPortfolioBSContentUMFactory factory for creating add to portfolio bottom sheet content - * @property tokenActionsHandler token actions handler - * @property currentState current state provider - * @property updateTokens callback for updating tokens - * -[REDACTED_AUTHOR] - */ -internal class MyPortfolioUMFactory( - private val onAddClick: () -> Unit, - private val addToPortfolioBSContentUMFactory: AddToPortfolioBSContentUMFactory, - private val tokenActionsHandler: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) { - - fun create( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): MyPortfolioUM { - val addToPortfolioData = portfolioUIData.addToPortfolioData - - val isOnlyUnavailableNetworks = addToPortfolioData.availableNetworks?.isEmpty() == true - if (isOnlyUnavailableNetworks) return MyPortfolioUM.Unavailable - - val walletsWithCurrencies = if (addToPortfolioData.availableNetworks == null) { - portfolioData.walletsWithCurrencies - } else { - portfolioData.walletsWithCurrencies.filterAvailableNetworks(networks = addToPortfolioData.availableNetworks) - } - - val isPortfolioEmpty = walletsWithCurrencies.flatMap { it.value }.isEmpty() - if (isPortfolioEmpty) { - val hasMultiWallets = walletsWithCurrencies.filterKeys(UserWallet::isMultiCurrency).isNotEmpty() - - return if (hasMultiWallets) { - MyPortfolioUM.AddFirstToken( - addToPortfolioBSConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - ) - } else { - MyPortfolioUM.UnavailableForWallet - } - } - - return TokensPortfolioUMConverter( - appCurrency = portfolioData.appCurrency, - isBalanceHidden = portfolioData.isBalanceHidden, - addButtonState = walletsWithCurrencies.getAddButtonState( - availableNetworks = addToPortfolioData.availableNetworks, - ), - bsConfig = createAddToPortfolioBSConfig( - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - artworks = artworks, - ), - onAddClick = onAddClick, - quickActionsIntents = tokenActionsHandler, - currentState = currentState, - updateTokens = updateTokens, - ) - .convert(walletsWithCurrencies) - } - - private fun createAddToPortfolioBSConfig( - portfolioData: PortfolioData, - portfolioUIData: PortfolioUIData, - artworks: Map, - ): TangemBottomSheetConfig { - val selectedWallet = portfolioData.walletsWithCurrencies.keys - .firstOrNull { it.walletId == portfolioUIData.selectedWalletId } - ?: portfolioData.walletsWithCurrencies.keys.firstOrNull { it.isMultiCurrency } - - val availableNetworks = portfolioUIData.addToPortfolioData.availableNetworks.orEmpty() - - val alreadyAddedNetworks = portfolioData.walletsWithCurrencies - .filterAvailableNetworks(availableNetworks)[selectedWallet] - ?.filter { !it.status.currency.isCustom } - ?.map { it.status.currency.network.backendId } - ?.toSet() - - return addToPortfolioBSContentUMFactory.create( - currentState = currentState().addToPortfolioBSConfig, - portfolioData = portfolioData, - portfolioUIData = portfolioUIData, - selectedWallet = selectedWallet, - alreadyAddedNetworks = alreadyAddedNetworks, - artworks = artworks, - ) - } - - private fun Map>.getAddButtonState( - availableNetworks: Set?, - ): MyPortfolioUM.Tokens.AddButtonState { - if (availableNetworks == null) return MyPortfolioUM.Tokens.AddButtonState.Loading - - val networkIds = availableNetworks.map { it.networkId } - - val isAllAvailableNetworksAdded = this - // User can add currencies only in multi-currency wallets - .filterKeys(UserWallet::isMultiCurrency) - .mapValues { entry -> entry.value.map { it.status.currency.network.backendId } } - // Each wallets contains all available networks? - .all { it.value.containsAll(networkIds) } - - return if (isAllAvailableNetworksAdded) { - MyPortfolioUM.Tokens.AddButtonState.Unavailable - } else { - MyPortfolioUM.Tokens.AddButtonState.Available - } - } - - /** Filter map values by available networks [networks] */ - private fun Map>.filterAvailableNetworks( - networks: Set, - ): Map> { - return mapValues { entry -> entry.value.filterAvailableNetworks(networks) } - } - - /** Filter list of [CryptoCurrencyStatus] by available networks [networks] */ - private fun List.filterAvailableNetworks( - networks: Set, - ): List { - val networkIds = networks.map(TokenMarketInfo.Network::networkId) - - return mapNotNull { cryptoCurrencyData -> - cryptoCurrencyData.takeIf { networkIds.contains(it.status.currency.network.backendId) } - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioBSVisibilityModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioBSVisibilityModel.kt deleted file mode 100644 index dd3f13b3e7..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioBSVisibilityModel.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -/** - * Model for portfolio bottom sheet visibility - * - * @property isAddToPortfolioBSVisible visibility of add to portfolio bottom sheet - * @property isWalletSelectorBSVisible visibility of wallet selector bottom sheet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioBSVisibilityModel( - val isAddToPortfolioBSVisible: Boolean = false, - val isWalletSelectorBSVisible: Boolean = false, -) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioUIData.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioUIData.kt deleted file mode 100644 index 894b86dc98..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioUIData.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.domain.models.wallet.UserWalletId - -/** - * Portfolio UI data. Combined data from all UI flows that required to setup portfolio - * - * @property portfolioBSVisibilityModel portfolio bottom sheet visibility model - * @property selectedWalletId selected wallet id - * @property addToPortfolioData add to portfolio data - * @property isNeededColdWalletInteraction flag that indicates if user has missed derivations and has a cold wallet - * -[REDACTED_AUTHOR] - */ -internal data class PortfolioUIData( - val portfolioBSVisibilityModel: PortfolioBSVisibilityModel, - val selectedWalletId: UserWalletId?, - val addToPortfolioData: AddToPortfolioManager.AddToPortfolioData, - val isNeededColdWalletInteraction: Boolean, -) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/SelectNetworkUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/SelectNetworkUMConverter.kt deleted file mode 100644 index 5f9c6b282e..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/SelectNetworkUMConverter.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.domain.markets.TokenMarketParams -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [TokenMarketParams] to [SelectNetworkUM] - * - * @property networksWithToggle map of networks with toggles - * @property alreadyAddedNetworks already added networks - * @property onNetworkSwitchClick callback is called when network switch is clicked - * -[REDACTED_AUTHOR] - */ -internal class SelectNetworkUMConverter( - private val networksWithToggle: Map, - private val alreadyAddedNetworks: Set, - private val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) : Converter { - - override fun convert(value: TokenMarketParams): SelectNetworkUM { - return SelectNetworkUM( - tokenId = value.id.value, - iconUrl = value.imageUrl, - tokenName = value.name, - tokenCurrencySymbol = value.symbol, - networks = BlockchainRowUMConverter(alreadyAddedNetworks) - .convertList(networksWithToggle.toList()) - .toImmutableList(), - onNetworkSwitchClick = { um, isChecked -> onNetworkSwitchClick(um, isChecked) }, - ) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokensPortfolioUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokensPortfolioUMConverter.kt deleted file mode 100644 index 249415e21f..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokensPortfolioUMConverter.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.MyPortfolioUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter -import com.tangem.utils.extensions.isZero -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.toImmutableList - -/** - * Converter from [Map] of [UserWallet] and [CryptoCurrencyStatus] to [MyPortfolioUM.Tokens] - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class TokensPortfolioUMConverter( - private val appCurrency: AppCurrency, - private val isBalanceHidden: Boolean, - private val addButtonState: MyPortfolioUM.Tokens.AddButtonState, - private val bsConfig: TangemBottomSheetConfig, - private val onAddClick: () -> Unit, - private val quickActionsIntents: TokenActionsHandler, - private val currentState: Provider, - private val updateTokens: ((ImmutableList) -> ImmutableList) -> Unit, -) : Converter>, MyPortfolioUM.Tokens> { - - override fun convert(value: Map>): MyPortfolioUM.Tokens { - val currentTokensState = currentState() as? MyPortfolioUM.Tokens - - return MyPortfolioUM.Tokens( - tokens = value - .flatMap { entry -> entry.value } - .map { cryptoData -> - PortfolioTokenUMConverter( - appCurrency = appCurrency, - isBalanceHidden = isBalanceHidden, - onTokenItemClick = { toggleQuickActions(cryptoData) }, - tokenActionsHandler = quickActionsIntents, - ).convert(value = cryptoData) to cryptoData - } - .setQuickActionsVisibility(currentState = currentTokensState) - .toImmutableList(), - buttonState = addButtonState, - addToPortfolioBSConfig = bsConfig, - onAddClick = onAddClick, - ) - } - - private fun List>.setQuickActionsVisibility( - currentState: MyPortfolioUM.Tokens?, - ): List { - return when { - // if there is only one token and it has empty balance, show quick actions for it - currentState == null && this.size == 1 && isEmptyBalance(this.first().second) -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = true) - } - } - // if there is no previous state, hide quick actions for all tokens - currentState == null -> { - this.map { (token, _) -> - token.copy(isQuickActionsShown = false) - } - } - else -> { - val previousList = currentState.tokens - - // otherwise, keep previous state - this.map { (token, _) -> - token.copy( - isQuickActionsShown = previousList - .firstOrNull { it.matchWith(token) } - ?.isQuickActionsShown == true, - ) - } - } - } - } - - private fun isEmptyBalance(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return cryptoData.status.value.amount?.isZero() == true - } - - private fun toggleQuickActions(cryptoData: PortfolioData.CryptoCurrencyData) { - updateTokens { tokenList -> - tokenList.map { portfolioTokenUM -> - portfolioTokenUM.copy( - isQuickActionsShown = if (portfolioTokenUM.matchWith(cryptoData)) { - !portfolioTokenUM.isQuickActionsShown - } else { - false - }, - ) - }.toImmutableList() - } - } - - private fun PortfolioTokenUM.matchWith(token: PortfolioTokenUM): Boolean { - return this.walletId == token.walletId && this.tokenItemState.id == token.tokenItemState.id - } - - private fun PortfolioTokenUM.matchWith(cryptoData: PortfolioData.CryptoCurrencyData): Boolean { - return this.walletId == cryptoData.userWallet.walletId && - this.tokenItemState.id == cryptoData.status.currency.id.value - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/AddToPortfolioBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/AddToPortfolioBottomSheet.kt deleted file mode 100644 index 16993ea52d..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/AddToPortfolioBottomSheet.kt +++ /dev/null @@ -1,383 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.vectorResource -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.util.fastForEachIndexed -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.block.information.InformationBlock -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.buttons.common.TangemButton -import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition -import com.tangem.core.ui.components.buttons.common.TangemButtonSize -import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults -import com.tangem.core.ui.components.currency.icon.CoinIcon -import com.tangem.core.ui.components.rows.ArrowRow -import com.tangem.core.ui.components.rows.BlockchainRow -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.haptic.TangemHapticEffect -import com.tangem.core.ui.res.LocalHapticManager -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.features.feed.impl.R -import kotlinx.coroutines.delay - -@Composable -internal fun AddToPortfolioBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - titleText = resourceReference(R.string.common_add_to_portfolio), - ) { contentUM -> - Content( - modifier = Modifier.fillMaxWidth(), - state = contentUM, - ) - - WalletSelectorBottomSheet(contentUM.walletSelectorConfig) - } -} - -@Composable -private fun Content(state: AddToPortfolioBSContentUM, modifier: Modifier = Modifier) { - var continueButtonAreaHeight by remember { mutableIntStateOf(0) } - val density = LocalDensity.current - val scrollState = rememberScrollState() - - Box(modifier = modifier) { - Column( - modifier = Modifier - .verticalScroll(state = scrollState) - .padding(horizontal = TangemTheme.dimens.spacing16), - ) { - if (state.isWalletBlockVisible) { - UserWalletItem( - state = state.selectedWallet, - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) - SpacerH12() - } - - NetworkSelection( - modifier = Modifier.fillMaxWidth(), - state = state.selectNetworkUM, - ) - - SpacerH12() - - AnimatedVisibility( - visible = state.isScanCardNotificationVisible, - modifier = Modifier.fillMaxWidth(), - ) { - Column { - ScanWalletWarning(modifier = Modifier.fillMaxWidth()) - SpacerH12() - } - - // Scroll to the bottom when the notification appears and the scroll is at the bottom - LaunchedEffect(Unit) { - if (scrollState.canScrollForward.not()) { - delay(timeMillis = 500) - scrollState.animateScrollTo(scrollState.maxValue) - } - } - } - - SpacerH(with(density) { continueButtonAreaHeight.toDp() }) - } - - AnimatedVisibility( - visible = scrollState.canScrollForward, - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier.align(Alignment.BottomCenter), - ) { - BottomFade(Modifier.align(Alignment.BottomCenter)) - } - - ContinueButton( - modifier = Modifier - .align(Alignment.BottomCenter) - .onGloballyPositioned { - continueButtonAreaHeight = it.size.height - }, - enabled = state.isContinueButtonEnabled, - isTangemIconVisible = state.isScanCardNotificationVisible, - onClick = state.onContinueButtonClick, - ) - } -} - -@Composable -private fun ContinueButton( - enabled: Boolean, - isTangemIconVisible: Boolean, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TangemButton( - enabled = enabled, - modifier = modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ) - .navigationBarsPadding() - .fillMaxWidth(), - text = stringResourceSafe(R.string.common_continue), - icon = if (enabled && isTangemIconVisible) { - TangemButtonIconPosition.End(R.drawable.ic_tangem_24) - } else { - TangemButtonIconPosition.None - }, - showProgress = false, - size = TangemButtonSize.Default, - colors = TangemButtonsDefaults.primaryButtonColors, - textStyle = TangemTheme.typography.subtitle1, - onClick = onClick, - animateContentChange = true, - ) -} - -@Suppress("LongMethod") -@Composable -private fun NetworkSelection(state: SelectNetworkUM, modifier: Modifier = Modifier) { - val hapticManager = LocalHapticManager.current - - InformationBlock( - modifier = modifier, - title = { - Text( - text = stringResourceSafe(R.string.markets_select_network), - style = TangemTheme.typography.subtitle2, - color = TangemTheme.colors.text.tertiary, - ) - }, - ) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = TangemTheme.dimens.spacing14), - verticalAlignment = Alignment.CenterVertically, - ) { - CoinIcon( - modifier = Modifier.size(TangemTheme.dimens.size36), - url = state.iconUrl, - alpha = 1f, - colorFilter = null, - fallbackResId = R.drawable.ic_custom_token_44, - ) - SpacerW12() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .weight(1f, fill = false) - .alignByBaseline(), - text = state.tokenName, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) - SpacerW6() - Text( - modifier = Modifier - .align(Alignment.CenterVertically) - .alignByBaseline(), - text = state.tokenCurrencySymbol, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - overflow = TextOverflow.Visible, - maxLines = 1, - ) - } - - state.networks.fastForEachIndexed { index, network -> - ArrowRow( - isLastItem = index == state.networks.lastIndex, - content = { - BlockchainRow( - modifier = Modifier.padding(end = TangemTheme.dimens.spacing4), - model = network, - action = { - TangemSwitch( - checked = network.isSelected, - checkedColor = if (network.isEnabled) { - TangemTheme.colors.control.checked - } else { - TangemTheme.colors.icon.inactive - }, - onCheckedChange = { checked -> - if (checked) { - hapticManager.perform(TangemHapticEffect.View.ToggleOn) - } else { - hapticManager.perform(TangemHapticEffect.View.ToggleOff) - } - - state.onNetworkSwitchClick(network, checked) - }, - enabled = network.isEnabled, - ) - }, - ) - }, - ) - } - } - } -} - -@Composable -private fun ScanWalletWarning(modifier: Modifier = Modifier) { - Row( - modifier = modifier - .background( - color = TangemTheme.colors.button.disabled, - shape = TangemTheme.shapes.roundedCornersXMedium, - ) - .padding(TangemTheme.dimens.spacing12), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing10), - ) { - Icon( - modifier = Modifier.requiredSize(TangemTheme.dimens.size20), - imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), - tint = TangemTheme.colors.icon.primary1, - contentDescription = null, - ) - Text( - text = stringResourceSafe(R.string.markets_generate_addresses_notification), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun Preview( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - content = content, - onDismissRequest = {}, - ), - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContent( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -@Composable -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun PreviewContentRtl( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview(rtl = true) { - Content( - modifier = Modifier - .background(TangemTheme.colors.background.tertiary) - .fillMaxWidth(), - state = content, - ) - } -} - -// For on device testing -@Composable -@Preview -private fun PreviewContentTestOnDevice( - @PreviewParameter(PreviewAddToPortfolioBSContentProvider::class) content: AddToPortfolioBSContentUM, -) { - TangemThemePreview( - alwaysShowBottomSheets = false, - ) { - var isShow by remember { mutableStateOf(false) } - - var contentState by remember { - mutableStateOf(content) - } - - LaunchedEffect(Unit) { - contentState = content.copy( - onContinueButtonClick = { - contentState = contentState.copy( - isScanCardNotificationVisible = !contentState.isScanCardNotificationVisible, - ) - }, - isContinueButtonEnabled = true, - selectedWallet = content.selectedWallet.copy( - onClick = { - contentState = contentState.copy( - isContinueButtonEnabled = !contentState.isContinueButtonEnabled, - ) - }, - ), - ) - } - - AddToPortfolioBottomSheet( - config = TangemBottomSheetConfig( - isShown = isShow, - content = contentState, - onDismissRequest = { isShow = false }, - ), - ) - - Button( - onClick = { isShow = !isShow }, - ) { - Text(text = "Toggle") - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/MyPortfolio.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/MyPortfolio.kt index 8d6911c448..c7c885eca6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/MyPortfolio.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/MyPortfolio.kt @@ -70,11 +70,6 @@ internal fun MyPortfolio(state: MyPortfolioUM, modifier: Modifier = Modifier) { is MyPortfolioUM.Content -> PortfolioList(state = state) } } - - val bsConfig = state.addToPortfolioBSConfig - if (bsConfig != null) { - AddToPortfolioBottomSheet(config = bsConfig) - } } @Composable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/TokenActionsBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/TokenActionsBottomSheet.kt deleted file mode 100644 index 7b4a266b9e..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/TokenActionsBottomSheet.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheetTitle -import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM -import kotlinx.collections.immutable.toImmutableList - -@Composable -fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - title = { content -> - TangemBottomSheetTitle(content.title) - }, - containerColor = TangemTheme.colors.background.tertiary, - content = { Content(it) }, - ) -} - -@Composable -private fun Content(content: TokenActionsBSContentUM) { - Column( - modifier = Modifier - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing16, - ), - ) { - content.actions.forEachIndexed { index, action -> - Box( - modifier = Modifier - .roundedShapeItemDecoration( - currentIndex = index, - lastIndex = content.actions.lastIndex, - addDefaultPadding = false, - ) - .background(TangemTheme.colors.background.action), - ) { - SimpleSettingsRow( - title = action.text.resolveReference(), - icon = action.iconRes, - redesign = true, - onItemsClick = { content.onActionClick(action) }, - ) - } - } - } -} - -@Preview(widthDp = 360, heightDp = 640) -@Preview(widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview( - alwaysShowBottomSheets = true, - ) { - Box(Modifier.background(TangemTheme.colors.background.secondary)) { - TokenActionsBottomSheet( - TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = TokenActionsBSContentUM( - title = "Wallet 1", - actions = TokenActionsBSContentUM.Action.entries.toImmutableList(), - onActionClick = {}, - ), - ), - ) - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/WalletSelectorBottomSheet.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/WalletSelectorBottomSheet.kt deleted file mode 100644 index 530786cf30..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/WalletSelectorBottomSheet.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui - -import android.content.res.Configuration -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.tooling.preview.Preview -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.SpacerH -import com.tangem.core.ui.components.appbar.TangemTopAppBar -import com.tangem.core.ui.components.appbar.TangemTopAppBarHeight -import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM -import com.tangem.core.ui.components.block.BlockCard -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview.PreviewAddToPortfolioBSContentProvider -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.WalletSelectorBSContentUM -import com.tangem.features.feed.impl.R -import kotlinx.collections.immutable.persistentListOf - -@Composable -internal fun WalletSelectorBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet( - config = config, - containerColor = TangemTheme.colors.background.tertiary, - addBottomInsets = false, - title = { content -> - TangemTopAppBar( - title = resourceReference(R.string.common_choose_wallet), - titleAlignment = Alignment.CenterHorizontally, - startButton = TopAppBarButtonUM.Back(content.onBack), - height = TangemTopAppBarHeight.BOTTOM_SHEET, - ) - }, - ) { content -> - Content( - modifier = Modifier - .fillMaxSize() - .padding( - horizontal = TangemTheme.dimens.spacing16, - vertical = TangemTheme.dimens.spacing8, - ), - state = content, - ) - } -} - -@Composable -private fun Content(state: WalletSelectorBSContentUM, modifier: Modifier = Modifier) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - Column( - modifier = modifier - .verticalScroll(rememberScrollState()), - ) { - BlockCard( - modifier = Modifier.fillMaxSize(), - colors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - ) { - state.userWallets.forEach { state -> - key(state.id) { - UserWalletItem( - modifier = Modifier.fillMaxWidth(), - blockColors = TangemBlockCardColors.copy( - containerColor = TangemTheme.colors.background.action, - disabledContainerColor = TangemTheme.colors.background.action, - ), - state = state, - ) - } - } - } - SpacerH(bottomBarHeight) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun Preview() { - TangemThemePreview { - WalletSelectorBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = {}, - content = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ), - ) - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun PreviewContent() { - TangemThemePreview { - Content( - state = WalletSelectorBSContentUM( - userWallets = persistentListOf( - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.Checkmark, - ), - PreviewAddToPortfolioBSContentProvider().userWallet.copy( - endIcon = UserWalletItemUM.EndIcon.None, - ), - ), - onBack = {}, - ), - ) - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt deleted file mode 100644 index 4de442ca27..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewAddToPortfolioBSContentProvider.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui.preview - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.stringReference -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.AddToPortfolioBSContentUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.SelectNetworkUM -import com.tangem.features.feed.impl.R -import kotlinx.collections.immutable.persistentListOf - -internal class PreviewAddToPortfolioBSContentProvider : PreviewParameterProvider { - - private val blockchainRow = BlockchainRowUM( - id = "1", - name = "Etherium 3", - type = "TEST", - iconResId = R.drawable.ic_eth_16, - isMainNetwork = false, - isSelected = false, - ) - - val userWallet = UserWalletItemUM( - id = "1", - name = stringReference("Wallet 1"), - information = UserWalletItemUM.Information.Loaded(TextReference.Str("3 cards")), - balance = UserWalletItemUM.Balance.Loading, - isEnabled = true, - endIcon = UserWalletItemUM.EndIcon.Arrow, - onClick = {}, - ) - - override val values: Sequence - get() = sequenceOf( - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ), - blockchainRow, - blockchainRow, - ), - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = true, - isContinueButtonEnabled = true, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - AddToPortfolioBSContentUM( - selectedWallet = userWallet, - selectNetworkUM = SelectNetworkUM( - tokenId = "etherium", - tokenName = "Etherium Etherium Etherium Etherium", - tokenCurrencySymbol = "ETH", - networks = persistentListOf( - blockchainRow.copy( - type = "MAIN", - isMainNetwork = true, - isSelected = true, - ).copy(name = "Etherium Etherium Etherium Etherium"), - *Array(25) { blockchainRow }, - ), - - onNetworkSwitchClick = { _, _ -> }, - iconUrl = null, - ), - isScanCardNotificationVisible = true, - isWalletBlockVisible = false, - isContinueButtonEnabled = false, - onContinueButtonClick = {}, - walletSelectorConfig = TangemBottomSheetConfig.Empty, - ), - ) -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt index 8c6bf7a7a1..f84d983885 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/preview/PreviewMyPortfolioUMProvider.kt @@ -3,7 +3,6 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.ui.pre import androidx.compose.ui.tooling.preview.PreviewParameterProvider import com.tangem.common.ui.account.AccountIconPreviewData import com.tangem.common.ui.account.AccountTitleUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState @@ -22,25 +21,19 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider Unit, - val walletSelectorConfig: TangemBottomSheetConfig, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/MyPortfolioUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/MyPortfolioUM.kt index d567f3095f..b9d4593c30 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/MyPortfolioUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/MyPortfolioUM.kt @@ -1,16 +1,12 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state import androidx.compose.runtime.Immutable -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import kotlinx.collections.immutable.ImmutableList @Immutable internal sealed class MyPortfolioUM { - abstract val addToPortfolioBSConfig: TangemBottomSheetConfig? - data class Tokens( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, val tokens: ImmutableList, val buttonState: AddButtonState, val onAddClick: () -> Unit, @@ -27,25 +23,15 @@ internal sealed class MyPortfolioUM { val items: ImmutableList, val buttonState: Tokens.AddButtonState, val onAddClick: () -> Unit, - ) : MyPortfolioUM() { - - override val addToPortfolioBSConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty - } + ) : MyPortfolioUM() data class AddFirstToken( - override val addToPortfolioBSConfig: TangemBottomSheetConfig, val onAddClick: () -> Unit, ) : MyPortfolioUM() - data object Loading : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } + data object Loading : MyPortfolioUM() - data object Unavailable : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } + data object Unavailable : MyPortfolioUM() - data object UnavailableForWallet : MyPortfolioUM() { - override val addToPortfolioBSConfig: TangemBottomSheetConfig? = null - } + data object UnavailableForWallet : MyPortfolioUM() } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/SelectNetworkUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/SelectNetworkUM.kt deleted file mode 100644 index 4ee09913a4..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/SelectNetworkUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state - -import com.tangem.core.ui.components.rows.model.BlockchainRowUM -import kotlinx.collections.immutable.ImmutableList - -internal data class SelectNetworkUM( - val tokenId: String, - val iconUrl: String?, - val tokenName: String, - val tokenCurrencySymbol: String, - val networks: ImmutableList, - val onNetworkSwitchClick: (BlockchainRowUM, Boolean) -> Unit, -) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt deleted file mode 100644 index e93a65ddcd..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/WalletSelectorBSContentUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state - -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import kotlinx.collections.immutable.ImmutableList - -internal data class WalletSelectorBSContentUM( - val userWallets: ImmutableList, - val onBack: () -> Unit, -) : TangemBottomSheetConfigContent \ No newline at end of file From dcaae36f6057138aaa74f3c19cdc89470714d044 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 12:55:15 +0400 Subject: [PATCH 55/97] Updated on 2026-08-14 --- .../AccountListCryptoCurrenciesProducer.kt | 42 +++++++------------ .../producer/DefaultFlowProducerTools.kt | 11 +++-- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt index 795c6dbc37..3820a2e528 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/producer/AccountListCryptoCurrenciesProducer.kt @@ -2,12 +2,11 @@ package com.tangem.data.account.producer import arrow.core.Option import arrow.core.some -import com.tangem.data.account.store.AccountsResponseStoreFactory -import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.core.flow.FlowProducerTools -import com.tangem.domain.models.account.DerivationIndex +import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer @@ -15,31 +14,29 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map /** * Implementation of [MultiWalletCryptoCurrenciesProducer] that produces crypto currencies of all accounts * - * @property params params - * @property userWalletsListRepository repository for getting user wallets - * @property accountsResponseStoreFactory factory to create store with accounts response - * @property responseCryptoCurrenciesFactory factory for creating [CryptoCurrency] from `UserTokensResponse` - * @property dispatchers dispatchers + * @property params params + * @property userWalletsListRepository repository for getting user wallets + * @property dispatchers dispatchers * [REDACTED_AUTHOR] */ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor( @Assisted val params: MultiWalletCryptoCurrenciesProducer.Params, + private val singleAccountListSupplier: SingleAccountListSupplier, private val userWalletsListRepository: UserWalletsListRepository, - private val accountsResponseStoreFactory: AccountsResponseStoreFactory, - private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, override val flowProducerTools: FlowProducerTools, private val dispatchers: CoroutineDispatcherProvider, ) : MultiWalletCryptoCurrenciesProducer { override val fallback: Option> = emptySet().some() - @Suppress("NullableToStringCall") override fun produce(): Flow> { val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) @@ -47,23 +44,12 @@ internal class AccountListCryptoCurrenciesProducer @AssistedInject constructor( error("${this::class.simpleName ?: this::class.toString()} supports only multi-currency wallet") } - return accountsResponseStoreFactory.create(userWalletId = userWallet.walletId).data - .distinctUntilChanged() - .map { response -> - if (response == null) return@map emptySet() - - response.accounts.flatMapTo(hashSetOf()) { accountDTO -> - val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() - ?: return@map emptySet() - - responseCryptoCurrenciesFactory.createCurrencies( - tokens = accountDTO.tokens.orEmpty(), - userWallet = userWallet, - accountIndex = accountIndex, - ) - } + return singleAccountListSupplier.invoke(params.userWalletId) + .map { accountList -> + accountList.accounts + .filterIsInstance() + .flatMapTo(hashSetOf(), Account.CryptoPortfolio::cryptoCurrencies) } - .onEmpty { emit(emptySet()) } .flowOn(dispatchers.default) } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt index 358e3e4ce2..dfe0ea8bf4 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultFlowProducerTools.kt @@ -16,7 +16,7 @@ import javax.inject.Inject import kotlin.coroutines.CoroutineContext class DefaultFlowProducerAppScope @Inject constructor( - private val dispatchers: CoroutineDispatcherProvider, + dispatchers: CoroutineDispatcherProvider, private val analyticsExceptionHandler: AnalyticsExceptionHandler, ) : FlowProducerScope { @@ -76,10 +76,13 @@ class DefaultFlowProducerTools @Inject constructor( .shareIn( scope = scope, replay = 1, - // params control flow cleanup + // stopTimeoutMillis = 0: upstream collection stops immediately when the last subscriber disappears. + // replayExpirationMillis = 0: replay cache is cleared immediately after upstream stops. + // This ensures that when there are no subscribers, the first subscriber always triggers a fresh + // upstream collection instead of receiving a stale replay. started = SharingStarted.WhileSubscribed( - stopTimeoutMillis = 5_000, - replayExpirationMillis = 30_000, + stopTimeoutMillis = 0, + replayExpirationMillis = 0, ), ) } From c6578aa65247deb25b36c437931a125fb10d325e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 14:32:14 +0400 Subject: [PATCH 56/97] Updated on 2026-08-14 --- .../tap/di/domain/ManageTokensDomainModule.kt | 43 --- .../tap/di/domain/MarketsDomainModule.kt | 36 --- .../tap/di/domain/TokensDomainModule.kt | 50 ---- .../DefaultCustomTokensRepository.kt | 41 --- .../managetokens/di/ManageTokensDataModule.kt | 5 - .../repository/DefaultCurrenciesRepository.kt | 148 ----------- ...emoveCustomManagedCryptoCurrencyUseCase.kt | 19 -- .../managetokens/SaveManagedTokensUseCase.kt | 174 ------------ .../repository/CustomTokensRepository.kt | 3 - .../domain/markets/SaveMarketTokensUseCase.kt | 145 ---------- .../tokens/AddCryptoCurrenciesUseCase.kt | 251 ------------------ .../tokens/ApplyTokenListSortingUseCase.kt | 115 -------- .../domain/tokens/RemoveCurrencyUseCase.kt | 60 ----- .../tokens/repository/CurrenciesRepository.kt | 64 ----- .../ApplyTokenListSortingUseCaseTest.kt | 207 --------------- .../repository/MockCurrenciesRepository.kt | 152 ----------- .../list/CustomTokenFormUseCasesFacade.kt | 12 +- .../utils/list/ManageTokensUseCasesFacade.kt | 20 +- .../model/OnrampAddToPortfolioModel.kt | 36 +-- .../referral/domain/ReferralInteractor.kt | 4 +- .../referral/domain/ReferralInteractorImpl.kt | 73 ++--- .../domain/di/ReferralDomainModule.kt | 9 - .../feature/referral/model/ReferralModel.kt | 50 ++-- .../v2/send/confirm/model/SendConfirmModel.kt | 24 +- .../tokendetails/model/TokenDetailsModel.kt | 26 +- .../factory/express/ExchangeStatusFactory.kt | 63 ++--- .../TokenDetailsExchangeStatusFactory.kt | 63 ++--- 27 files changed, 111 insertions(+), 1782 deletions(-) delete mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt delete mode 100644 domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt delete mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt delete mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt delete mode 100644 domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt index b1d7b0a206..b527533fc1 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/ManageTokensDomainModule.kt @@ -3,20 +3,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.managetokens.* import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -57,40 +48,6 @@ internal object ManageTokensDomainModule { return CheckIsCurrencyNotAddedUseCase(customTokensRepository) } - @Provides - @Singleton - fun provideRemoveCustomManagedCryptoCurrencyUseCase( - customTokensRepository: CustomTokensRepository, - ): RemoveCustomManagedCryptoCurrencyUseCase { - return RemoveCustomManagedCryptoCurrencyUseCase(customTokensRepository) - } - - @Provides - @Singleton - fun provideSaveManagedTokensUseCase( - customTokensRepository: CustomTokensRepository, - walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, - derivationsRepository: DerivationsRepository, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - stakingIdFactory: StakingIdFactory, - dispatchers: CoroutineDispatcherProvider, - ): SaveManagedTokensUseCase { - return SaveManagedTokensUseCase( - customTokensRepository = customTokensRepository, - walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, - derivationsRepository = derivationsRepository, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiStakingBalanceFetcher = multiStakingBalanceFetcher, - stakingIdFactory = stakingIdFactory, - parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), - ) - } - @Provides @Singleton fun provideGetSupportedNetworksUseCase( diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index d96861be6a..3be28c02e2 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -4,22 +4,12 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.domain.promo.PromoRepository -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import javax.inject.Singleton @Module @@ -66,32 +56,6 @@ object MarketsDomainModule { return GetCurrencyQuotesUseCase(singleQuoteStatusSupplier = singleQuoteStatusSupplier) } - @Provides - @Singleton - fun provideSaveMarketTokensUseCase( - derivationsRepository: DerivationsRepository, - marketsTokenRepository: MarketsTokenRepository, - walletManagersFacade: WalletManagersFacade, - currenciesRepository: CurrenciesRepository, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - stakingIdFactory: StakingIdFactory, - dispatchers: CoroutineDispatcherProvider, - ): SaveMarketTokensUseCase { - return SaveMarketTokensUseCase( - derivationsRepository = derivationsRepository, - marketsTokenRepository = marketsTokenRepository, - walletManagersFacade = walletManagersFacade, - currenciesRepository = currenciesRepository, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - multiStakingBalanceFetcher = multiStakingBalanceFetcher, - stakingIdFactory = stakingIdFactory, - parallelUpdatingScope = CoroutineScope(SupervisorJob() + dispatchers.default), - ) - } - @Provides @Singleton fun provideGetTokenMarketCryptoCurrency( diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index ecba7042ef..56bf1c96ba 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -40,28 +40,6 @@ import javax.inject.Singleton @Suppress("TooManyFunctions", "LargeClass") internal object TokensDomainModule { - @Provides - @Singleton - fun provideAddCryptoCurrenciesUseCase( - currenciesRepository: CurrenciesRepository, - walletManagersFacade: WalletManagersFacade, - multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - singleStakingBalanceFetcher: SingleStakingBalanceFetcher, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - stakingIdFactory: StakingIdFactory, - ): AddCryptoCurrenciesUseCase { - return AddCryptoCurrenciesUseCase( - currenciesRepository = currenciesRepository, - walletManagersFacade = walletManagersFacade, - multiNetworkStatusFetcher = multiNetworkStatusFetcher, - multiQuoteStatusFetcher = multiQuoteStatusFetcher, - singleStakingBalanceFetcher = singleStakingBalanceFetcher, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, - ) - } - @Provides @Singleton fun provideFetchPendingTransactionsUseCase( @@ -88,20 +66,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideRemoveCurrencyUseCase( - currenciesRepository: CurrenciesRepository, - walletManagersFacade: WalletManagersFacade, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - ): RemoveCurrencyUseCase { - return RemoveCurrencyUseCase( - currenciesRepository = currenciesRepository, - walletManagersFacade = walletManagersFacade, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - ) - } - @Provides @Singleton fun provideGetCurrencyUseCase( @@ -177,20 +141,6 @@ internal object TokensDomainModule { return GetCryptoCurrencyUseCase(currenciesRepository, multiWalletCryptoCurrenciesSupplier) } - @Provides - @Singleton - fun provideApplyTokenListSortingUseCase( - currenciesRepository: CurrenciesRepository, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - dispatchers: CoroutineDispatcherProvider, - ): ApplyTokenListSortingUseCase { - return ApplyTokenListSortingUseCase( - currenciesRepository = currenciesRepository, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - dispatchers = dispatchers, - ) - } - @Provides @Singleton fun provideGetCryptoCurrencyActionsUseCase( diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt index 7725874819..492194a851 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultCustomTokensRepository.kt @@ -6,8 +6,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.currency.CryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.network.NetworkFactory import com.tangem.data.managetokens.utils.TokenAddressesConverter import com.tangem.datasource.api.common.response.getOrThrow @@ -26,7 +24,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.withContext @@ -35,10 +32,8 @@ internal class DefaultCustomTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsListRepository: UserWalletsListRepository, private val userTokensResponseStore: UserTokensResponseStore, - private val walletManagersFacade: WalletManagersFacade, private val excludedBlockchains: ExcludedBlockchains, private val dispatchers: CoroutineDispatcherProvider, - private val userTokensSaver: UserTokensSaver, private val networkFactory: NetworkFactory, ) : CustomTokensRepository { @@ -48,7 +43,6 @@ internal class DefaultCustomTokensRepository( ) private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) - private val userTokensResponseFactory = UserTokensResponseFactory() private val tokenAddressConverter = TokenAddressesConverter() override suspend fun validateContractAddress(contractAddress: String, networkId: Network.ID): Boolean = @@ -213,41 +207,6 @@ internal class DefaultCustomTokensRepository( ) } - @Deprecated("Use ManageCryptoCurrenciesUseCase") - override suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) = - withContext(dispatchers.io) { - val cryptoCurrency = when (currency) { - is ManagedCryptoCurrency.Custom.Coin -> createCoin( - userWalletId = userWalletId, - networkId = currency.network.id, - derivationPath = currency.network.derivationPath, - ) - is ManagedCryptoCurrency.Custom.Token -> cryptoCurrencyFactory.createToken( - network = currency.network, - rawId = currency.currencyId.rawCurrencyId, - name = currency.name, - symbol = currency.symbol, - decimals = currency.decimals, - contractAddress = currency.contractAddress, - ) - } - val storedCurrencies = userTokensResponseStore.getSyncOrNull(userWalletId) - - requireNotNull(storedCurrencies) { - "User tokens not found for user wallet [$userWalletId] while removing currency" - } - - val token = userTokensResponseFactory.createResponseToken(currency = cryptoCurrency, accountId = null) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = storedCurrencies.copy(tokens = storedCurrencies.tokens.filterNot { it == token }), - ) - when (cryptoCurrency) { - is CryptoCurrency.Coin -> walletManagersFacade.remove(userWalletId, setOf(cryptoCurrency.network)) - is CryptoCurrency.Token -> walletManagersFacade.removeTokens(userWalletId, setOf(cryptoCurrency)) - } - } - override suspend fun convertToCryptoCurrency( userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom, diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index ea538bf3d2..bd5efcfcc6 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -15,7 +15,6 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -65,20 +64,16 @@ internal object ManageTokensDataModule { tangemTechApi: TangemTechApi, userWalletsListRepository: UserWalletsListRepository, userTokensResponseStore: UserTokensResponseStore, - walletManagersFacade: WalletManagersFacade, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, - userTokensSaver: UserTokensSaver, networkFactory: NetworkFactory, ): CustomTokensRepository { return DefaultCustomTokensRepository( tangemTechApi = tangemTechApi, userWalletsListRepository = userWalletsListRepository, userTokensResponseStore = userTokensResponseStore, - walletManagersFacade = walletManagersFacade, excludedBlockchains = excludedBlockchains, dispatchers = dispatchers, - userTokensSaver = userTokensSaver, networkFactory = networkFactory, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index b2ec4a4968..3e5eadeda3 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -34,7 +34,6 @@ import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -66,134 +65,6 @@ internal class DefaultCurrenciesRepository( userTokensSaver = userTokensSaver, ) - override suspend fun saveTokens( - userWalletId: UserWalletId, - currencies: List, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ) = withContext(dispatchers.io) { - ensureIsCorrectUserWallet(userWalletId, isMultiCurrencyWalletExpected = true) - - val response = userTokensResponseFactory.createUserTokensResponse( - currencies = currencies, - isGroupedByNetwork = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - userTokensSaver.storeAndPush(userWalletId, response) - } - - override suspend fun addCurrenciesCache( - userWalletId: UserWalletId, - currencies: List, - ): List = withContext(dispatchers.io) { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, - ) - - val currenciesToAdd = filterAlreadyAddedCurrencies( - savedCurrencies = savedCurrencies.tokens, - currenciesToAdd = populateCurrenciesWithMissedCoins(currencies = currencies), - ) - - val updatedResponse = savedCurrencies.copy( - tokens = savedCurrencies.tokens + currenciesToAdd.map(userTokensResponseFactory::createResponseToken), - ) - - userTokensSaver.store( - userWalletId = userWalletId, - response = updatedResponse, - ) - - fetchExpressAssetsByNetworkIds( - userWallet = userWalletsListRepository.getSyncStrict(id = userWalletId), - userTokens = updatedResponse, - ) - - currenciesToAdd - } - - private fun filterAlreadyAddedCurrencies( - savedCurrencies: List, - currenciesToAdd: List, - ): List { - return currenciesToAdd.filter { currency -> - val networkId = currency.network.toBlockchain().toNetworkId() - val contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress - - savedCurrencies.none { token -> - token.contractAddress == contractAddress && - token.networkId == networkId && - token.derivationPath == currency.network.derivationPath.value - } - } - } - - private fun populateCurrenciesWithMissedCoins(currencies: List): List { - val currenciesSequence = currencies.asSequence() - - val networksWithTokens = currenciesSequence - .filterIsInstance() - .map { it.network } - .distinct() - - val networksWithCoins = currenciesSequence - .filterIsInstance() - .map { it.network } - .distinct() - - val networksNeedingCoins = (networksWithTokens - networksWithCoins.toSet()).toMutableList() - - if (networksNeedingCoins.isEmpty()) return currencies - - return buildList { - currencies.forEach { currency -> - if (currency is CryptoCurrency.Token && currency.network in networksNeedingCoins) { - val coin = cryptoCurrencyFactory.createCoin(currency.network) - add(coin) - - networksNeedingCoins.remove(currency.network) - } - - add(currency) - } - } - } - - override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) = - withContext(dispatchers.io) { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform remove currency action" }, - ) - - val token = userTokensResponseFactory.createResponseToken(currency) - val updatedResponse = - savedCurrencies.copy(tokens = savedCurrencies.tokens.filterNot { it == token }) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = updatedResponse, - ) - } - - override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) { - return withContext(dispatchers.io) { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform remove currencies action" }, - ) - - val tokens = currencies.map(userTokensResponseFactory::createResponseToken) - val updatedResponse = savedCurrencies.copy( - tokens = savedCurrencies.tokens.filterNot(tokens::contains), - ) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = updatedResponse, - ) - } - } - override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) @@ -577,19 +448,6 @@ internal class DefaultCurrenciesRepository( return blockchain?.isNetworkFeeZero() == true } - override suspend fun syncTokens(userWalletId: UserWalletId) { - runSuspendCatching { - val savedCurrencies = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { "Saved tokens empty. Can not perform add currencies action" }, - ) - userTokensSaver.storeAndPush( - userWalletId = userWalletId, - response = savedCurrencies, - ) - } - } - override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? { return (userWalletsListRepository.getSyncStrict(userWalletId) as? UserWallet.Cold)?.cardTypesResolver } @@ -712,12 +570,6 @@ internal class DefaultCurrenciesRepository( accountId = null, ) - private fun ensureIsCorrectUserWallet(userWalletId: UserWalletId, isMultiCurrencyWalletExpected: Boolean) { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - - ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected) - } - private fun ensureIsCorrectUserWallet(userWallet: UserWallet, isMultiCurrencyWalletExpected: Boolean) { val userWalletId = userWallet.walletId diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt deleted file mode 100644 index b82f1cf58a..0000000000 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/RemoveCustomManagedCryptoCurrencyUseCase.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.domain.managetokens - -import arrow.core.Either -import com.tangem.domain.managetokens.model.ManagedCryptoCurrency -import com.tangem.domain.managetokens.repository.CustomTokensRepository -import com.tangem.domain.models.wallet.UserWalletId - -@Deprecated("Use ManageCryptoCurrenciesUseCase") -class RemoveCustomManagedCryptoCurrencyUseCase(private val repository: CustomTokensRepository) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - customCurrency: ManagedCryptoCurrency.Custom, - ): Either { - return Either.catch { - repository.removeCurrency(userWalletId, customCurrency) - } - } -} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt deleted file mode 100644 index 3fd2900bd7..0000000000 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/SaveManagedTokensUseCase.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.tangem.domain.managetokens - -import arrow.core.Either -import arrow.core.flatten -import com.tangem.domain.managetokens.model.ManagedCryptoCurrency -import com.tangem.domain.managetokens.repository.CustomTokensRepository -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -@Deprecated("Use ManageCryptoCurrenciesUseCase") -@Suppress("LongParameterList") -class SaveManagedTokensUseCase( - private val customTokensRepository: CustomTokensRepository, - private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, - private val derivationsRepository: DerivationsRepository, - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - private val stakingIdFactory: StakingIdFactory, - private val parallelUpdatingScope: CoroutineScope, -) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - currenciesToAdd: Map>, - currenciesToRemove: Map>, - ): Either = Either.catch { - if (currenciesToRemove.isNotEmpty()) { - val removingCurrencies = currenciesToRemove.mapToCryptoCurrencies(userWalletId) - - currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = removingCurrencies) - - removeCurrenciesFromWalletManager(userWalletId = userWalletId, currencies = removingCurrencies) - } - - if (currenciesToAdd.isNotEmpty()) { - derivationsRepository.derivePublicKeysByNetworks( - userWalletId = userWalletId, - networks = currenciesToAdd.values.flatten(), - ) - - val addingCurrencies = currenciesToAdd.mapToCryptoCurrencies(userWalletId) - - val savedCurrencies = currenciesRepository.addCurrenciesCache( - userWalletId = userWalletId, - currencies = addingCurrencies, - ) - - parallelUpdatingScope.launch { - withContext(NonCancellable) { - syncTokens(userWalletId = userWalletId, addedCurrencies = savedCurrencies) - - launch { - refreshUpdatedNetworks( - userWalletId = userWalletId, - addedCurrencies = savedCurrencies, - ) - } - launch { - refreshUpdatedStakingBalances( - userWalletId = userWalletId, - addedCurrencies = savedCurrencies, - ) - } - launch { refreshUpdatedQuotes(addedCurrencies = savedCurrencies) } - } - } - } - } - - private suspend fun removeCurrenciesFromWalletManager( - userWalletId: UserWalletId, - currencies: List, - ) { - walletManagersFacade.remove( - userWalletId = userWalletId, - networks = currencies - .filterIsInstance() - .mapTo(hashSetOf(), CryptoCurrency::network), - ) - - walletManagersFacade.removeTokens( - userWalletId = userWalletId, - tokens = currencies.filterIsInstance().toSet(), - ) - } - - private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { - createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) - currenciesRepository.syncTokens(userWalletId) - } - - /** - * Creates wallet managers for the given [currencies] if they do not already exist. - * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. - * - * @param userWalletId The ID of the user's wallet. - * @param currencies The list of cryptocurrencies for which to create wallet managers. - */ - private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { - val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) - - for (network in networks) { - walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) - } - } - - private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = addedCurrencies.map(CryptoCurrency::network).toSet(), - ), - ) - } - - private suspend fun refreshUpdatedStakingBalances( - userWalletId: UserWalletId, - addedCurrencies: List, - ) { - val stakingIds = addedCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() - } - - multiStakingBalanceFetcher( - params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), - ) - } - - private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, - appCurrencyId = null, - ), - ) - } - - private suspend fun Map>.mapToCryptoCurrencies( - userWalletId: UserWalletId, - ): List { - return flatMap { (token, networks) -> - token.availableNetworks - .filter { sourceNetwork -> networks.contains(sourceNetwork.network) } - .map { sourceNetwork -> - when (sourceNetwork) { - is ManagedCryptoCurrency.SourceNetwork.Default -> customTokensRepository.createToken( - managedCryptoCurrency = token, - sourceNetwork = sourceNetwork, - rawId = CryptoCurrency.RawID(token.id.value), - ) - is ManagedCryptoCurrency.SourceNetwork.Main -> customTokensRepository.createCoin( - userWalletId = userWalletId, - networkId = sourceNetwork.id, - derivationPath = sourceNetwork.network.derivationPath, - ) - } - } - } - } -} \ No newline at end of file diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt index 2f6cb3035e..219d138628 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/repository/CustomTokensRepository.kt @@ -43,9 +43,6 @@ interface CustomTokensRepository { formValues: AddCustomTokenForm.Validated.All, ): CryptoCurrency.Token - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun removeCurrency(userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom) - suspend fun convertToCryptoCurrency( userWalletId: UserWalletId, currency: ManagedCryptoCurrency.Custom, diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt deleted file mode 100644 index 08b7e83e21..0000000000 --- a/domain/markets/src/main/java/com/tangem/domain/markets/SaveMarketTokensUseCase.kt +++ /dev/null @@ -1,145 +0,0 @@ -package com.tangem.domain.markets - -import arrow.core.Either -import com.tangem.domain.markets.repositories.MarketsTokenRepository -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.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.multi.MultiStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -/** - * Use case for saving tokens from Markets - * - * @property derivationsRepository derivations repository - * @property marketsTokenRepository markets token repository - * @property currenciesRepository currencies repository - * -[REDACTED_AUTHOR] - */ -@Deprecated("Use ManageCryptoCurrenciesUseCase") -@Suppress("LongParameterList") -class SaveMarketTokensUseCase( - private val derivationsRepository: DerivationsRepository, - private val marketsTokenRepository: MarketsTokenRepository, - private val walletManagersFacade: WalletManagersFacade, - private val currenciesRepository: CurrenciesRepository, - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val multiStakingBalanceFetcher: MultiStakingBalanceFetcher, - private val stakingIdFactory: StakingIdFactory, - private val parallelUpdatingScope: CoroutineScope, -) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - tokenMarketParams: TokenMarketParams, - addedNetworks: Set, - removedNetworks: Set, - ): Either = Either.catch { - if (removedNetworks.isNotEmpty()) { - val removedCurrencies = removedNetworks.mapNotNull { network -> - marketsTokenRepository.createCryptoCurrency( - userWalletId = userWalletId, - token = tokenMarketParams, - network = network, - ) - } - - currenciesRepository.removeCurrencies(userWalletId = userWalletId, currencies = removedCurrencies) - } - - if (addedNetworks.isNotEmpty()) { - derivationsRepository.derivePublicKeysByNetworkIds( - userWalletId = userWalletId, - networkIds = addedNetworks.map { Network.RawID(it.networkId) }, - accountIndex = DerivationIndex.Main, - ) - - val addedCurrencies = addedNetworks.mapNotNull { network -> - marketsTokenRepository.createCryptoCurrency( - userWalletId = userWalletId, - token = tokenMarketParams, - network = network, - accountIndex = DerivationIndex.Main, - ) - } - - val savedCurrencies = currenciesRepository.addCurrenciesCache( - userWalletId = userWalletId, - currencies = addedCurrencies, - ) - - parallelUpdatingScope.launch { - withContext(NonCancellable) { - syncTokens(userWalletId, savedCurrencies) - - launch { refreshUpdatedNetworks(userWalletId, savedCurrencies) } - launch { refreshUpdatedStakingBalances(userWalletId, savedCurrencies) } - launch { refreshUpdatedQuotes(savedCurrencies) } - } - } - } - } - - private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { - createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) - currenciesRepository.syncTokens(userWalletId) - } - - /** - * Creates wallet managers for the given [currencies] if they do not already exist. - * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. - * - * @param userWalletId The ID of the user's wallet. - * @param currencies The list of cryptocurrencies for which to create wallet managers. - */ - private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { - val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) - - for (network in networks) { - walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) - } - } - - private suspend fun refreshUpdatedNetworks(userWalletId: UserWalletId, addedCurrencies: List) { - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = addedCurrencies.map(CryptoCurrency::network).toSet(), - ), - ) - } - - private suspend fun refreshUpdatedStakingBalances( - userWalletId: UserWalletId, - existingCurrencies: List, - ) { - val stakingIds = existingCurrencies.mapNotNullTo(hashSetOf()) { - stakingIdFactory.create(userWalletId = userWalletId, cryptoCurrency = it).getOrNull() - } - - multiStakingBalanceFetcher( - params = MultiStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingIds = stakingIds), - ) - } - - private suspend fun refreshUpdatedQuotes(addedCurrencies: List) { - multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = addedCurrencies.mapNotNullTo(hashSetOf()) { it.id.rawCurrencyId }, - appCurrencyId = null, - ), - ) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt deleted file mode 100644 index 7473e2eb33..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/AddCryptoCurrenciesUseCase.kt +++ /dev/null @@ -1,251 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.getOrElse -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.either -import arrow.core.right -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher -import com.tangem.domain.quotes.multi.MultiQuoteStatusFetcher -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.single.SingleStakingBalanceFetcher -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope - -/** - * A use case for adding multiple cryptocurrencies to a user's wallet. - * - * This use case interacts with the underlying repositories to both add currencies and refresh - * network statuses, particularly after the addition of new tokens. - */ -@Deprecated("Use ManageCryptoCurrenciesUseCase") -@Suppress("LongParameterList") -class AddCryptoCurrenciesUseCase( - private val currenciesRepository: CurrenciesRepository, - private val walletManagersFacade: WalletManagersFacade, - private val multiNetworkStatusFetcher: MultiNetworkStatusFetcher, - private val multiQuoteStatusFetcher: MultiQuoteStatusFetcher, - private val singleStakingBalanceFetcher: SingleStakingBalanceFetcher, - private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val stakingIdFactory: StakingIdFactory, -) { - - /** - * Adds a [cryptoCurrency] token with specific [network] and derivation to the wallet identified by [userWalletId]. - * - * After successfully adding a currency, it also refreshes the networks for tokens - * that are being added and have corresponding coins in the existing currencies list. - * - * @param userWalletId The ID of the user's wallet. - * @param cryptoCurrency Token to add. - * @param network Network where we add - * @return Either an [Throwable] or [Unit] indicating the success of the operation. - */ - suspend operator fun invoke( - userWalletId: UserWalletId, - cryptoCurrency: CryptoCurrency.Token, - network: Network, - ): Either = either { - val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency = cryptoCurrency, network = network) - invoke(userWalletId = userWalletId, currency = tokenToAdd) - } - - /** - * Adds a [currency] to the wallet identified by [userWalletId]. - * - * After successfully adding a currency, it also refreshes the networks for tokens - * that are being added and have corresponding coins in the existing currencies list. - * - * @param userWalletId The ID of the user's wallet. - * @param currency Cryptocurrency to add. - * @return Either an [Throwable] or [Unit] indicating the success of the operation. - */ - suspend operator fun invoke(userWalletId: UserWalletId, currency: CryptoCurrency): Either = - either { - val existingCurrencies = catch( - block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) }, - catch = ::raise, - ) - val currencyToAdd = currency.takeUnless(existingCurrencies::contains) ?: return@either - - val addedCurrencies = addCurrencies(userWalletId, currencyToAdd) - - coroutineScope { - syncTokens(userWalletId, addedCurrencies) - - awaitAll( - async { refreshUpdatedNetworks(userWalletId, currencyToAdd, existingCurrencies) }, - async { refreshUpdatedStakingBalances(userWalletId, currencyToAdd) }, - async { refreshUpdatedQuotes(currencyToAdd) }, - ) - } - } - - suspend operator fun invoke( - userWalletId: UserWalletId, - contractAddress: String, - networkId: String, - ): Either = either { - val existingCurrencies = catch( - block = { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - .toList() - }, - catch = ::raise, - ) - - val foundToken = existingCurrencies - .filterIsInstance() - .firstOrNull { token -> - token.network.backendId == networkId && - !token.isCustom && - token.contractAddress.equals(contractAddress, true) - } - if (foundToken != null) { - return@either foundToken - } - val tokenToAdd = createTokenCurrency(userWalletId, contractAddress, networkId) - val addedCurrencies = addCurrencies(userWalletId, tokenToAdd) - - coroutineScope { - syncTokens(userWalletId = userWalletId, addedCurrencies = addedCurrencies) - - awaitAll( - async { refreshUpdatedNetworks(userWalletId, tokenToAdd, existingCurrencies) }, - async { refreshUpdatedStakingBalances(userWalletId, tokenToAdd) }, - async { refreshUpdatedQuotes(tokenToAdd) }, - ) - } - - tokenToAdd - } - - private suspend fun syncTokens(userWalletId: UserWalletId, addedCurrencies: List) { - createWalletManagers(userWalletId = userWalletId, currencies = addedCurrencies) - currenciesRepository.syncTokens(userWalletId) - } - - /** - * Creates wallet managers for the given [currencies] if they do not already exist. - * The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature. - * - * @param userWalletId The ID of the user's wallet. - * @param currencies The list of cryptocurrencies for which to create wallet managers. - */ - private suspend fun createWalletManagers(userWalletId: UserWalletId, currencies: List) { - val networks = currencies.mapTo(hashSetOf(), CryptoCurrency::network) - - for (network in networks) { - walletManagersFacade.getOrCreateWalletManager(userWalletId = userWalletId, network = network) - } - } - - /** - * Refreshes the network statuses for tokens that have corresponding coins in the - * [existingCurrencies] list. - */ - private suspend fun refreshUpdatedNetworks( - userWalletId: UserWalletId, - currencyToAdd: CryptoCurrency, - existingCurrencies: List, - ) { - val networksToUpdate = currencyToAdd.takeIf { currency -> - currency is CryptoCurrency.Token && hasCoinForToken(existingCurrencies, currency) - } - ?.network - - val networkToUpdate = currencyToAdd.takeIf { - !existingCurrencies.map(CryptoCurrency::network).contains(it.network) - } - ?.network - - multiNetworkStatusFetcher( - MultiNetworkStatusFetcher.Params( - userWalletId = userWalletId, - networks = setOfNotNull(networksToUpdate, networkToUpdate), - ), - ) - } - - private suspend fun refreshUpdatedStakingBalances( - userWalletId: UserWalletId, - addedCurrency: CryptoCurrency, - ): Either = either { - val stakingId = stakingIdFactory.create( - userWalletId = userWalletId, - currencyId = addedCurrency.id, - network = addedCurrency.network, - ) - .getOrElse { error -> - when (error) { - is StakingIdFactory.Error.UnableToGetAddress -> raise(IllegalStateException("$error")) - StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() - } - - return@either - } - - singleStakingBalanceFetcher( - params = SingleStakingBalanceFetcher.Params(userWalletId = userWalletId, stakingId = stakingId), - ) - .bind() - } - - private suspend fun refreshUpdatedQuotes(currencyToAdd: CryptoCurrency) { - multiQuoteStatusFetcher( - params = MultiQuoteStatusFetcher.Params( - currenciesIds = setOfNotNull(currencyToAdd.id.rawCurrencyId), - appCurrencyId = null, - ), - ) - } - - private suspend fun Raise.createTokenCurrency( - userWalletId: UserWalletId, - contractAddress: String, - networkId: String, - ): CryptoCurrency.Token { - return catch( - block = { - currenciesRepository.createTokenCurrency( - userWalletId = userWalletId, - contractAddress = contractAddress, - networkId = networkId, - ) - }, - catch = { - raise(it) - }, - ) - } - - private suspend fun Raise.addCurrencies( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): List { - return catch( - block = { currenciesRepository.addCurrenciesCache(userWalletId, listOf(currency)) }, - catch = ::raise, - ) - } - - /** - * Determines if the [existingCurrencies] list contains a coin that corresponds - * to the given [token]. - */ - private fun hasCoinForToken(existingCurrencies: List, token: CryptoCurrency.Token): Boolean { - return existingCurrencies.any { currency -> - currency is CryptoCurrency.Coin && currency.network == token.network - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt deleted file mode 100644 index e98428eee2..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCase.kt +++ /dev/null @@ -1,115 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.Raise -import arrow.core.raise.catch -import arrow.core.raise.either -import arrow.core.raise.ensureNotNull -import arrow.core.toNonEmptyListOrNull -import arrow.core.toNonEmptySetOrNull -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.withContext - -@Deprecated("Use ApplyAccountListSortingUseCase") -class ApplyTokenListSortingUseCase( - private val currenciesRepository: CurrenciesRepository, - private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val dispatchers: CoroutineDispatcherProvider, -) { - - suspend operator fun invoke( - userWalletId: UserWalletId, - sortedTokensIds: List, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ): Either { - return either { - val storedCurrencies = getCurrencies(userWalletId) - val isSortingTypeChanged = checkIsCurrenciesSortedByBalance(userWalletId) != isSortedByBalance - val isGroupingTypeChanged = checkIsCurrenciesGroupedByNetwork(userWalletId) != isGroupedByNetwork - - val sortedCurrencies = sortTokens(sortedTokensIds, storedCurrencies) - - if (storedCurrencies != sortedCurrencies || isSortingTypeChanged || isGroupingTypeChanged) { - applySorting( - userWalletId = userWalletId, - currencies = sortedCurrencies, - isGrouped = isGroupedByNetwork, - isSortedByBalance = isSortedByBalance, - ) - } - } - } - - private suspend fun Raise.checkIsCurrenciesSortedByBalance(userWalletId: UserWalletId) = - catch( - block = { currenciesRepository.isTokensSortedByBalance(userWalletId).firstOrNull() == true }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - - private suspend fun Raise.checkIsCurrenciesGroupedByNetwork(userWalletId: UserWalletId) = - catch( - block = { currenciesRepository.isTokensGrouped(userWalletId).firstOrNull() == true }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - - private suspend fun Raise.sortTokens( - sortedCurrenciesIds: List, - unsortedCurrencies: List, - ): List = withContext(dispatchers.default) { - val nonEmptySortedTokensIds = ensureNotNull(sortedCurrenciesIds.toNonEmptySetOrNull()) { - TokenListSortingError.TokenListIsEmpty - } - - val sortedTokens = sortedMapOf() - - unsortedCurrencies.distinct().forEach { currency -> - val index = nonEmptySortedTokensIds.indexOfFirst { currencyId -> - currencyId == currency.id - } - - if (index >= 0) { - sortedTokens[index] = currency - } else { - raise(TokenListSortingError.UnableToSortTokenList) - } - } - - ensureNotNull(sortedTokens.values.toNonEmptyListOrNull()) { - TokenListSortingError.TokenListIsEmpty - } - } - - private suspend fun Raise.getCurrencies(userWalletId: UserWalletId): List { - val tokens = catch( - block = { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - - return ensureNotNull(tokens.toNonEmptyListOrNull()) { - TokenListSortingError.TokenListIsEmpty - } - } - - private suspend fun Raise.applySorting( - userWalletId: UserWalletId, - currencies: List, - isGrouped: Boolean, - isSortedByBalance: Boolean, - ) = withContext(dispatchers.io) { - catch( - block = { currenciesRepository.saveTokens(userWalletId, currencies, isGrouped, isSortedByBalance) }, - catch = { raise(TokenListSortingError.DataError(it)) }, - ) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt deleted file mode 100644 index 1ff28dd2af..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.raise.catch -import arrow.core.raise.either -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.remove.RemoveCurrencyError -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.domain.walletmanager.WalletManagersFacade - -@Deprecated("Use ManageCryptoCurrenciesUseCase") -class RemoveCurrencyUseCase( - private val currenciesRepository: CurrenciesRepository, - private val walletManagersFacade: WalletManagersFacade, - private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, -) { - - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend operator fun invoke( - userWalletId: UserWalletId, - currency: CryptoCurrency, - ): Either { - return either { - if (hasLinkedTokens(userWalletId, currency)) { - raise(RemoveCurrencyError.HasLinkedTokens) - } - - catch( - block = { - currenciesRepository.removeCurrency(userWalletId, currency) - - when (currency) { - is CryptoCurrency.Coin -> { - walletManagersFacade.remove(userWalletId, setOf(currency.network)) - } - is CryptoCurrency.Token -> { - walletManagersFacade.removeTokens(userWalletId, setOf(currency)) - } - } - }, - catch = { raise(RemoveCurrencyError.DataError(it)) }, - ) - } - } - - suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean { - return when (currency) { - is CryptoCurrency.Coin -> { - val walletCurrencies = multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId), - ) - .orEmpty() - - walletCurrencies.any { it is CryptoCurrency.Token && it.network == currency.network } - } - is CryptoCurrency.Token -> false - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 91a762bc11..779e36f447 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -16,58 +16,6 @@ import kotlinx.coroutines.flow.Flow @Suppress("TooManyFunctions") interface CurrenciesRepository { - /** - * Saves the given list of cryptocurrencies, along with the preferences for grouping and sorting, for a specific - * multi-currency user wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * @param currencies The list of cryptocurrencies to be saved. - * @param isGroupedByNetwork A boolean flag indicating whether the tokens should be grouped by network. - * @param isSortedByBalance A boolean flag indicating whether the tokens should be sorted by balance. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun saveTokens( - userWalletId: UserWalletId, - currencies: List, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ) - - /** - * Add currencies to a specific user wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * @param currencies The currencies which must be added. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun addCurrenciesCache(userWalletId: UserWalletId, currencies: List): List - - /** - * Removes currency from a specific user wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * @param currency The currency which must be removed. - * @throws DataError.UserWalletError.WrongUserWallet If multi-currency user wallet - * ID provided. - */ - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) - - /** - * Removes currencies from a specific user wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * @param currencies The currencies which must be removed. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use ManageCryptoCurrenciesUseCase") - suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) - /** * Retrieves the list of cryptocurrencies within a user wallet. * @@ -211,18 +159,6 @@ interface CurrenciesRepository { fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean - /** - * Synchronizes local tokens with remote data for a specific user wallet. - * This method ensures that the local token list matches the remote state by fetching - * the token data from the local cache and push it to backend. - * - * @param userWalletId The unique identifier of the user wallet to sync tokens for. - * @throws Exception if the sync request to the backend fails - */ - @Deprecated("Use AccountsCRUDRepository instead") - @Throws - suspend fun syncTokens(userWalletId: UserWalletId) - @Throws fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver? } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt deleted file mode 100644 index be67de0762..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/ApplyTokenListSortingUseCaseTest.kt +++ /dev/null @@ -1,207 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import arrow.core.left -import arrow.core.right -import com.tangem.domain.core.error.DataError -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListSortingError -import com.tangem.domain.tokens.mock.MockTokens -import com.tangem.domain.tokens.repository.MockCurrenciesRepository -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.mockk -import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.runTest -import org.junit.Test -import kotlin.random.Random - -internal class ApplyTokenListSortingUseCaseTest { - - private val userWalletId = UserWalletId(value = null) - - @Test - fun `when tokens are empty then error should be received`() = runTest { - // Given - val expectedResult = TokenListSortingError.TokenListIsEmpty.left() - - val useCase = getUseCase() - - // When - val result = useCase( - userWalletId = userWalletId, - sortedTokensIds = emptyList(), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when tokens saving failed then error should be received`() = runTest { - // Given - val expectedResult = TokenListSortingError.DataError(DataError.NetworkError.NoInternetConnection).left() - - val repository = getTokensRepository( - sortTokensResult = DataError.NetworkError.NoInternetConnection.left(), - ) - val useCase = getUseCase(repository) - - // When - val result = useCase( - userWalletId = userWalletId, - sortedTokensIds = MockTokens.tokens.map { it.id }.sortedByDescending { it.value }, - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - // Then - assertEquals(expectedResult, result) - } - - @Test - fun `when apply sorting for sorted and grouped list then correct args should be used`() = runTest { - // Given - val expectedTokens = getSortedTokens() - val expectedIsGrouped = true - val expectedIsSorted = true - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - useCase( - userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.id }, - isGroupedByNetwork = expectedIsGrouped, - isSortedByBalance = expectedIsSorted, - ) - - // Then - assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) - assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) - assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) - } - - @Test - fun `when apply sorting for unsorted and grouped list then correct args should be used`() = runTest { - // Given - val expectedTokens = getSortedTokens() - val expectedIsGrouped = true - val expectedIsSorted = false - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - useCase( - userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.id }, - isGroupedByNetwork = expectedIsGrouped, - isSortedByBalance = expectedIsSorted, - ) - - // Then - assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) - assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) - assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) - } - - @Test - fun `when apply sorting for sorted and ungrouped list then correct args should be used`() = runTest { - // Given - val expectedTokens = getSortedTokens() - val expectedIsGrouped = false - val expectedIsSorted = true - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - useCase( - userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.id }, - isGroupedByNetwork = expectedIsGrouped, - isSortedByBalance = expectedIsSorted, - ) - - // Then - assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) - assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) - assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) - } - - @Test - fun `when apply sorting for unsorted and ungrouped list then correct args should be used`() = runTest { - // Given - val expectedTokens = getSortedTokens() - val expectedIsGrouped = false - val expectedIsSorted = false - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - useCase( - userWalletId = userWalletId, - sortedTokensIds = expectedTokens.map { it.id }, - isGroupedByNetwork = expectedIsGrouped, - isSortedByBalance = expectedIsSorted, - ) - - // Then - assertEquals(expectedTokens, repository.tokensIdsAfterSortingApply) - assertEquals(expectedIsGrouped, repository.isTokensGroupedAfterSortingApply) - assertEquals(expectedIsSorted, repository.isTokensSortedByBalanceAfterSortingApply) - } - - @Test - fun `when sorted tokens IDs do not contain all tokens IDs then error should be received`() = runTest { - // Given - val expectedResult = TokenListSortingError.UnableToSortTokenList.left() - - val repository = getTokensRepository() - val useCase = getUseCase(repository) - - // When - val result = useCase( - userWalletId = userWalletId, - sortedTokensIds = getSortedTokens().drop(n = 3).map { it.id }, - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - - // Then - assertEquals(expectedResult, result) - } - - private fun getSortedTokens() = MockTokens.tokens - .sortedBy { Random.nextInt(0, MockTokens.tokens.size) } - - private fun getUseCase(tokensRepository: MockCurrenciesRepository = getTokensRepository()) = - ApplyTokenListSortingUseCase( - currenciesRepository = tokensRepository, - dispatchers = TestingCoroutineDispatcherProvider(), - multiWalletCryptoCurrenciesSupplier = mockk(), - ) - - private fun getTokensRepository( - sortTokensResult: Either = Unit.right(), - removeCurrencyResult: Either = Unit.right(), - tokens: Flow>> = flowOf(MockTokens.tokens.right()), - ): MockCurrenciesRepository { - return MockCurrenciesRepository( - sortTokensResult = sortTokensResult, - removeCurrencyResult = removeCurrencyResult, - token = MockTokens.token1.right(), - tokens = tokens, - isGrouped = emptyFlow(), - isSortedByBalance = emptyFlow(), - ) - } -} \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt deleted file mode 100644 index befef5c116..0000000000 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.tangem.domain.tokens.repository - -import arrow.core.Either -import arrow.core.getOrElse -import com.tangem.domain.card.CardTypesResolver -import com.tangem.domain.core.error.DataError -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.model.FeePaidCurrency -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map - -internal class MockCurrenciesRepository( - private val sortTokensResult: Either, - private val removeCurrencyResult: Either, - private val token: Either, - private val tokens: Flow>>, - private val isGrouped: Flow>, - private val isSortedByBalance: Flow>, -) : CurrenciesRepository { - - var tokensIdsAfterSortingApply: List? = null - private set - - var isTokensGroupedAfterSortingApply: Boolean? = null - private set - - var isTokensSortedByBalanceAfterSortingApply: Boolean? = null - private set - - override suspend fun saveTokens( - userWalletId: UserWalletId, - currencies: List, - isGroupedByNetwork: Boolean, - isSortedByBalance: Boolean, - ) { - sortTokensResult.onLeft { throw it } - - tokensIdsAfterSortingApply = currencies - isTokensGroupedAfterSortingApply = isGroupedByNetwork - isTokensSortedByBalanceAfterSortingApply = isSortedByBalance - } - - override suspend fun addCurrenciesCache( - userWalletId: UserWalletId, - currencies: List, - ): List = emptyList() - - override suspend fun removeCurrency(userWalletId: UserWalletId, currency: CryptoCurrency) { - removeCurrencyResult.onLeft { throw it } - } - - override suspend fun removeCurrencies(userWalletId: UserWalletId, currencies: List) = Unit - - override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { - return emptyFlow() - } - - override suspend fun getMultiCurrencyWalletCurrenciesSync( - userWalletId: UserWalletId, - refresh: Boolean, - ): List { - return tokens.first().getOrElse { e -> throw e } - } - - override suspend fun getSingleCurrencyWalletPrimaryCurrency( - userWalletId: UserWalletId, - refresh: Boolean, - ): CryptoCurrency { - return token.getOrElse { e -> throw e } - } - - override suspend fun getSingleCurrencyWalletWithCardCurrencies( - userWalletId: UserWalletId, - refresh: Boolean, - ): List { - return tokens.first().getOrElse { e -> throw e } - } - - override suspend fun getSingleCurrencyWalletWithCardCurrency( - userWalletId: UserWalletId, - id: CryptoCurrency.ID, - ): CryptoCurrency { - return token.getOrElse { e -> throw e } - } - - override suspend fun getNetworkCoin( - userWalletId: UserWalletId, - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin { - TODO("Not yet implemented") - } - - override fun isTokensGrouped(userWalletId: UserWalletId): Flow { - return isGrouped.map { it.getOrElse { e -> throw e } } - } - - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { - return isSortedByBalance.map { it.getOrElse { e -> throw e } } - } - - override suspend fun isSendBlockedByPendingTransactions( - userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Boolean { - return false - } - - override suspend fun getFeePaidCurrency(userWalletId: UserWalletId, network: Network): FeePaidCurrency { - return FeePaidCurrency.Coin - } - - override fun createCoinCurrency(network: Network): CryptoCurrency.Coin { - error("not implemented") - } - - override fun createTokenCurrency(cryptoCurrency: CryptoCurrency.Token, network: Network): CryptoCurrency.Token { - return cryptoCurrency - } - - override suspend fun createTokenCurrency( - userWalletId: UserWalletId, - contractAddress: String, - networkId: String, - ): CryptoCurrency.Token { - error("not implemented") - } - - override fun getAllWalletsCryptoCurrencies( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return emptyFlow() - } - - override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean { - return false - } - - override suspend fun syncTokens(userWalletId: UserWalletId) { - return Unit - } - - override fun getCardTypesResolver(userWalletId: UserWalletId): CardTypesResolver { - error("No-op") - } -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt index 51a46bd0e0..42e7cf42b9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/CustomTokenFormUseCasesFacade.kt @@ -19,7 +19,6 @@ 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.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.lib.crypto.derivation.AccountNodeRecognizer import dagger.assisted.Assisted @@ -30,7 +29,6 @@ import timber.log.Timber @Suppress("LongParameterList") internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( @Assisted private val userWalletId: UserWalletId, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val derivePublicKeysUseCase: DerivePublicKeysUseCase, private val checkIsCurrencyNotAddedUseCase: CheckIsCurrencyNotAddedUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, @@ -40,14 +38,10 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( ) { suspend fun addCryptoCurrenciesUseCase(currency: CryptoCurrency): Either { - return if (accountsFeatureToggles.isFeatureEnabled) { - either { - val accountId = getAccountId(currency) + return either { + val accountId = getAccountId(currency) - manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind() - } - } else { - addCryptoCurrenciesUseCase.invoke(userWalletId = userWalletId, currency = currency) + manageCryptoCurrenciesUseCase(accountId = accountId, add = currency).bind() } } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt index e692352aad..e291de38c9 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt @@ -7,7 +7,10 @@ import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier -import com.tangem.domain.managetokens.* +import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase +import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase +import com.tangem.domain.managetokens.GetDistinctManagedCurrenciesUseCase +import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState import com.tangem.domain.managetokens.model.ManageTokensListConfig import com.tangem.domain.managetokens.model.ManagedCryptoCurrency @@ -27,10 +30,8 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( val getManagedTokensUseCase: GetManagedTokensUseCase, val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase, private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, - private val removeCustomCurrencyUseCase: RemoveCustomManagedCryptoCurrencyUseCase, private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, - private val saveManagedTokensUseCase: SaveManagedTokensUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val customTokensRepository: CustomTokensRepository, private val accountsFeatureToggles: AccountsFeatureToggles, @@ -69,10 +70,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( manageCryptoCurrenciesUseCase(accountId = mode.accountId, remove = currency) } - is ManageTokensMode.Wallet -> removeCustomCurrencyUseCase.invoke( - userWalletId = mode.userWalletId, - customCurrency = customCurrency, - ) + is ManageTokensMode.Wallet -> error("Unsupported") ManageTokensMode.None -> nonePortfolioError.left() } } @@ -147,13 +145,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( remove = currenciesToRemove.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId), ) } - is ManageTokensMode.Wallet -> { - saveManagedTokensUseCase.invoke( - userWalletId = mode.userWalletId, - currenciesToAdd = currenciesToAdd, - currenciesToRemove = currenciesToRemove, - ) - } + is ManageTokensMode.Wallet -> error("Unsupported") ManageTokensMode.None -> nonePortfolioError.left() } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt index 66fa4f5693..9863a66e16 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddToPortfolioModel.kt @@ -1,12 +1,11 @@ package com.tangem.features.onramp.hottokens.portfolio.model -import arrow.core.getOrElse import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase -import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent import com.tangem.features.onramp.hottokens.portfolio.entity.OnrampAddToPortfolioUM @@ -21,11 +20,10 @@ import javax.inject.Inject /** * Model for adding token to portfolio * - * @param paramsContainer params container - * @property dispatchers dispatchers - * @property derivePublicKeysUseCase use case for deriving public key - * @property addCryptoCurrenciesUseCase use case for adding crypto currency - * @property getUserWalletUseCase use case for getting user wallet by id + * @param paramsContainer params container + * @property dispatchers dispatchers + * @property manageCryptoCurrenciesUseCase use case for managing crypto currencies + * @property getUserWalletUseCase use case for getting user wallet by id * [REDACTED_AUTHOR] */ @@ -33,8 +31,7 @@ import javax.inject.Inject internal class OnrampAddToPortfolioModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, + private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, ) : Model() { @@ -72,21 +69,14 @@ internal class OnrampAddToPortfolioModel @Inject constructor( private fun onAddClick() { modelScope.launch { changeAddButtonProgressStatus(isProgress = true) - derivePublicKeysUseCase( - userWalletId = params.userWalletId, - currencies = listOf(params.cryptoCurrency), - ).getOrElse { throwable -> - Timber.e("Failed to derive public keys: $throwable") - changeAddButtonProgressStatus(isProgress = false) - } - - addCryptoCurrenciesUseCase( - userWalletId = params.userWalletId, - currency = params.cryptoCurrency, - ) + val accountId = AccountId.forMainCryptoPortfolio(params.userWalletId) + manageCryptoCurrenciesUseCase(accountId = accountId, add = params.cryptoCurrency) .onRight { params.onSuccessAdding(params.cryptoCurrency.id) } - .onLeft { changeAddButtonProgressStatus(isProgress = false) } + .onLeft { throwable -> + Timber.e("Failed to add crypto currency: $throwable") + changeAddButtonProgressStatus(isProgress = false) + } } } diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt index 67af26dc2a..49ad108455 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractor.kt @@ -1,6 +1,6 @@ package com.tangem.feature.referral.domain -import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId @@ -11,7 +11,7 @@ interface ReferralInteractor { suspend fun getReferralStatus(userWalletId: UserWalletId): ReferralData - suspend fun startReferral(portfolioId: PortfolioId): ReferralData + suspend fun startReferral(accountId: AccountId): ReferralData suspend fun getCryptoCurrency( userWalletId: UserWalletId, diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt index 930418c920..5e67063fe0 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/ReferralInteractorImpl.kt @@ -1,30 +1,22 @@ package com.tangem.feature.referral.domain -import arrow.core.getOrElse import com.tangem.common.core.TangemSdkError import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.domain.errors.ReferralError import com.tangem.feature.referral.domain.models.ReferralData import com.tangem.feature.referral.domain.models.TokenData import timber.log.Timber -@Suppress("LongParameterList") internal class ReferralInteractorImpl( private val repository: ReferralRepository, - private val derivePublicKeysUseCase: DerivePublicKeysUseCase, - private val getUserWalletUseCase: GetUserWalletUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val singleAccountSupplier: SingleAccountSupplier, private val walletManagersFacade: WalletManagersFacade, @@ -40,64 +32,41 @@ internal class ReferralInteractorImpl( return referralData } - override suspend fun startReferral(portfolioId: PortfolioId): ReferralData { + override suspend fun startReferral(accountId: AccountId): ReferralData { if (tokensForReferral.isEmpty()) error("Tokens for ref is empty") val tokenData = tokensForReferral.first() - val userWalletId = portfolioId.userWalletId - val userWallet = getUserWalletUseCase(userWalletId).getOrElse { - error("Failed to get user wallet $userWalletId: $it") - } + val userWalletId = accountId.userWalletId - val accountIndex = when (portfolioId) { - is PortfolioId.Account -> { - val account = singleAccountSupplier.getSyncOrNull( - params = SingleAccountProducer.Params(accountId = portfolioId.accountId), - ) - ?: error("Account not found: ${portfolioId.accountId}") + val account = singleAccountSupplier.getSyncOrNull( + params = SingleAccountProducer.Params(accountId = accountId), + ) + ?: error("Account not found: $accountId") - when (account) { - is Account.CryptoPortfolio -> account.derivationIndex - is Account.Payment -> TODO("[REDACTED_JIRA]") - } - } - is PortfolioId.Wallet -> null + val accountIndex = when (account) { + is Account.CryptoPortfolio -> account.derivationIndex + is Account.Payment -> TODO("[REDACTED_JIRA]") } val cryptoCurrency = getCryptoCurrency( - userWalletId = portfolioId.userWalletId, + userWalletId = accountId.userWalletId, tokenData = tokenData, accountIndex = accountIndex, ) ?: error("Failed to create crypto currency") - when (portfolioId) { - is PortfolioId.Account -> { - manageCryptoCurrenciesUseCase( - accountId = portfolioId.accountId, - add = cryptoCurrency, - skipDerivationErrors = false, - ).mapLeft { - it.mapToDomainError() - }.onLeft { error -> - if (error is ReferralError.UserCancelledException) { - throw error - } + manageCryptoCurrenciesUseCase( + accountId = accountId, + add = cryptoCurrency, + skipDerivationErrors = false, + ) + .mapLeft { it.mapToDomainError() } + .onLeft { error -> + Timber.e(error) + if (error is ReferralError.UserCancelledException) { + throw error } } - is PortfolioId.Wallet -> { - derivePublicKeysUseCase(userWallet.walletId, listOf(cryptoCurrency)).getOrElse { throwable -> - Timber.e("Failed to derive public keys: $throwable") - throw throwable.mapToDomainError() - } - - addCryptoCurrenciesUseCase( - userWalletId = userWallet.walletId, - currency = cryptoCurrency, - ) - } - } - .onLeft(Timber::e) val publicAddress = walletManagersFacade.getDefaultAddress( userWalletId = userWalletId, diff --git a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt index c82165bda3..f560ae39b1 100644 --- a/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt +++ b/features/referral/domain/src/main/java/com/tangem/feature/referral/domain/di/ReferralDomainModule.kt @@ -4,10 +4,7 @@ import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.usecase.DerivePublicKeysUseCase -import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.referral.domain.ReferralInteractor import com.tangem.feature.referral.domain.ReferralInteractorImpl import com.tangem.feature.referral.domain.ReferralRepository @@ -23,18 +20,12 @@ class ReferralDomainModule { @ModelScoped fun provideReferralInteractor( referralRepository: ReferralRepository, - derivePublicKeysUseCase: DerivePublicKeysUseCase, - getUserWalletUseCase: GetUserWalletUseCase, - addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, singleAccountSupplier: SingleAccountSupplier, walletManagersFacade: WalletManagersFacade, ): ReferralInteractor { return ReferralInteractorImpl( repository = referralRepository, - derivePublicKeysUseCase = derivePublicKeysUseCase, - getUserWalletUseCase = getUserWalletUseCase, - addCryptoCurrenciesUseCase = addCryptoCurrenciesUseCase, manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase, singleAccountSupplier = singleAccountSupplier, walletManagersFacade = walletManagersFacade, diff --git a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt index eddb1e949d..f928bf5249 100644 --- a/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt +++ b/features/referral/impl/src/main/java/com/tangem/feature/referral/model/ReferralModel.kt @@ -15,7 +15,6 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.navigation.share.ShareManager import com.tangem.core.navigation.url.UrlOpener -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.model.AccountCryptoCurrency import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer @@ -24,8 +23,6 @@ import com.tangem.domain.account.status.usecase.GetAccountCurrencyByAddressUseCa import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.PortfolioId -import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -44,10 +41,12 @@ import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorComponent import com.tangem.features.account.PortfolioSelectorController import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import javax.inject.Inject +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("LongParameterList") @Stable @ModelScoped @@ -60,7 +59,6 @@ internal class ReferralModel @Inject constructor( private val urlOpener: UrlOpener, private val getUserWalletUseCase: GetUserWalletUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val portfolioFetcherFactory: PortfolioFetcher.Factory, val portfolioSelectorController: PortfolioSelectorController, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, @@ -92,26 +90,24 @@ internal class ReferralModel @Inject constructor( init { analyticsEventHandler.send(ReferralEvents.ReferralScreenOpened()) - if (accountsFeatureToggles.isFeatureEnabled) { - combine( - flow = referralData.filterNotNull().onEach(::selectAccount), - flow2 = portfolioSelectorController.isAccountMode, - transform = { referralData, isAccountMode -> referralData to isAccountMode }, - ).transformLatest { (referralData, isAccountMode) -> - when (isAccountMode) { - false -> showContent(referralData) - true -> combineAccountUI(referralData) + + combine( + flow = referralData.filterNotNull().onEach(::selectAccount), + flow2 = portfolioSelectorController.isAccountMode, + transform = { referralData, isAccountMode -> referralData to isAccountMode }, + ) + .transformLatest { (referralData, isAccountMode) -> + if (!isAccountMode) { + showContent(referralData) + } else { + combineAccountUI(referralData) .map { referralData to it } .collect(::emit) } } - .onEach { (referralData, accountAward) -> showContent(referralData, accountAward) } - .launchIn(modelScope) - } else { - referralData.filterNotNull() - .onEach(::showContent) - .launchIn(modelScope) - } + .onEach { (referralData, accountAward) -> showContent(referralData, accountAward) } + .launchIn(modelScope) + loadReferralData() } @@ -123,12 +119,7 @@ internal class ReferralModel @Inject constructor( flow3 = getSelectedAppCurrencyUseCase.invokeOrDefault(), flow4 = portfolioFetcher.data, ) { pair, isBalanceHidden, appCurrency, portfolios -> - val selectedAccount = pair?.second ?: return@combine null - - val cryptoPortfolio = when (selectedAccount) { - is AccountStatus.CryptoPortfolio -> selectedAccount - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") - } + val cryptoPortfolio = pair?.second ?: return@combine null val awardCryptoCurrency = referralInteractor.getCryptoCurrency( userWalletId = params.userWalletId, @@ -185,11 +176,8 @@ internal class ReferralModel @Inject constructor( val lastInfoState = uiState.referralInfoState uiState = uiState.copy(referralInfoState = ReferralInfoState.Loading) modelScope.launch { - val portfolioId = when (accountsFeatureToggles.isFeatureEnabled) { - true -> PortfolioId(requireNotNull(portfolioSelectorController.selectedAccountSync)) - false -> PortfolioId(params.userWalletId) - } - runCatching { referralInteractor.startReferral(portfolioId) } + val accountId = requireNotNull(portfolioSelectorController.selectedAccountSync) + runCatching { referralInteractor.startReferral(accountId) } .onSuccess { referral -> analyticsEventHandler.send(ReferralEvents.ParticipateSuccessful()) referralData.value = referral diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt index 3411103f5c..c4b53d2a7f 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/confirm/model/SendConfirmModel.kt @@ -23,7 +23,6 @@ import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.feedback.GetWalletMetaInfoUseCase @@ -36,7 +35,6 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.IsAmountSubtractAvailableUseCase import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase @@ -75,10 +73,8 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.stripZeroPlainString import com.tangem.utils.transformer.update -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject @@ -100,7 +96,6 @@ internal class SendConfirmModel @Inject constructor( private val saveBlockchainErrorUseCase: SaveBlockchainErrorUseCase, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val isAmountSubtractAvailableUseCase: IsAmountSubtractAvailableUseCase, private val feeSelectorCheckReloadListener: FeeSelectorCheckReloadListener, @@ -114,7 +109,6 @@ internal class SendConfirmModel @Inject constructor( private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, private val sendAmountReduceTrigger: SendAmountReduceTrigger, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val currenciesRepository: CurrenciesRepository, private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase, @@ -466,24 +460,14 @@ internal class SendConfirmModel @Inject constructor( .firstOrNull { it.address == confirmData.enteredDestination } ?: return - val userWalletId = receivingUserWallet.userWalletId ?: return val network = receivingUserWallet.network ?: return modelScope.launch(dispatchers.default) { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountId = receivingUserWallet.accountId ?: return@launch + val accountId = receivingUserWallet.accountId ?: return@launch - val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency, network) - manageCryptoCurrenciesUseCase(accountId = accountId, add = tokenToAdd) - } else { - withContext(NonCancellable) { - addCryptoCurrenciesUseCase( - userWalletId = userWalletId, - cryptoCurrency = cryptoCurrency, - network = network, - ) - } - }.onLeft(Timber::e) + val tokenToAdd = currenciesRepository.createTokenCurrency(cryptoCurrency, network) + manageCryptoCurrenciesUseCase(accountId = accountId, add = tokenToAdd) + .onLeft(Timber::e) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index a0b3afb86c..17c4f1ad60 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -115,7 +115,7 @@ internal class TokenDetailsModel @Inject constructor( private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase, - private val removeCurrencyUseCase: RemoveCurrencyUseCase, + private val isCryptoCurrencyCoinCouldHideUseCase: IsCryptoCurrencyCoinCouldHideUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getCurrencyWarningsUseCase: GetCurrencyWarningsUseCase, private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, @@ -758,8 +758,12 @@ internal class TokenDetailsModel @Inject constructor( analyticsEventsHandler.send(TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrency.symbol)) modelScope.launch { - val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(userWalletId, cryptoCurrency) - internalUiState.value = if (hasLinkedTokens) { + val canHide = cryptoCurrency is CryptoCurrency.Coin && isCryptoCurrencyCoinCouldHideUseCase( + userWalletId = userWalletId, + cryptoCurrencyCoin = cryptoCurrency, + ) + + internalUiState.value = if (!canHide) { stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency) } else { stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency) @@ -769,18 +773,14 @@ internal class TokenDetailsModel @Inject constructor( override fun onHideConfirmed() { modelScope.launch { - if (accountsFeatureToggles.isFeatureEnabled) { - val accountId = account?.accountId + val accountId = account?.accountId - if (accountId == null) { - Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}") - return@launch - } - - manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency) - } else { - removeCurrencyUseCase(userWalletId, cryptoCurrency) + if (accountId == null) { + Timber.e("Account ID is null, cannot hide currency ${cryptoCurrency.id}") + return@launch } + + manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrency) .onLeft { Timber.e(it) } .onRight { router.popBackStack() } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt index 778b4f2edf..20d13122a3 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/ExchangeStatusFactory.kt @@ -5,7 +5,6 @@ import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.datasource.local.swap.SwapTransactionStatusStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -14,7 +13,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository @@ -38,8 +36,6 @@ internal class ExchangeStatusFactory @AssistedInject constructor( private val swapTransactionRepository: SwapTransactionRepository, private val swapRepository: SwapRepository, private val quotesRepository: QuotesRepository, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val swapTransactionStatusStore: SwapTransactionStatusStore, @@ -117,30 +113,22 @@ internal class ExchangeStatusFactory @AssistedInject constructor( ifRight = { statusModel -> sendStatusUpdateAnalytics(statusModel, provider) - val accountId = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = userWallet.walletId, - currency = cryptoCurrency, - ) - .map { it.account.accountId } - .getOrNull() - } else { - null - } + val accountId = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + ) + .map { it.account.accountId } + .getOrNull() - val refundTokenCurrency = if (accountsFeatureToggles.isFeatureEnabled) { - if (accountId != null) { - addRefundCurrencyIfNeededNew( - accountId = accountId, - status = statusModel, - type = provider.type, - ) - } else { - Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") - null - } + val refundTokenCurrency = if (accountId != null) { + addRefundCurrencyIfNeeded( + accountId = accountId, + status = statusModel, + type = provider.type, + ) } else { - addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type) + Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + null } swapTransactionRepository.storeTransactionState( @@ -170,28 +158,7 @@ internal class ExchangeStatusFactory @AssistedInject constructor( } } - /** - * For now do it only for dex-bridge provider - */ - private suspend fun addRefundCurrencyIfNeededLegacy( - status: ExchangeStatusModel?, - type: ExchangeProviderType, - ): CryptoCurrency? { - status ?: return null - if (type != ExchangeProviderType.DEX_BRIDGE) return null - val refundNetwork = status.refundNetwork - val refundContractAddress = status.refundContractAddress - if (refundNetwork != null && refundContractAddress != null) { - return addCryptoCurrenciesUseCase( - userWalletId = userWallet.walletId, - contractAddress = refundContractAddress, - networkId = refundNetwork, - ).getOrNull() - } - return null - } - - private suspend fun addRefundCurrencyIfNeededNew( + private suspend fun addRefundCurrencyIfNeeded( accountId: AccountId, status: ExchangeStatusModel?, type: ExchangeProviderType, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt index 7c10c7d5d6..d145a7ab35 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/express/TokenDetailsExchangeStatusFactory.kt @@ -4,7 +4,6 @@ import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.datasource.local.swap.ExpressAnalyticsStatus import com.tangem.datasource.local.swap.SwapTransactionStatusStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -13,7 +12,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.quote.QuoteStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase import com.tangem.domain.tokens.model.analytics.TokenExchangeAnalyticsEvent import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.api.SwapRepository @@ -39,8 +37,6 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( private val swapTransactionRepository: SwapTransactionRepository, private val swapRepository: SwapRepository, private val quotesRepository: QuotesRepository, - private val addCryptoCurrenciesUseCase: AddCryptoCurrenciesUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val swapTransactionStatusStore: SwapTransactionStatusStore, @@ -118,30 +114,22 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( ifRight = { statusModel -> sendStatusUpdateAnalytics(statusModel, provider) - val accountId = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = userWallet.walletId, - currency = cryptoCurrency, - ) - .map { it.account.accountId } - .getOrNull() - } else { - null - } + val accountId = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWallet.walletId, + currency = cryptoCurrency, + ) + .map { it.account.accountId } + .getOrNull() - val refundTokenCurrency = if (accountsFeatureToggles.isFeatureEnabled) { - if (accountId != null) { - addRefundCurrencyIfNeededNew( - accountId = accountId, - status = statusModel, - type = provider.type, - ) - } else { - Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") - null - } + val refundTokenCurrency = if (accountId != null) { + addRefundCurrencyIfNeeded( + accountId = accountId, + status = statusModel, + type = provider.type, + ) } else { - addRefundCurrencyIfNeededLegacy(status = statusModel, type = provider.type) + Timber.e("Account ID is null, cannot add refund currency ${cryptoCurrency.id}") + null } swapTransactionRepository.storeTransactionState( @@ -171,28 +159,7 @@ internal class TokenDetailsExchangeStatusFactory @AssistedInject constructor( } } - /** - * For now do it only for dex-bridge provider - */ - private suspend fun addRefundCurrencyIfNeededLegacy( - status: ExchangeStatusModel?, - type: ExchangeProviderType, - ): CryptoCurrency? { - status ?: return null - if (type != ExchangeProviderType.DEX_BRIDGE) return null - val refundNetwork = status.refundNetwork - val refundContractAddress = status.refundContractAddress - if (refundNetwork != null && refundContractAddress != null) { - return addCryptoCurrenciesUseCase( - userWalletId = userWallet.walletId, - contractAddress = refundContractAddress, - networkId = refundNetwork, - ).getOrNull() - } - return null - } - - private suspend fun addRefundCurrencyIfNeededNew( + private suspend fun addRefundCurrencyIfNeeded( accountId: AccountId, status: ExchangeStatusModel?, type: ExchangeProviderType, From 692c6514fcac6804c0a24db8bd81c775842c5e9b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 15:28:32 +0200 Subject: [PATCH 57/97] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 15 +++++++-------- .../configurations/EnvironmentConfigGenerator.kt | 11 +++-------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 7e80091db4..353cb3f5ab 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -32,8 +32,6 @@ abstract class GenerateEnvironmentConfigTask : DefaultTask() { android { namespace = "com.tangem.datasource" - sourceSets["main"].java.srcDir(layout.buildDirectory.dir("generated/source/environment-config")) - room { schemaDirectory("$projectDir/schemas") } @@ -46,17 +44,18 @@ androidComponents { "app/src/main/assets/tangem-app-config/config_${buildType.environment}.json", ) - tasks.register( + val taskProvider = tasks.register( "generateEnvironmentConfig${variant.name.replaceFirstChar { it.uppercaseChar() }}", ) { this.configFile.set(configFile) - outputDir.set(layout.buildDirectory.dir("generated/source/environment-config")) + outputDir.set(layout.buildDirectory.dir("generated/source/environment-config/${variant.name}")) + doFirst { + logger.lifecycle("[Environment config] Running: ${this.name}") + } } - } -} -tasks.named("preBuild") { - dependsOn(tasks.matching { it.name.startsWith("generateEnvironmentConfig") }) + variant.sources.java?.addGeneratedSourceDirectory(taskProvider, GenerateEnvironmentConfigTask::outputDir) + } } tasks.withType().configureEach { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt index 89206fda87..5c6d9e6e79 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt @@ -73,14 +73,9 @@ object EnvironmentConfigGenerator { when { value.isString -> { val stringValue = value.content - val isNullable = stringValue.isEmpty() - val propertySpec = PropertySpec.builder(name, STRING.copy(nullable = isNullable)) - .initializer(if (isNullable) "null" else "%S", stringValue) - - // Add const modifier for non-nullable strings - if (!isNullable) { - propertySpec.addModifiers(KModifier.CONST) - } + val propertySpec = PropertySpec.builder(name, STRING) + .addModifiers(KModifier.CONST) + .initializer("%S", stringValue) builder.addProperty(propertySpec.build()) } From bba915b9ec295317c9b86d10fb2b54d7fe6a33ae Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 18:44:37 +0400 Subject: [PATCH 58/97] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + .../java/com/tangem/tap/TangemApplication.kt | 2 + .../customerio/CustomerIoAnalyticsClient.kt | 8 +++ .../customerio/CustomerIoAnalyticsHandler.kt | 50 +++++++++++++++++++ .../handlers/customerio/CustomerIoClient.kt | 43 ++++++++++++++++ .../customerio/CustomerIoLogClient.kt | 23 +++++++++ .../pushes/TangemPushNotificationService.kt | 5 ++ .../config/environment/EnvironmentConfig.kt | 1 + .../com/tangem/domain/common/LogConfig.kt | 1 + gradle/dependencies.toml | 3 ++ 10 files changed, 138 insertions(+) create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt create mode 100644 app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 53abf2183b..3c4c234aa7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -383,6 +383,8 @@ dependencies { implementation(deps.amplitude) implementation(deps.appsflyer) implementation(deps.appsflyer.oaid) + implementation(deps.customerio.analytics) + implementation(deps.customerio.messaging) implementation("com.android.installreferrer:installreferrer:2.2") implementation(deps.spongecastle.core) implementation(deps.lottie) diff --git a/app/src/main/java/com/tangem/tap/TangemApplication.kt b/app/src/main/java/com/tangem/tap/TangemApplication.kt index 7e7ecd4baa..8cf20b1b8a 100644 --- a/app/src/main/java/com/tangem/tap/TangemApplication.kt +++ b/app/src/main/java/com/tangem/tap/TangemApplication.kt @@ -66,6 +66,7 @@ import com.tangem.tap.common.analytics.handlers.BlockchainExceptionHandler import com.tangem.tap.common.analytics.handlers.amplitude.AmplitudeAnalyticsHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerAnalyticsHandler import com.tangem.tap.common.analytics.handlers.appsflyer.AppsFlyerClient +import com.tangem.tap.common.analytics.handlers.customerio.CustomerIoAnalyticsHandler import com.tangem.tap.common.analytics.handlers.firebase.FirebaseAnalyticsHandler import com.tangem.tap.common.images.createCoilImageLoader import com.tangem.tap.common.log.TangemAppLoggerInitializer @@ -410,6 +411,7 @@ open class TangemApplication : Application(), ImageLoaderFactory, Configuration. factory.addHandlerBuilder(AmplitudeAnalyticsHandler.Builder()) factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder()) factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder(appsFlyerClientFactory)) + factory.addHandlerBuilder(CustomerIoAnalyticsHandler.Builder()) factory.addFilter(oneTimeEventFilter) factory.addFilter(AppsFlyerEventFilter()) diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt new file mode 100644 index 0000000000..5c81ccfd73 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsClient.kt @@ -0,0 +1,8 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import com.tangem.core.analytics.api.UserIdHolder + +/** + * Client interface for Customer.io SDK operations. + */ +interface CustomerIoAnalyticsClient : UserIdHolder \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt new file mode 100644 index 0000000000..2c34c4518b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt @@ -0,0 +1,50 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import com.tangem.core.analytics.api.AnalyticsHandler +import com.tangem.core.analytics.api.AnalyticsUserIdHandler +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.tap.common.analytics.api.AnalyticsHandlerBuilder + +/** + * Customer.io analytics handler. + */ +class CustomerIoAnalyticsHandler( + private val client: CustomerIoAnalyticsClient, +) : AnalyticsHandler, AnalyticsUserIdHandler { + + override fun id(): String = ID + + override fun setUserId(userId: String) { + client.setUserId(userId) + } + + override fun clearUserId() { + client.clearUserId() + } + + override fun send(event: AnalyticsEvent) { + // No-op: product events are not sent to Customer.io. + // Triggers are configured to come from Amplitude directly. + } + + companion object { + const val ID = "CustomerIO" + } + + class Builder : AnalyticsHandlerBuilder { + override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? { + return if (data.logConfig.isCustomerIoLogEnabled) { + CustomerIoAnalyticsHandler(client = CustomerIoLogClient()) + } else if (data.config.customerIoCdpApiKey.isNotBlank()) { + CustomerIoAnalyticsHandler( + client = CustomerIoClient( + application = data.application, + cdpApiKey = data.config.customerIoCdpApiKey, + ), + ) + } else { + null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt new file mode 100644 index 0000000000..834244e2ec --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoClient.kt @@ -0,0 +1,43 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import android.app.Application +import io.customer.messagingpush.ModuleMessagingPushFCM +import io.customer.sdk.CustomerIO +import io.customer.sdk.CustomerIOBuilder +import timber.log.Timber + +/** + * Real Customer.io SDK client. + * + * Initializes the SDK with the given CDP API key and provides: + * - User identification (identify / clearIdentify) + * + * Auto-tracking of application lifecycle events is disabled since it is not needed. + * Auto-tracking of screen views is disabled. + */ +internal class CustomerIoClient( + application: Application, + cdpApiKey: String, +) : CustomerIoAnalyticsClient { + + init { + CustomerIOBuilder( + applicationContext = application, + cdpApiKey = cdpApiKey, + ) + .trackApplicationLifecycleEvents(false) + .autoTrackActivityScreens(false) + .addCustomerIOModule(ModuleMessagingPushFCM()) + .build() + + Timber.d("CustomerIO SDK initialized") + } + + override fun setUserId(userId: String) { + CustomerIO.instance().identify(userId = userId) + } + + override fun clearUserId() { + CustomerIO.instance().clearIdentify() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt new file mode 100644 index 0000000000..62a36d09f1 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoLogClient.kt @@ -0,0 +1,23 @@ +package com.tangem.tap.common.analytics.handlers.customerio + +import timber.log.Timber + +/** + * Log client for Customer.io (used in debug mode). + * + * Logs all operations to Timber instead of sending them to Customer.io. + */ +internal class CustomerIoLogClient : CustomerIoAnalyticsClient { + + private var userId: String? = null + + override fun setUserId(userId: String) { + this.userId = userId + Timber.tag(CustomerIoAnalyticsHandler.ID).d("identify: userId=$userId") + } + + override fun clearUserId() { + Timber.tag(CustomerIoAnalyticsHandler.ID).d("clearIdentify: previous userId=$userId") + this.userId = null + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt index 97026725ea..7c584a3bd6 100644 --- a/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt +++ b/app/src/main/java/com/tangem/tap/common/pushes/TangemPushNotificationService.kt @@ -3,6 +3,7 @@ package com.tangem.tap.common.pushes import android.annotation.SuppressLint import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage +import io.customer.messagingpush.CustomerIOFirebaseMessagingService import timber.log.Timber @SuppressLint("MissingFirebaseInstanceTokenRefresh") @@ -15,11 +16,15 @@ internal class TangemPushNotificationService : FirebaseMessagingService() { override fun onNewToken(token: String) { super.onNewToken(token) Timber.d("New FCM token received: $token") + + CustomerIOFirebaseMessagingService.onNewToken(applicationContext, token) } override fun onMessageReceived(message: RemoteMessage) { super.onMessageReceived(message) + CustomerIOFirebaseMessagingService.onMessageReceived(applicationContext, message) + val notification = message.notification ?: return val channelId = notification.channelId ?: TANGEM_CHANNEL_ID diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index 22545b5329..b8624c9ec8 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -28,4 +28,5 @@ data class EnvironmentConfig( val bffStaticTokenDev: String? = null, val gaslessTxApiKeyDev: String? = null, val gaslessTxApiKey: String? = null, + val customerIoCdpApiKey: String = "", ) \ No newline at end of file diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt index 1a8068cfb5..840894f1ad 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt @@ -17,4 +17,5 @@ object AnalyticsHandlersLogConfig { val isFirebaseLogEnabled: Boolean = BuildConfig.LOG_ENABLED val isAmplitudeLogEnabled: Boolean = BuildConfig.LOG_ENABLED val isAppsflyerLogEnabled: Boolean = BuildConfig.LOG_ENABLED + val isCustomerIoLogEnabled: Boolean = BuildConfig.LOG_ENABLED } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 68f64e61cf..7169e359a2 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -103,6 +103,7 @@ usedesk = "4.4.0" sumsub = "1.38.0" haze = "1.7.1" kotlinpoet = "1.18.1" +customerio = "4.6.3" # endregion Other libraries # region Tools @@ -313,4 +314,6 @@ usedesk-chat-gui = { module = "com.github.Usedesk.Android_SDK:chat-gui", version sumsub-sdk = { module = "com.sumsub.sns:idensic-mobile-sdk", version.ref = "sumsub" } haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } +customerio-analytics = { module = "io.customer.android:datapipelines", version.ref = "customerio" } +customerio-messaging = { module = "io.customer.android:messaging-push-fcm", version.ref = "customerio" } # endregion Other From c9c99ff6d34af76985ac91bfbd8ddf97daff8e26 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 18:48:03 +0300 Subject: [PATCH 59/97] Updated on 2026-08-14 --- .../com/tangem/scenarios/SwapScenarios.kt | 44 +++ .../screens/SwapChooseTokenPageObject.kt | 30 +- ...apSelectNetworkFeeBottomSheetPageObject.kt | 7 + .../com/tangem/screens/SwapTokenPageObject.kt | 59 +++- .../tests/swap/SwapChooseTokenScreenTest.kt | 182 ++++++++++ .../tangem/tests/swap/SwapTokenScreenTest.kt | 308 ++++++++++++++++- .../tests/swap/SwapTokenScreenWarningsTest.kt | 314 ++++++++++++++++++ .../ui/components/appbar/AppBarWithSearch.kt | 8 +- .../core/ui/test/AppBarWithSearchTestTags.kt | 6 + .../core/ui/test/SwapTokenScreenTestTags.kt | 2 + .../tangem/feature/swap/ui/TransactionCard.kt | 7 +- 11 files changed, 953 insertions(+), 14 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt index a86ca56854..cc2c466cb3 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/SwapScenarios.kt @@ -170,6 +170,45 @@ fun BaseTestCase.checkStoriesChanges() { } } +fun BaseTestCase.selectFeeType(feeType: FeeType, selectedFeeAmount: String) { + step("Click on 'Select fee' icon") { + onSwapTokenScreen { selectFeeIcon.performClick() } + } + + when (feeType) { + FeeType.Market -> { + step("Click on 'Market' item") { + onSwapSelectNetworkFeeBottomSheet { marketSelectorItem.performClick() } + } + step("Assert fee amount is equal to 'Market' fee:'$selectedFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) } + } + } + FeeType.Fast -> { + step("Click on 'Fast' item") { + onSwapSelectNetworkFeeBottomSheet { fastSelectorItem.performClick() } + } + step("Assert fee amount is equal to 'Fast' fee:'$selectedFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(selectedFeeAmount, substring = true) } + } + } + } +} + +fun BaseTestCase.chackUnableToCoverFeeNotification(networkName: String, currencySymbol: String) { + step("Assert 'Unable to cover '$networkName' fee notification title is displayed'") { + onSwapTokenScreen { unableToCoverFeeNotificationTitle(networkName).assertIsDisplayed() } + } + step("Assert 'Unable to cover '$networkName' fee notification text is displayed'") { + onSwapTokenScreen { + unableToCoverFeeNotificationText( + currencyName = networkName, + currencySymbol = currencySymbol + ).assertIsDisplayed() + } + } +} + sealed class SwapEntryPoint { object MainScreen : SwapEntryPoint() object TokenDetails : SwapEntryPoint() @@ -177,4 +216,9 @@ sealed class SwapEntryPoint { object TokenActionsBottomSheet : SwapEntryPoint() } +enum class FeeType { + Market, + Fast +} + diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt index e8e256017c..6da3d7a7f5 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapChooseTokenPageObject.kt @@ -3,7 +3,8 @@ 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.TokenElementsTestTags +import com.tangem.core.ui.test.AppBarWithSearchTestTags +import com.tangem.core.ui.test.BuyTokenScreenTestTags 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 @@ -21,11 +22,30 @@ class SwapChooseTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProv hasText(getResourceString(R.string.exchange_tokens_available_tokens_header)) } - fun tokenWithTitle(tokenTitle: String): KNode = child { - hasTestTag(TokenElementsTestTags.TOKEN_TITLE) - hasAnyDescendant(withText(tokenTitle)) - useUnmergedTree = true + val searchIcon: KNode = child { + hasTestTag(AppBarWithSearchTestTags.SEARCH_ICON) + } + + val searchTextField: KNode = child { + hasTestTag(AppBarWithSearchTestTags.TEXT_FIELD) + } + + val noTokensFoundText: KNode = child { + hasText(getResourceString(R.string.express_token_list_empty_search)) + } + + fun tokenWithTitle(tokenTitle: String, availableForSwap: Boolean = true): KNode = child { + hasTestTag(BuyTokenScreenTestTags.LAZY_LIST_ITEM) + hasAnyDescendant(withText(tokenTitle)) + if (!availableForSwap) { + hasAnyDescendant( + withText( + getResourceString(R.string.tokens_list_unavailable_to_swap_source_header) + ) + ) } + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapChooseTokenScreen(function: SwapChooseTokenPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt index 6705194521..007cfe289c 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapSelectNetworkFeeBottomSheetPageObject.kt @@ -2,6 +2,7 @@ package com.tangem.screens import androidx.compose.ui.test.SemanticsNodeInteractionsProvider import com.tangem.common.BaseTestCase +import com.tangem.core.ui.test.BaseButtonTestTags import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags import com.tangem.wallet.R import io.github.kakaocup.compose.node.element.ComposeScreen @@ -33,6 +34,12 @@ class SwapSelectNetworkFeeBottomSheetPageObject(semanticsProvider: SemanticsNode hasTestTag(SelectNetworkFeeBottomSheetTestTags.LEARN_MORE_TEXT) useUnmergedTree = true } + + val applyButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.common_apply)) + useUnmergedTree = true + } } internal fun BaseTestCase.onSwapSelectNetworkFeeBottomSheet(function: SwapSelectNetworkFeeBottomSheetPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt index 15be0dee77..2d705445e0 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/SwapTokenPageObject.kt @@ -43,6 +43,11 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + val feeAmount: KNode = child { + hasParent(withTestTag(FeeSelectorBlockTestTags.FEE_AMOUNT)) + useUnmergedTree = true + } + val receiveAmountShimmer: KNode = child { hasTestTag(SwapTokenScreenTestTags.RECEIVE_AMOUNT_SHIMMER) } @@ -71,6 +76,43 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + fun unableToCoverFeeNotificationTitle(networkName: String): KNode = child { + hasTestTag(NotificationTestTags.TITLE) + hasText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_title, + networkName + ) + ) + useUnmergedTree = true + } + + fun unableToCoverFeeNotificationText(currencyName: String, currencySymbol: String): KNode = child { + hasTestTag(NotificationTestTags.MESSAGE) + hasText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_description, + currencyName, + currencySymbol + ) + ) + useUnmergedTree = true + } + + fun unableToCoverFeeNotificationIcon(networkName: String): KNode = child { + hasTestTag(NotificationTestTags.ICON) + hasAnySibling(withTestTag(NotificationTestTags.TITLE)) + hasAnySibling( + withText( + getResourceString( + R.string.warning_express_not_enough_fee_for_token_tx_title, + networkName, + ) + ) + ) + useUnmergedTree = true + } + val refreshButton: KNode = child { hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.warning_button_refresh)) @@ -95,15 +137,30 @@ class SwapTokenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) useUnmergedTree = true } + val insufficientFundsErrorTitle: KNode = child { + hasTestTag(SendScreenTestTags.AMOUNT_CONTAINER_TITLE) + hasText(getResourceString(R.string.swapping_insufficient_funds)) + useUnmergedTree = true + } + val receiveFiatAmount: KNode = child { hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT) } + val receiveFiatAmountWithPriceImpactWarning: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING) + } + + val receiveFiatAmountInformationIcon: KNode = child { + hasTestTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON) + useUnmergedTree = true + } + val swapFiatAmount: KNode = child { hasTestTag(SwapTokenScreenTestTags.SWAP_FIAT_AMOUNT) } - val changeTokenIcon: KNode = child { + val selectTokenIcon: KNode = child { hasTestTag(SwapTokenScreenTestTags.SELECT_TOKEN_ICON) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt new file mode 100644 index 0000000000..9bf3c4f8a1 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapChooseTokenScreenTest.kt @@ -0,0 +1,182 @@ +package com.tangem.tests.swap + +import com.tangem.common.BaseTestCase +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.SwapEntryPoint +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.openSwapScreen +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SwapChooseTokenScreenTest : BaseTestCase() { + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8505") + @DisplayName("Swap: check available to swap tokens list") + @Test + fun checkAvailableToSwapTokensListTest() { + val tokenTitle = "Polygon" + val inputAmount = "100" + val ethereum = "Ethereum" + val polExMatic = "POL (ex-MATIC)" + val bitcoin = "Bitcoin" + val scenarioState = "CustomTokenAndJesusAdded" + val jesusCoin = "Jesus Coin" + val salam = "Salam" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Click on 'Select token' icon") { + onSwapTokenScreen { selectTokenIcon.performClick() } + } + step("Assert '$ethereum' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsDisplayed() } + } + step("Assert '$polExMatic' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() } + } + step("Assert '$bitcoin' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(bitcoin).assertIsNotDisplayed() } + } + step("Assert '$jesusCoin' is displayed and unavailable for swap") { + onSwapChooseTokenScreen { + tokenWithTitle( + tokenTitle = jesusCoin, + availableForSwap = false + ).assertIsDisplayed() + } + } + step("Assert custom token without backend id '$salam' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(salam).assertIsNotDisplayed() } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8506") + @DisplayName("Swap: check search on choose swap token screen") + @Test + fun checkSearchOnSwapChooseTokenScreenTest() { + val tokenTitle = "Polygon" + val inputAmount = "100" + val ethereum = "Ethereum" + val polExMatic = "POL (ex-MATIC)" + val polExMaticSymbol = "POL" + val invalidSearchText = "f" + val validSearchText = "pol" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Click on 'Select token' icon") { + onSwapTokenScreen { selectTokenIcon.performClick() } + } + step("Click on 'Search' icon") { + onSwapChooseTokenScreen { searchIcon.performClick() } + } + step("Click on 'Search' text field") { + onSwapChooseTokenScreen { searchTextField.performClick() } + } + step("Type invalid search text: '$invalidSearchText' in 'Search' text field") { + onSwapChooseTokenScreen { searchTextField.performTextReplacement(invalidSearchText) } + } + step("Assert '$ethereum' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() } + } + step("Assert '$polExMatic' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsNotDisplayed() } + } + step("Press 'Delete' button") { + device.uiDevice.pressDelete() + } + step("Type valid search text: '$validSearchText' in 'Search' text field") { + onSwapChooseTokenScreen { searchTextField.performTextReplacement(validSearchText) } + } + step("Assert '$ethereum' is not displayed") { + onSwapChooseTokenScreen { tokenWithTitle(ethereum).assertIsNotDisplayed() } + } + step("Assert '$polExMatic' is displayed") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).assertIsDisplayed() } + } + step("Select new receive token: $polExMatic") { + onSwapChooseTokenScreen { tokenWithTitle(polExMatic).performClick() } + } + step("Assert new receive token symbol: '$polExMaticSymbol' is displayed") { + onSwapTokenScreen { receiveTokenSymbol(polExMaticSymbol).assertIsDisplayed() } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt index 2048e0fa7e..e4112e9c00 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenTest.kt @@ -4,16 +4,16 @@ import androidx.compose.ui.test.hasText import com.tangem.common.BaseTestCase import com.tangem.common.annotations.ApiEnv import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_LONG import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState import com.tangem.common.utils.resetWireMockScenarios +import com.tangem.common.utils.setWireMockScenarioState import com.tangem.datasource.api.common.config.ApiConfig import com.tangem.datasource.api.common.config.ApiEnvironment -import com.tangem.scenarios.SwapEntryPoint -import com.tangem.scenarios.openMainScreen -import com.tangem.scenarios.openSwapScreen -import com.tangem.scenarios.synchronizeAddresses +import com.tangem.scenarios.* import com.tangem.screens.* import dagger.hilt.android.testing.HiltAndroidTest import io.qameta.allure.kotlin.AllureId @@ -266,6 +266,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("2828") @DisplayName("Swap: network fee") @Test @@ -317,6 +320,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("575") @DisplayName("Swap: check UI") @Test @@ -354,7 +360,7 @@ class SwapTokenScreenTest : BaseTestCase() { onSwapTokenScreen { receiveTokenSymbol(receiveTokenSymbol).assertIsDisplayed() } } step("Click on 'Select token' icon") { - onSwapTokenScreen { changeTokenIcon.performClick() } + onSwapTokenScreen { selectTokenIcon.performClick() } } step("Select new receive token: $newReceiveToken") { onSwapChooseTokenScreen { tokenWithTitle(newReceiveToken).performClick() } @@ -401,6 +407,9 @@ class SwapTokenScreenTest : BaseTestCase() { } } + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) @AllureId("5162") @DisplayName("Swap: check swap tokens switch") @Test @@ -444,4 +453,293 @@ class SwapTokenScreenTest : BaseTestCase() { } } } + + @AllureId("573") + @DisplayName("Swap: check 'Swap' button availability") + @Test + fun checkSwapButtonAvailabilityTest() { + val polygon = "Polygon" + val bitcoin = "Bitcoin" + val salam = "Salam" + val jesusCoin = "Jesus Coin" + val myria = "Myria" + val scenarioState = "CustomTokenAndJesusAdded" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$polygon'") { + onMainScreen { tokenWithTitleAndAddress(polygon).clickWithAssertion() } + } + step("Assert 'Swap' button is not dimmed. Swap available") { + onTokenDetailsScreen { swapButton().assertIsDimmed(false) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Click on token with name: '$bitcoin'. Swap unavailable") { + onMainScreen { tokenWithTitleAndAddress(bitcoin).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Click on unknown custom token with name: '$salam'. Swap unavailable") { + onMainScreen { tokenWithTitleAndAddress(salam).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Swipe up") { + onMainScreen { tokenWithTitleAndAddress(jesusCoin).assertIsDisplayed() } + waitForIdle() + swipeVertical(SwipeDirection.UP) + } + step("Click on token with name: '$jesusCoin' in 'Ethereum' network. Swap unavailable") { + waitForIdle() + onMainScreen { tokenWithTitleAndAddress(jesusCoin).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + step("Press 'Back' button") { + device.uiDevice.pressBack() + } + step("Click on custom token with 'exchangeAvailable=true': '$myria'. Swap unavailable") { + onMainScreen { tokenWithTitleAndAddress(myria).clickWithAssertion() } + } + step("Assert 'Swap' button is dimmed") { + onTokenDetailsScreen { swapButton().assertIsDimmed(true) } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("583") + @DisplayName("Swap: check switch fee type (enable to cover 'Market' and 'Fast' fee)") + @Test + fun enableToCoverMarketAndFastFeeTest() { + val tokenName = "Ethereum" + val inputAmount = "0.99" + val market = "Market" + val fast = "Fast" + val marketFeeAmount = "$1.12" + val fastFeeAmount = "$1.43" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Select '$market' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount) + } + } + step("Select '$fast' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount) + } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8536") + @DisplayName("Swap: check switch fee type (unable to cover 'Market' and 'Fast' fee)") + @Test + fun unableToCoverMarketAndFastFeeTest() { + val tokenName = "POL (ex-MATIC)" + val inputAmount = "0.0001" + val market = "Market" + val fast = "Fast" + val marketFeeAmount = "$18,932" + val fastFeeAmount = "$24,139" + val scenarioName = "eth_network_balance" + val scenarioState = "LessThanDollar" + val networkName = "Ethereum" + val currencySymbol = "ETH" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Select '$market' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + step("Select '$fast' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + } + } + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("8537") + @DisplayName("Swap: check switch fee type (unable to cover 'Fast' fee)") + @Test + fun unableToCoverFastFeeTest() { + val tokenName = "POL (ex-MATIC)" + val inputAmount = "3000" + val fastFeeType = "Fast" + val fastFeeAmount = "$2," + val marketFeeType = "Market" + val marketFeeAmount = "$1." + val scenarioName = "eth_fee_history" + val scenarioState = "UnableToCoverFastFee" + val networkName = "Ethereum" + val currencySymbol = "ETH" + + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(scenarioName) + } + ).run { + + step("Set WireMock scenario: '$scenarioName' to state: '$scenarioState'") { + setWireMockScenarioState(scenarioName = scenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenName'") { + onMainScreen { tokenWithTitleAndAddress(tokenName).clickWithAssertion() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Swap' button is enabled") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + onSwapTokenScreen { swapButton.assertIsEnabled() } + } + } + step("Select '$fastFeeType' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Fast, selectedFeeAmount = fastFeeAmount) + } + } + step("Assert fee amount is equal to '$fastFeeType' fee:'$fastFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(fastFeeAmount, substring = true) } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + step("Select '$marketFeeType' fee type") { + flakySafely(WAIT_UNTIL_TIMEOUT_LONG) { + selectFeeType(FeeType.Market, selectedFeeAmount = marketFeeAmount) + } + } + step("Assert fee amount is equal to '$marketFeeType' fee:'$marketFeeAmount'") { + onSwapTokenScreen { feeAmount.assertTextContains(marketFeeAmount, substring = true) } + } + step("Assert 'Swap' button is enabled") { + waitForIdle() + onSwapTokenScreen { swapButton.assertIsEnabled() } + } + } + } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt new file mode 100644 index 0000000000..4ba809b933 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/swap/SwapTokenScreenWarningsTest.kt @@ -0,0 +1,314 @@ +package com.tangem.tests.swap + +import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.* +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.common.utils.setWireMockScenarioState +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.SwapEntryPoint +import com.tangem.scenarios.chackUnableToCoverFeeNotification +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.openSwapScreen +import com.tangem.scenarios.synchronizeAddresses +import com.tangem.screens.* +import dagger.hilt.android.testing.HiltAndroidTest +import io.github.kakaocup.kakao.common.utilities.getResourceString +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Test + +@HiltAndroidTest +class SwapTokenScreenWarningsTest : BaseTestCase() { + + @ApiEnv( + ApiEnvConfig(ApiConfig.ID.Express, ApiEnvironment.PROD) + ) + @AllureId("580") + @DisplayName("Swap: check 'Insufficient funds' warning") + @Test + fun checkSwapInsufficientFundsWarningTest() { + val tokenTitle = "Polygon" + val inputAmount = "1000" + + setupHooks().run { + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert 'Insufficient funds' error is displayed") { + waitForIdle() + onSwapTokenScreen { insufficientFundsErrorTitle.assertIsDisplayed() } + } + } + } + + @AllureId("8502") + @DisplayName("Swap: check 'Unable to cover network fee' warning") + @Test + fun checkUnableToCoverBlockchainFeeWarningTest() { + val tokenTitle = "USDC" + val inputAmount = "1000" + val tokensScenarioState = "SolanaUSDC" + val balanceScenarioName = "solana_balance" + val balanceScenarioState = "Empty" + val networkName = "Solana" + val currencySymbol = "SOL" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(balanceScenarioState) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$balanceScenarioName' to state: $balanceScenarioState") { + setWireMockScenarioState(scenarioName = balanceScenarioName, state = balanceScenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Check 'Unable to cover '$networkName' fee notification") { + chackUnableToCoverFeeNotification(networkName = networkName, currencySymbol = currencySymbol) + } + step("Assert 'Unable to cover '$networkName' fee notification icon is displayed'") { + onSwapTokenScreen { unableToCoverFeeNotificationIcon(networkName).assertIsDisplayed() } + } + step("Assert 'Swap' button is disabled") { + onSwapTokenScreen { swapButton.assertIsNotEnabled() } + } + } + } + + @AllureId("8503") + @DisplayName("Swap: check 'High price impact' warning on CEX") + @Test + fun checkHighPriceImpactWarningCEXTest() { + val tokenTitle = "USDC" + val inputAmount = "100" + val currencySymbol = "SOL" + val slippagePercent = "5%" + val tokensScenarioState = "SolanaUSDC" + val exchangeQuoteScenarioName = "exchange_quote_solana" + val exchangeQuoteScenarioState = "HighPriceImpact" + val dialogTitle = getResourceString(R.string.swapping_alert_title) + val dialogText = getResourceString( + R.string.swapping_alert_cex_description_with_slippage, + currencySymbol, + slippagePercent + ) + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + resetWireMockScenarioState(exchangeQuoteScenarioName) + } + ).run { + + step("Set WireMock scenario: '$USER_TOKENS_API_SCENARIO' to state: '$tokensScenarioState'") { + setWireMockScenarioState(scenarioName = USER_TOKENS_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$QUOTES_API_SCENARIO' to state: $tokensScenarioState") { + setWireMockScenarioState(scenarioName = QUOTES_API_SCENARIO, state = tokensScenarioState) + } + step("Set WireMock scenario: '$exchangeQuoteScenarioName' to state: $exchangeQuoteScenarioState") { + setWireMockScenarioState( + scenarioName = exchangeQuoteScenarioName, + state = exchangeQuoteScenarioState + ) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert fiat amount with warning is displayed") { + onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) } + } + step("Assert receive amount information icon is displayed") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() } + } + step("Click on receive amount information icon") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() } + } + step("Assert information dialog is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert information dialog title is displayed") { + onDialog { title.assertTextEquals(dialogTitle) } + } + step("Assert information dialog text for CEX is displayed") { + onDialog { text.assertTextEquals(dialogText) } + } + step("Assert dialog 'OK' button is displayed") { + onDialog { okButton.assertIsDisplayed() } + } + } + } + + @AllureId("8504") + @DisplayName("Swap: check 'High price impact' warning on DEX") + @Test + fun checkHighPriceImpactWarningDEXTest() { + val tokenTitle = "Polygon" + val inputAmount = "1000" + val slippagePercent = "3.5%" + val dialogTitle = getResourceString(R.string.swapping_alert_title) + val highPriceImpactDescription = getResourceString(R.string.swapping_high_price_impact_description) + val swappingAlertDEXDescription = getResourceString(R.string.swapping_alert_dex_description) + val swappingAlertDEXDescriptionWithSlippage = getResourceString( + R.string.swapping_alert_dex_description_with_slippage, + slippagePercent + ) + val pairsToScenarioName = "polygon_pos_to_pairs" + val pairsFromScenarioName = "polygon_pos_from_pairs" + val scenarioState = "DexProvider" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(pairsToScenarioName) + resetWireMockScenarioState(pairsFromScenarioName) + } + ).run { + step("Set WireMock scenario: '$pairsToScenarioName' to state: $scenarioState") { + setWireMockScenarioState(scenarioName = pairsToScenarioName, state = scenarioState) + } + step("Set WireMock scenario: '$pairsFromScenarioName' to state: $scenarioState") { + setWireMockScenarioState(scenarioName = pairsFromScenarioName, state = scenarioState) + } + + step("Open 'Main Screen'") { + openMainScreen() + } + step("Synchronize addresses") { + synchronizeAddresses() + } + step("Click on token with name: '$tokenTitle'") { + onMainScreen { tokenWithTitleAndAddress(tokenTitle).clickWithAssertion() } + } + step("Assert title: '$tokenTitle' is displayed") { + onTokenDetailsScreen { title.assertIsDisplayed() } + } + step("Open 'Swap' screen") { + openSwapScreen(from = SwapEntryPoint.TokenDetails) + } + step("Assert 'You swap' block is displayed") { + onSwapTokenScreen { youSwapBlock.assertIsDisplayed() } + } + step("Input swap amount = '$inputAmount'") { + waitForIdle() + onSwapTokenScreen { + textInput.clickWithAssertion() + textInput.performTextReplacement(inputAmount) + } + } + step("Assert fiat amount with warning is displayed") { + onSwapTokenScreen { receiveFiatAmountWithPriceImpactWarning.assertTextContains("%", substring = true) } + } + step("Assert receive amount information icon is displayed") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.assertIsDisplayed() } + } + step("Click on receive amount information icon") { + onSwapTokenScreen { receiveFiatAmountInformationIcon.performClick() } + } + step("Assert information dialog is displayed") { + onDialog { dialogContainer.assertIsDisplayed() } + } + step("Assert information dialog title is displayed") { + onDialog { title.assertTextEquals(dialogTitle) } + } + step("Assert information dialog text for DEX is displayed") { + onDialog { + text.assertTextContains(highPriceImpactDescription, substring = true) + text.assertTextContains(swappingAlertDEXDescription, substring = true) + text.assertTextContains(swappingAlertDEXDescriptionWithSlippage, substring = true) + } + } + step("Assert dialog 'OK' button is displayed") { + onDialog { okButton.assertIsDisplayed() } + } + step("Click on 'OK' button") { + onDialog { okButton.performClick() } + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index f6f4dd18c0..32bf5c0667 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction @@ -30,6 +31,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.test.AppBarWithSearchTestTags /** * App bar with close icon and search functionality @@ -135,7 +137,8 @@ private fun CollapsedSearchView( contentDescription = null, modifier = Modifier .clickable { onExpandedChange(true) } - .padding(end = TangemTheme.dimens.spacing16), + .padding(end = TangemTheme.dimens.spacing16) + .testTag(AppBarWithSearchTestTags.SEARCH_ICON), ) } } @@ -210,7 +213,8 @@ private fun ExpandedSearchView( modifier = Modifier .fillMaxWidth() .focusRequester(textFieldFocusRequester) - .onFocusChanged { onFocusChange(it.hasFocus) }, + .onFocusChanged { onFocusChange(it.hasFocus) } + .testTag(AppBarWithSearchTestTags.TEXT_FIELD), placeholder = { Text(text = placeholderSearchText) }, diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt new file mode 100644 index 0000000000..d9b8583dcb --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/AppBarWithSearchTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object AppBarWithSearchTestTags { + const val SEARCH_ICON = "APP_BAR_WITH_SEARCH_SEARCH_ICON" + const val TEXT_FIELD = "APP_BAR_WITH_SEARCH_TEXT_FIELD" +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt index 3ddbefea69..9cdf82f3f3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/SwapTokenScreenTestTags.kt @@ -16,5 +16,7 @@ object SwapTokenScreenTestTags { const val TOKEN_ICON = "SWAP_TOKEN_SCREEN_TOKEN_ICON" const val SELECT_TOKEN_ICON = "SWAP_TOKEN_SCREEN_SELECT_TOKEN_ICON" const val RECEIVE_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT" + const val RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING = "SWAP_TOKEN_SCREEN_RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT" + const val RECEIVE_FIAT_AMOUNT_INFORMATION_ICON = "SWAP_TOKEN_SCREEN_PRICE_IMPACT_INFORMATION_ICON" const val SWAP_FIAT_AMOUNT = "SWAP_TOKEN_SCREEN_SWAP_FIAT_AMOUNT" } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 25aac0f046..b29e4b8257 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -328,6 +328,9 @@ private fun Content( ), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, + modifier = Modifier.testTag( + SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_WITH_PRICE_IMPACT_WARNING, + ), ) } else { AnimatedContent(targetState = amountEquivalent, label = "") { amount -> @@ -355,7 +358,9 @@ private fun Content( } else { TangemTheme.colors.text.tertiary }, - modifier = Modifier.align(Alignment.CenterVertically), + modifier = Modifier + .align(Alignment.CenterVertically) + .testTag(SwapTokenScreenTestTags.RECEIVE_FIAT_AMOUNT_INFORMATION_ICON), ) } } From 849c7f7a0340d4ea844935f26443efdddafe9b86 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 16:06:43 +0000 Subject: [PATCH 60/97] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index b1002f2997..6934faad61 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.34-1430" +tangemBlockchainSdk = "develop-1437" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-584" +tangemCardSdk = "develop-582" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From e047c491cf98eab609db3584a40d96663cafdec7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 12:16:41 +0400 Subject: [PATCH 61/97] Updated on 2026-08-14 --- .../approval/api/GiveApprovalComponent.kt | 5 +- features/approval/impl/build.gradle.kts | 3 + .../impl/DefaultGiveApprovalComponent.kt | 5 +- .../impl/DefaultGiveApprovalFeatureToggles.kt | 3 +- .../impl/di/GiveApprovalBindsModule.kt | 6 ++ .../approval/impl/model/GiveApprovalModel.kt | 56 ++++++++++++++++--- .../approval/impl/model/GiveApprovalUM.kt | 2 + features/swap/impl/build.gradle.kts | 1 + .../feature/swap/DefaultSwapComponent.kt | 46 +++++++++++++++ .../tangem/feature/swap/model/SwapModel.kt | 48 ++++++++++++++-- .../tangem/feature/swap/ui/StateBuilder.kt | 3 + 11 files changed, 158 insertions(+), 20 deletions(-) diff --git a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt index b680b9470b..e2345bbc2d 100644 --- a/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt +++ b/features/approval/api/src/main/java/com/tangem/features/approval/api/GiveApprovalComponent.kt @@ -4,12 +4,12 @@ import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletId interface GiveApprovalComponent : ComposableBottomSheetComponent { data class Params( - val userWallet: UserWallet, + val userWalletId: UserWalletId, val cryptoCurrencyStatus: CryptoCurrencyStatus, val feeCryptoCurrencyStatus: CryptoCurrencyStatus, val amount: String, @@ -19,6 +19,7 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent { ) interface Callback { + fun onApproveClick() fun onApproveDone() fun onApproveFailed() fun onCancelClick() diff --git a/features/approval/impl/build.gradle.kts b/features/approval/impl/build.gradle.kts index 01c43ad67f..e15b2662cd 100644 --- a/features/approval/impl/build.gradle.kts +++ b/features/approval/impl/build.gradle.kts @@ -22,6 +22,8 @@ dependencies { implementation(projects.core.navigation) implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) /** Common */ implementation(projects.common.ui) @@ -33,6 +35,7 @@ dependencies { /** Domain */ implementation(projects.domain.models) + implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) implementation(projects.domain.transaction.models) implementation(projects.domain.transaction) diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt index 51c2fe3986..c0102d54a6 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalComponent.kt @@ -4,7 +4,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.common.ui.userwallet.ext.walletInterationIcon import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel @@ -46,7 +45,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor( feeDisplaySource = FeeSelectorParams.FeeDisplaySource.Screen, analyticsCategoryName = CommonSendAnalyticEvents.APPROVE_CATEGORY, analyticsSendSource = CommonSendAnalyticEvents.CommonSendSource.Approve, - userWalletId = params.userWallet.walletId, + userWalletId = params.userWalletId, ), onResult = model::onFeeResult, ) @@ -84,7 +83,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor( approveType = uiState.approveType, approveItems = uiState.approveItems, onChangeApproveType = model::onChangeApproveType, - walletInteractionIcon = walletInterationIcon(params.userWallet), + walletInteractionIcon = uiState.walletInteractionIcon, isApproveEnabled = uiState.isApproveButtonEnabled, isApproveLoading = uiState.isApproveLoading, onApproveClick = model::onApproveClick, diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt index 07867cc6a6..b135009ed6 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/DefaultGiveApprovalFeatureToggles.kt @@ -2,8 +2,9 @@ package com.tangem.features.approval.impl import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.approval.api.GiveApprovalFeatureToggles +import javax.inject.Inject -internal class DefaultGiveApprovalFeatureToggles( +internal class DefaultGiveApprovalFeatureToggles @Inject constructor( private val featureToggles: FeatureTogglesManager, ) : GiveApprovalFeatureToggles { diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt index 94d432e9b9..3f6fdce55a 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/di/GiveApprovalBindsModule.kt @@ -3,7 +3,9 @@ package com.tangem.features.approval.impl.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalFeatureToggles import com.tangem.features.approval.impl.DefaultGiveApprovalComponent +import com.tangem.features.approval.impl.DefaultGiveApprovalFeatureToggles import com.tangem.features.approval.impl.model.GiveApprovalModel import dagger.Binds import dagger.Module @@ -17,6 +19,10 @@ import javax.inject.Singleton @Module internal interface GiveApprovalFeatureModule { + @Singleton + @Binds + fun bindGiveApprovalFeatureToggle(toggles: DefaultGiveApprovalFeatureToggles): GiveApprovalFeatureToggles + @Binds @Singleton fun bindComponentFactory(factory: DefaultGiveApprovalComponent.Factory): GiveApprovalComponent.Factory diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt index 2b3fb3fa8f..87882d24ce 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalModel.kt @@ -8,6 +8,9 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsParam +import com.tangem.core.analytics.models.Basic import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -28,6 +31,8 @@ import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.core.navigation.url.UrlOpener +import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -52,14 +57,23 @@ internal class GiveApprovalModel @Inject constructor( private val createAndSendGaslessTransactionUseCase: CreateAndSendGaslessTransactionUseCase, private val uiMessageSender: UiMessageSender, private val urlOpener: UrlOpener, + private val getUserWalletUseCase: GetUserWalletUseCase, + private val analyticsEventHandler: AnalyticsEventHandler, ) : Model(), FeeSelectorModelCallback { private val params: GiveApprovalComponent.Params = paramsContainer.require() + private val userWallet by lazy { + requireNotNull( + getUserWalletUseCase(params.userWalletId).getOrNull(), + ) { "No wallet found for id: $params.userWalletId" } + } + val uiState: StateFlow field = MutableStateFlow( GiveApprovalUM( approveType = ApproveType.LIMITED, + walletInteractionIcon = walletInterationIcon(userWallet), isApproveButtonEnabled = false, isApproveLoading = false, ), @@ -73,6 +87,7 @@ internal class GiveApprovalModel @Inject constructor( } fun onApproveClick() { + params.callback.onApproveClick() uiState.update { it.copy(isApproveLoading = true) } modelScope.launch(dispatchers.main) { val isSuccess = sendApprovalTransaction() @@ -113,7 +128,7 @@ internal class GiveApprovalModel @Inject constructor( return createApprovalTransactionUseCase( cryptoCurrencyStatus = cryptoCurrencyStatus, - userWalletId = params.userWallet.walletId, + userWalletId = params.userWalletId, amount = getApprovalAmount(), contractAddress = tokenCurrency.contractAddress, spenderAddress = params.spenderAddress, @@ -126,7 +141,7 @@ internal class GiveApprovalModel @Inject constructor( return getFeeUseCase( transactionData = approvalTransaction, - userWallet = params.userWallet, + userWallet = userWallet, network = params.cryptoCurrencyStatus.currency.network, ) } @@ -138,13 +153,13 @@ internal class GiveApprovalModel @Inject constructor( return if (maybeToken == null) { getFeeForGaslessUseCase( transactionData = approvalTransaction, - userWallet = params.userWallet, + userWallet = userWallet, network = params.cryptoCurrencyStatus.currency.network, ) } else { getFeeForTokenUseCase( transactionData = approvalTransaction, - userWallet = params.userWallet, + userWallet = userWallet, token = maybeToken.currency, ) } @@ -162,7 +177,7 @@ internal class GiveApprovalModel @Inject constructor( val transactionData = createApprovalTransactionUseCase( cryptoCurrencyStatus = cryptoCurrencyStatus, - userWalletId = params.userWallet.walletId, + userWalletId = params.userWalletId, amount = getApprovalAmount(), fee = selectedFee, contractAddress = tokenCurrency.contractAddress, @@ -174,14 +189,14 @@ internal class GiveApprovalModel @Inject constructor( return if (isFeeInTokenCurrency) { createAndSendGaslessTransactionUseCase( - userWallet = params.userWallet, + userWallet = userWallet, transactionData = transactionData, fee = feeExtended, ) } else { sendTransactionUseCase( txData = transactionData, - userWallet = params.userWallet, + userWallet = userWallet, network = tokenCurrency.network, ) }.fold( @@ -189,7 +204,32 @@ internal class GiveApprovalModel @Inject constructor( Timber.e("Failed to send approval transaction: $error") false }, - ifRight = { true }, + ifRight = { + sendApproveSuccessAnalytics(feeContent) + true + }, + ) + } + + private fun sendApproveSuccessAnalytics(feeContent: FeeSelectorUM.Content) { + val currency = params.cryptoCurrencyStatus.currency + val feeToken = feeContent.feeExtraInfo.feeCryptoCurrencyStatus.currency.symbol + val permissionType = when (uiState.value.approveType) { + ApproveType.LIMITED -> "Current transaction" + ApproveType.UNLIMITED -> "Unlimited" + } + val event = AnalyticsParam.TxSentFrom.Approve( + blockchain = currency.network.name, + token = currency.symbol, + feeType = feeContent.toAnalyticType(), + feeToken = feeToken, + permissionType = permissionType, + ) + analyticsEventHandler.send( + Basic.TransactionSent( + sentFrom = event, + memoType = Basic.TransactionSent.MemoType.Null, + ), ) } diff --git a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt index 83bf60054d..96fc768b88 100644 --- a/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt +++ b/features/approval/impl/src/main/java/com/tangem/features/approval/impl/model/GiveApprovalUM.kt @@ -1,5 +1,6 @@ package com.tangem.features.approval.impl.model +import androidx.annotation.DrawableRes import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -7,6 +8,7 @@ import kotlinx.collections.immutable.toImmutableList internal data class GiveApprovalUM( val approveType: ApproveType, val approveItems: ImmutableList = ApproveType.entries.toImmutableList(), + @DrawableRes val walletInteractionIcon: Int?, val isApproveButtonEnabled: Boolean, val isApproveLoading: Boolean, ) \ No newline at end of file diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 5b8d84540a..8a54dbba3d 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { /** Api */ implementation(projects.features.swap.api) implementation(projects.features.tokendetails.api) + implementation(projects.features.approval.api) /** Libs */ implementation(projects.libs.crypto) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index 6a88017cb4..b3b291e98f 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -12,11 +12,15 @@ import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.essenty.lifecycle.subscribe +import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState import com.tangem.common.ui.swapStoriesScreen.SwapStoriesScreen import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.R import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent @@ -26,6 +30,7 @@ import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.ui.SwapScreen import com.tangem.feature.swap.ui.SwapSelectTokenScreen import com.tangem.feature.swap.ui.SwapSuccessScreen +import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents @@ -43,6 +48,7 @@ internal class DefaultSwapComponent @AssistedInject constructor( private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory, private val sendFeatureToggles: SendFeatureToggles, private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, + private val giveApprovalComponentFactory: GiveApprovalComponent.Factory, ) : SwapComponent, AppComponentContext by appComponentContext { private val model: SwapModel = getOrCreateModel(params) @@ -55,6 +61,21 @@ internal class DefaultSwapComponent @AssistedInject constructor( childFactory = { configuration, context -> bottomSheetChild(context) }, ) + private val approvalSlot = childSlot( + key = APPROVAL_SLOT_KEY, + source = model.approvalSlotNavigation, + serializer = null, + handleBackButton = true, + childFactory = { _, factoryContext -> + val approvalParams = getApprovalParams() + ?: error("Approval params are not available") + giveApprovalComponentFactory.create( + context = childByContext(factoryContext), + params = approvalParams, + ) + }, + ) + init { lifecycle.subscribe( onStart = model::onStart, @@ -191,6 +212,9 @@ internal class DefaultSwapComponent @AssistedInject constructor( } bottomSheet.child?.instance?.BottomSheet() + + val approvalSlotState by approvalSlot.subscribeAsState() + approvalSlotState.child?.instance?.BottomSheet() } @Suppress("UnsafeCallOnNullableType") @@ -205,6 +229,27 @@ internal class DefaultSwapComponent @AssistedInject constructor( ) } + fun getApprovalParams(): GiveApprovalComponent.Params? { + val permissionState = model.uiState.permissionState as? GiveTxPermissionState.ReadyForRequest + ?: return null + val fromCryptoCurrency = model.dataState.fromCryptoCurrency ?: return null + val feeCryptoCurrency = model.dataState.feePaidCryptoCurrency ?: return null + val providerName = model.dataState.selectedProvider?.name.orEmpty() + + return GiveApprovalComponent.Params( + userWalletId = params.userWalletId, + cryptoCurrencyStatus = fromCryptoCurrency, + feeCryptoCurrencyStatus = feeCryptoCurrency, + amount = permissionState.amount, + spenderAddress = requireNotNull(model.dataState.approveDataModel).spenderAddress, + subtitle = resourceReference( + id = R.string.give_permission_swap_subtitle, + formatArgs = wrappedList(providerName, permissionState.currency), + ), + callback = model.approvalCallback, + ) + } + private fun toBigDecimalOrZero(bigDecimalString: String?): BigDecimal { return bigDecimalString?.replace(",", ".")?.toBigDecimalOrNull() ?: BigDecimal.ZERO } @@ -217,5 +262,6 @@ internal class DefaultSwapComponent @AssistedInject constructor( private companion object { const val BOTTOM_SHEET_SLOT_KEY = "bottomSheetSlot" const val FEE_SELECTOR_SLOT_KEY = "feeSelectorSlot" + const val APPROVAL_SLOT_KEY = "approvalSlot" } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 20ebd990d5..3eb20abc33 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -14,6 +14,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalFeatureToggles import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -31,9 +33,9 @@ import com.tangem.core.ui.R import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.toWrappedList -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.utils.InputNumberFormatter @@ -118,6 +120,8 @@ import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.delay import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -173,6 +177,7 @@ internal class SwapModel @Inject constructor( private val appsFlyerStore: AppsFlyerStore, private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, private val messageSender: UiMessageSender, + giveApprovalFeatureToggles: GiveApprovalFeatureToggles, ) : Model() { private val params = paramsContainer.require() @@ -301,6 +306,33 @@ internal class SwapModel @Inject constructor( get() = swapRouter.currentScreen val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val approvalSlotNavigation = SlotNavigation() + private val shouldUseGaslessApproval: Boolean = giveApprovalFeatureToggles.isGaslessApprovalEnabled + + val approvalCallback = object : GiveApprovalComponent.Callback { + override fun onApproveClick() { + sendPermissionApproveClickedEvent() + } + + override fun onApproveDone() { + approvalSlotNavigation.dismiss() + updateWalletBalance() + uiState = stateBuilder.loadingPermissionState(uiState) + startLoadingQuotesFromLastState(isSilent = true) + } + + override fun onApproveFailed() { + approvalSlotNavigation.dismiss() + showAlert() + } + + override fun onCancelClick() { + approvalSlotNavigation.dismiss() + startLoadingQuotesFromLastState(isSilent = true) + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) + } + } + val addToPortfolioCallback = object : AddToPortfolioComponent.Callback { override fun onDismiss() = bottomSheetNavigation.dismiss() @@ -1224,7 +1256,7 @@ internal class SwapModel @Inject constructor( fromTokenStatus = fromCryptoCurrency, approveType = approveType, txFee = feeForPermission, - spenderAddress = approveDataModel.spenderAddress, + spenderAddress = requireNotNull(dataState.approveDataModel).spenderAddress, ), ) }.onSuccess { swapTransactionState -> @@ -1785,10 +1817,14 @@ internal class SwapModel @Inject constructor( openPermissionBottomSheet = { singleTaskScheduler.cancelTask() sendGivePermissionClickedEvent() - uiState = stateBuilder.showPermissionBottomSheet(uiState) { - startLoadingQuotesFromLastState(isSilent = true) - analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) - uiState = stateBuilder.dismissBottomSheet(uiState) + if (shouldUseGaslessApproval) { + approvalSlotNavigation.activate(Unit) + } else { + uiState = stateBuilder.showPermissionBottomSheet(uiState) { + startLoadingQuotesFromLastState(isSilent = true) + analyticsEventHandler.send(SwapEvents.ButtonPermissionCancelClicked()) + uiState = stateBuilder.dismissBottomSheet(uiState) + } } }, onAmountSelected = { onAmountSelected(it) }, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 2a61151dde..3b0249e51b 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -711,6 +711,9 @@ internal class StateBuilder( fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, + notifications = uiState.notifications + .filterNot { it is SwapNotificationUM.Info.PermissionNeeded } + .toImmutableList(), ) } From 7d13821e3c0f398282b963d227a2deed0f9344a0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 13:17:08 +0500 Subject: [PATCH 62/97] Updated on 2026-08-14 --- .../common/ui/notifications/Notifications.kt | 4 +- .../domain/GetMultiWalletWarningsFactory.kt | 1 + .../domain/GetSingleWalletWarningsFactory.kt | 1 + .../GetWalletNotificationsCarouselFactory.kt | 8 ++- ...ry.kt => GetWalletNotificationsFactory.kt} | 11 ++-- .../loaders/WalletContentLoaderFactory.kt | 22 +++++-- .../implementors/MultiWalletContentLoader.kt | 9 ++- .../implementors/SingleWalletContentLoader.kt | 29 ++++----- .../SingleWalletContentLoaderLegacy.kt | 35 ++++++++++ .../SingleWalletWithTokenContentLoader.kt | 7 +- .../wallet/state/model/WalletActionButtons.kt | 64 +++++++++++++++++++ .../transformers/AddWalletTransformer.kt | 1 + .../transformers/DeleteWalletTransformer.kt | 27 +++++--- .../InitializeWalletsTransformer.kt | 60 +++++++++++++++++ .../ReinitializeNewWalletTransformer.kt | 7 ++ .../ReinitializeWalletTransformer.kt | 4 +- .../transformers/RenameWalletsTransformer.kt | 26 +++++++- .../SetPrimaryCurrencyTransformer.kt | 3 +- .../SetRefreshStateTransformer.kt | 24 +++++-- .../SetTokenListErrorTransformer.kt | 20 ++++++ .../transformers/SetTokenListTransformer.kt | 15 +++++ .../transformers/UnlockWalletTransformer.kt | 26 ++++++++ .../MultiWalletBalanceUMTransformer.kt | 57 +++++++++++++++++ .../state/utils/MultiWalletActionsExt.kt | 10 +++ .../state/utils/UserWalletConverterExt.kt | 4 ++ .../state/utils/WalletLoadingStateFactory.kt | 56 ++++++++++++++++ .../BasicSingleWalletSubscriber.kt | 1 + .../MultiWalletWarningsSubscriber.kt | 1 + .../subscribers/PrimaryCurrencySubscriber.kt | 1 + .../SingleWalletButtonsSubscriber.kt | 1 + .../SingleWalletExpressStatusesSubscriber.kt | 1 + .../SingleWalletNotificationsSubscriber.kt | 1 + .../subscribers/SingleWalletSubscriber.kt | 35 ++++++++++ ... SingleWalletWithTokenSubscriberLegacy.kt} | 5 +- ...criber.kt => TxHistorySubscriberLegacy.kt} | 5 +- ...V2.kt => WalletNotificationsSubscriber.kt} | 18 ++++-- 36 files changed, 541 insertions(+), 59 deletions(-) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/{GetWalletWarningsFactory.kt => GetWalletNotificationsFactory.kt} (97%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderLegacy.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletSubscriber.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/{SingleWalletWithTokenSubscriber.kt => SingleWalletWithTokenSubscriberLegacy.kt} (88%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/{TxHistorySubscriber.kt => TxHistorySubscriberLegacy.kt} (96%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/{MultiWalletWarningsSubscriberV2.kt => WalletNotificationsSubscriber.kt} (85%) diff --git a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt index 39c4d36faa..161788fcb3 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/notifications/Notifications.kt @@ -138,7 +138,7 @@ fun LazyListScope.notifications( * @param containerColor Color to be used for the background of the notifications. * @param modifier Optional Modifier for the notifications. */ -fun LazyListScope.stackedNotifications( +fun LazyListScope.notificationsCarousel( notifications: ImmutableList?, containerColor: Color, modifier: Modifier = Modifier, @@ -192,7 +192,7 @@ private fun StackedNotifications_Preview( .background(contentColor) .padding(16.dp), ) { - stackedNotifications( + notificationsCarousel( notifications = params, containerColor = contentColor, ) 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 d08cbc8cb1..2438cafa5a 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 @@ -48,6 +48,7 @@ import kotlinx.coroutines.flow.map import javax.inject.Inject +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @Suppress("LongParameterList", "LargeClass") @ModelScoped internal class GetMultiWalletWarningsFactory @Inject constructor( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt index 30c506aa5e..de33b6fb9a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetSingleWalletWarningsFactory.kt @@ -25,6 +25,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import javax.inject.Inject +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @ModelScoped @Suppress("LongParameterList") internal class GetSingleWalletWarningsFactory @Inject constructor( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt index 8e0d92d545..0988254408 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsCarouselFactory.kt @@ -5,6 +5,7 @@ import com.tangem.common.ui.notifications.NotificationId import com.tangem.core.decompose.di.ModelScoped import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.promo.ShouldShowPromoWalletUseCase import com.tangem.domain.promo.models.PromoId @@ -48,8 +49,11 @@ internal class GetWalletNotificationsCarouselFactory @Inject constructor( buildList { addNoteMigrationNotification(userWallet, wallets, clickIntents) addRateAppNotification(showRateAppPromo, clickIntents) - addOnePlusOnePromoNotification(clickIntents, showOnePlusOnePromo) - addYieldPromoNotification(clickIntents, showYieldPromo) + + if (userWallet.isMultiCurrency) { + addOnePlusOnePromoNotification(clickIntents, showOnePlusOnePromo) + addYieldPromoNotification(clickIntents, showYieldPromo) + } addPushNotification( shouldShow = showPushesNotification, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt similarity index 97% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt index c900381523..8735cf42f4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetWalletNotificationsFactory.kt @@ -13,6 +13,7 @@ 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.wallet.UserWallet +import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase @@ -35,7 +36,7 @@ import javax.inject.Inject */ @Suppress("LongParameterList") @ModelScoped -internal class GetWalletWarningsFactory @Inject constructor( +internal class GetWalletNotificationsFactory @Inject constructor( private val isDemoCardUseCase: IsDemoCardUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val backupValidator: BackupValidator, @@ -194,7 +195,7 @@ internal class GetWalletWarningsFactory @Inject constructor( condition = flattenCurrencies.hasUnreachableNetworks(), ) - addCloreMigrationNotification(flattenCurrencies, clickIntents) + addCloreMigrationNotification(userWallet, flattenCurrencies, clickIntents) addNoAccountWarning(cryptoCurrencyStatus = flattenCurrencies.firstOrNull()) @@ -220,13 +221,15 @@ internal class GetWalletWarningsFactory @Inject constructor( } private fun MutableList.addCloreMigrationNotification( + userWallet: UserWallet, flattenCurrencies: List, clickIntents: WalletClickIntents, ) { val cloreCurrency = flattenCurrencies.findCloreCurrency() ?: return - add( - WalletNotificationUM.CloreMigration( + addIf( + condition = userWallet.isMultiCurrency, + element = WalletNotificationUM.CloreMigration( onStartMigrationClick = { clickIntents.onCloreMigrationClick(cloreCurrency) }, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt index 2b4ca41b34..bfac80ba45 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/WalletContentLoaderFactory.kt @@ -1,13 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.loaders import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.MultiWalletContentLoader -import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletContentLoader -import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.SingleWalletWithTokenContentLoader -import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.WalletContentLoader +import com.tangem.feature.wallet.presentation.wallet.loaders.implementors.* import javax.inject.Inject @Suppress("LongParameterList") @@ -15,7 +13,9 @@ import javax.inject.Inject internal class WalletContentLoaderFactory @Inject constructor( private val multiWalletContentLoaderFactory: MultiWalletContentLoader.Factory, private val singleWalletWithTokenContentLoaderFactory: SingleWalletWithTokenContentLoader.Factory, - private val singleWalletContentLoaderFactory: SingleWalletContentLoader.Factory, + private val singleWalletContentLoaderLegacyFactory: SingleWalletContentLoaderLegacy.Factory, + private val singleWalletContentLoader: SingleWalletContentLoader.Factory, + private val designFeatureToggles: DesignFeatureToggles, ) { fun create(userWallet: UserWallet, isRefresh: Boolean = false): WalletContentLoader? { @@ -24,10 +24,18 @@ internal class WalletContentLoaderFactory @Inject constructor( multiWalletContentLoaderFactory.create(userWallet) } userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() -> { - singleWalletWithTokenContentLoaderFactory.create(userWallet) + if (designFeatureToggles.isRedesignEnabled) { + singleWalletContentLoader.create(userWallet) + } else { + singleWalletWithTokenContentLoaderFactory.create(userWallet) + } } userWallet is UserWallet.Cold && !userWallet.isMultiCurrency -> { - singleWalletContentLoaderFactory.create(userWallet, isRefresh) + if (designFeatureToggles.isRedesignEnabled) { + singleWalletContentLoader.create(userWallet) + } else { + singleWalletContentLoaderLegacyFactory.create(userWallet, isRefresh) + } } else -> null } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 98fdd2bf6d..8abda2b315 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors +import com.tangem.core.ui.DesignFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.subscribers.* import dagger.assisted.Assisted @@ -13,15 +14,21 @@ internal class MultiWalletContentLoader @AssistedInject constructor( private val walletNFTListSubscriberFactory: WalletNFTListSubscriberV2.Factory, private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, + private val walletNotificationsSubscriberFactory: WalletNotificationsSubscriber.Factory, private val multiWalletActionButtonsSubscriberFactory: MultiWalletActionButtonsSubscriber.Factory, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, + private val designFeatureToggles: DesignFeatureToggles, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List = listOf( accountListSubscriberFactory.create(userWallet), walletNFTListSubscriberFactory.create(userWallet), checkWalletWithFundsSubscriberFactory.create(userWallet), - multiWalletWarningsSubscriberFactory.create(userWallet), + if (designFeatureToggles.isRedesignEnabled) { + walletNotificationsSubscriberFactory.create(userWallet) + } else { + multiWalletWarningsSubscriberFactory.create(userWallet) + }, multiWalletActionButtonsSubscriberFactory.create(userWallet), tangemPayMainSubscriberFactory.create(userWallet), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt index ae08b51df3..22f51e58d5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoader.kt @@ -1,34 +1,33 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.models.wallet.UserWallet -import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import com.tangem.feature.wallet.presentation.wallet.subscribers.CheckWalletWithFundsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletNotificationsSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -@Suppress("LongParameterList") +/** + * This content loader is used for the wallet screen when for single wallets. + * For example - [Note, Twins, Single with token] + */ internal class SingleWalletContentLoader @AssistedInject constructor( @Assisted private val userWallet: UserWallet.Cold, - @Assisted private val isRefresh: Boolean, - private val primaryCurrencySubscriberFactory: PrimaryCurrencySubscriber.Factory, - private val singleWalletButtonsSubscriberFactory: SingleWalletButtonsSubscriber.Factory, - private val singleWalletNotificationsSubscriberFactory: SingleWalletNotificationsSubscriber.Factory, - private val singleWalletExpressStatusesSubscriberFactory: SingleWalletExpressStatusesSubscriber.Factory, - private val txHistorySubscriberFactory: TxHistorySubscriber.Factory, + private val walletNotificationsSubscriber: WalletNotificationsSubscriber.Factory, + private val singleWalletSubscriber: SingleWalletSubscriber.Factory, private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List = listOf( - primaryCurrencySubscriberFactory.create(userWallet), - singleWalletButtonsSubscriberFactory.create(userWallet), - singleWalletNotificationsSubscriberFactory.create(userWallet), - singleWalletExpressStatusesSubscriberFactory.create(userWallet), - txHistorySubscriberFactory.create(userWallet, isRefresh), - checkWalletWithFundsSubscriberFactory.create(userWallet), + singleWalletSubscriber.create(userWallet = userWallet), + walletNotificationsSubscriber.create(userWallet = userWallet), + checkWalletWithFundsSubscriberFactory.create(userWallet = userWallet), ) @AssistedFactory interface Factory { - fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoader + fun create(userWallet: UserWallet.Cold): SingleWalletContentLoader } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderLegacy.kt new file mode 100644 index 0000000000..27228fe3bc --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletContentLoaderLegacy.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.presentation.wallet.loaders.implementors + +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.presentation.wallet.subscribers.* +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +@Suppress("LongParameterList") +internal class SingleWalletContentLoaderLegacy @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Cold, + @Assisted private val isRefresh: Boolean, + private val primaryCurrencySubscriberFactory: PrimaryCurrencySubscriber.Factory, + private val singleWalletButtonsSubscriberFactory: SingleWalletButtonsSubscriber.Factory, + private val singleWalletNotificationsSubscriberFactory: SingleWalletNotificationsSubscriber.Factory, + private val singleWalletExpressStatusesSubscriberFactory: SingleWalletExpressStatusesSubscriber.Factory, + private val txHistorySubscriberLegacyFactory: TxHistorySubscriberLegacy.Factory, + private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, +) : WalletContentLoader(id = userWallet.walletId) { + + override fun create(): List = listOf( + primaryCurrencySubscriberFactory.create(userWallet), + singleWalletButtonsSubscriberFactory.create(userWallet), + singleWalletNotificationsSubscriberFactory.create(userWallet), + singleWalletExpressStatusesSubscriberFactory.create(userWallet), + txHistorySubscriberLegacyFactory.create(userWallet, isRefresh), + checkWalletWithFundsSubscriberFactory.create(userWallet), + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): SingleWalletContentLoaderLegacy + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt index 81720d5bd6..a0d3b6344f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/SingleWalletWithTokenContentLoader.kt @@ -3,21 +3,22 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.subscribers.CheckWalletWithFundsSubscriber import com.tangem.feature.wallet.presentation.wallet.subscribers.MultiWalletWarningsSubscriber -import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenSubscriber +import com.tangem.feature.wallet.presentation.wallet.subscribers.SingleWalletWithTokenSubscriberLegacy import com.tangem.feature.wallet.presentation.wallet.subscribers.WalletSubscriber import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class SingleWalletWithTokenContentLoader @AssistedInject constructor( @Assisted private val userWallet: UserWallet.Cold, - private val singleWalletWithTokenSubscriberFactory: SingleWalletWithTokenSubscriber.Factory, + private val singleWalletWithTokenSubscriberLegacyFactory: SingleWalletWithTokenSubscriberLegacy.Factory, private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, ) : WalletContentLoader(id = userWallet.walletId) { override fun create(): List = listOf( - singleWalletWithTokenSubscriberFactory.create(userWallet), + singleWalletWithTokenSubscriberLegacyFactory.create(userWallet), multiWalletWarningsSubscriberFactory.create(userWallet), checkWalletWithFundsSubscriberFactory.create(userWallet), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt new file mode 100644 index 0000000000..512f16d709 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt @@ -0,0 +1,64 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonState +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.feature.wallet.impl.R + +/** + * Model for action buttons on the wallet card. It contains the button's text, icon, click listener, and enabled state. + */ +@Immutable +internal sealed class WalletActionButtons( + private val text: TextReference, + @DrawableRes private val iconRes: Int, +) { + + abstract val onClick: () -> Unit + + abstract val isEnabled: Boolean + + val buttonUM: TangemButtonUM + get() = TangemButtonUM( + text = text, + iconRes = iconRes, + type = TangemButtonType.Secondary, + shape = TangemButtonShape.Rounded, + onClick = onClick, + isEnabled = isEnabled, + state = if (isEnabled) { + TangemButtonState.Default + } else { + TangemButtonState.Disabled + }, + ) + + data class Buy( + override val onClick: () -> Unit, + override val isEnabled: Boolean, + ) : WalletActionButtons( + text = resourceReference(R.string.common_buy), + iconRes = R.drawable.ic_plus_default_24, + ) + + data class Swap( + override val onClick: () -> Unit, + override val isEnabled: Boolean, + ) : WalletActionButtons( + text = resourceReference(R.string.common_swap), + iconRes = R.drawable.ic_exchange_default_24, + ) + + data class Sell( + override val onClick: () -> Unit, + override val isEnabled: Boolean, + ) : WalletActionButtons( + text = resourceReference(R.string.common_sell), + iconRes = R.drawable.ic_dollar_default_24, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index e120fec1a1..69574485b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -23,6 +23,7 @@ internal class AddWalletTransformer( override fun transform(prevState: WalletScreenState): WalletScreenState { return prevState.copy( wallets = (prevState.wallets + walletLoadingStateFactory.create(userWallet)).toImmutableList(), + wallets2 = (prevState.wallets2 + walletLoadingStateFactory.create2(userWallet)).toImmutableList(), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt index 3b6aeb35c8..bf2960f3d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/DeleteWalletTransformer.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList import timber.log.Timber @@ -13,19 +14,29 @@ internal class DeleteWalletTransformer( override fun transform(prevState: WalletScreenState): WalletScreenState { val deletedWalletState = prevState.getDeletedWalletState() + val deletedWalletUM = prevState.getDeletedWalletState2() - if (deletedWalletState == null) { - Timber.e("Wallets does not contain deleted wallet") - return prevState + return when { + deletedWalletUM != null -> prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets2 = (prevState.wallets2 - deletedWalletUM).toImmutableList(), + ) + deletedWalletState != null -> prevState.copy( + selectedWalletIndex = selectedWalletIndex, + wallets = (prevState.wallets - deletedWalletState).toImmutableList(), + ) + else -> { + Timber.e("Wallets does not contain deleted wallet") + prevState + } } - - return prevState.copy( - selectedWalletIndex = selectedWalletIndex, - wallets = (prevState.wallets - deletedWalletState).toImmutableList(), - ) } private fun WalletScreenState.getDeletedWalletState(): WalletState? { return wallets.firstOrNull { it.walletCardState.id == deletedWalletId } } + + private fun WalletScreenState.getDeletedWalletState2(): WalletUM? { + return wallets2.firstOrNull { it.walletsBalanceUM.id == deletedWalletId } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index b7594959d3..8fe34bed8e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked @@ -9,9 +10,12 @@ import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import com.tangem.feature.wallet.presentation.wallet.state.utils.createStateByWalletType +import com.tangem.feature.wallet.presentation.wallet.state.utils.isSingleWallet +import com.tangem.utils.extensions.addIf import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList internal class InitializeWalletsTransformer( private val selectedWalletIndex: Int, @@ -42,6 +46,9 @@ internal class InitializeWalletsTransformer( } } .toImmutableList(), + wallets2 = wallets + .map(::createInitState) + .toImmutableList(), onWalletChange = clickIntents::onWalletChange, onDismissMarketsTooltip = clickIntents::onDismissMarketsTooltip, ) @@ -79,6 +86,16 @@ internal class InitializeWalletsTransformer( ) } + private fun createInitState(userWallet: UserWallet): WalletUM { + return if (userWallet.isLocked) { + userWallet.toLockedWalletUM() + } else { + walletLoadingStateFactory.create2( + userWallet = userWallet, + ) + } + } + private fun UserWallet.toLockedWalletCardState(): WalletCardState { return WalletCardState.LockedContent( id = walletId, @@ -89,6 +106,25 @@ internal class InitializeWalletsTransformer( ) } + private fun UserWallet.toLockedWalletUM(): WalletUM.Locked { + return WalletUM.Locked( + walletsBalanceUM = WalletBalanceUM.Loading( + id = walletId, + name = name, + ), + buttons = createWalletActions(userWallet = this), + type = when (this) { + is UserWallet.Cold -> WalletType.Cold + is UserWallet.Hot -> WalletType.Hot + }, + notifications = persistentListOf( + WalletNotificationUM.UnlockWallets( + onClick = clickIntents::onOpenUnlockWalletsBottomSheetClick, + ), + ), + ) + } + private fun createMultiWalletEnabledButtons(userWallet: UserWallet): PersistentList { val isSingleWalletWithToken = userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() @@ -109,4 +145,28 @@ internal class InitializeWalletsTransformer( WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}), ) } + + private fun createWalletActions(userWallet: UserWallet): PersistentList { + return buildList { + add( + WalletActionButtons.Buy( + isEnabled = false, + onClick = {}, + ).buttonUM, + ) + addIf( + condition = !userWallet.isSingleWallet(), + element = WalletActionButtons.Swap( + isEnabled = false, + onClick = {}, + ).buttonUM, + ) + add( + WalletActionButtons.Sell( + isEnabled = false, + onClick = {}, + ).buttonUM, + ) + }.toPersistentList() + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt index d6072f30f7..59a5358db8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -41,6 +41,13 @@ internal class ReinitializeNewWalletTransformer( ), ) .toImmutableList(), + wallets2 = prevState.wallets2 + .filterNot { it.walletsBalanceUM.id == prevWalletId } + .plus( + element = walletLoadingStateFactory.create2( + userWallet = newUserWallet, + ), + ).toImmutableList(), ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index bcfae039a0..fc2ade623b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -28,7 +28,9 @@ internal class ReinitializeWalletTransformer( } override fun transform(walletUM: WalletUM): WalletUM { - return walletUM // todo redesign main + return walletLoadingStateFactory.create2( + userWallet = userWallet, + ) } override fun transform(prevState: WalletState): WalletState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt index a87c58fcc2..af3f38b3a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/RenameWalletsTransformer.kt @@ -3,7 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import timber.log.Timber /** @@ -25,8 +27,16 @@ internal class RenameWalletsTransformer( } else { walletState } - } - .toImmutableList(), + }.toImmutableList(), + wallets2 = prevState.wallets2.map { walletUM -> + val renamedWallet = renamedWallets.firstOrNull { it.walletId == walletUM.walletsBalanceUM.id } + + if (renamedWallet != null) { + transform(prevState = walletUM, newName = renamedWallet.name) + } else { + walletUM + } + }.toPersistentList(), ) } @@ -46,4 +56,16 @@ internal class RenameWalletsTransformer( } } } + + private fun transform(prevState: WalletUM, newName: String): WalletUM { + return when (prevState) { + is WalletUM.Content -> { + prevState.copy(walletsBalanceUM = prevState.walletsBalanceUM.copySealed(name = newName)) + } + is WalletUM.Locked -> { + Timber.e("Impossible to rename wallet in locked state") + prevState + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt index ab69b7a4ce..5efac70c6d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetPrimaryCurrencyTransformer.kt @@ -11,6 +11,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.SingleWalletMarketPriceConverter import timber.log.Timber +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class SetPrimaryCurrencyTransformer( private val userWallet: UserWallet, private val status: CryptoCurrencyStatus, @@ -37,7 +38,7 @@ internal class SetPrimaryCurrencyTransformer( } override fun transform(walletUM: WalletUM): WalletUM { - return walletUM // todo redesign main + return walletUM // It will not be used } private fun WalletCardState.toLoadedSingleCurrencyState(): WalletCardState { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 985e877682..504e4192e7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -2,9 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton -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.model.* +import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.mutate @@ -35,7 +34,14 @@ internal class SetRefreshStateTransformer( } override fun transform(walletUM: WalletUM): WalletUM { - return walletUM // todo redesign main + return when (walletUM) { + is WalletUM.Content -> walletUM.copy( + pullToRefreshConfig = walletUM.pullToRefreshConfig.toUpdatedState(isRefreshing), + tokensListUM = walletUM.tokensListUM.toUpdatedState(), + buttons = walletUM.enableButtons(), + ) + is WalletUM.Locked -> walletUM + } } private fun PullToRefreshConfig.toUpdatedState(isRefreshing: Boolean): PullToRefreshConfig { @@ -54,6 +60,16 @@ internal class SetRefreshStateTransformer( } } + private fun WalletTokensListUM.toUpdatedState(): WalletTokensListUM { + return if (this is WalletTokensListUM.Content && organizeButtonUM != null) { + copy( + organizeButtonUM = organizeButtonUM.copy(isEnabled = !isRefreshing), + ) + } else { + this + } + } + private fun PersistentList.toUpdatedState(): PersistentList { val isButtonsEnabled = !isRefreshing diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 6efe15284e..130c44f114 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -2,6 +2,8 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet @@ -53,7 +55,9 @@ internal class SetTokenListErrorTransformer( return when (walletUM) { is WalletUM.Content -> { walletUM.copy( + walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState(), tokensListUM = WalletTokensListUM.Empty, + buttons = walletUM.disableButtons(), ) } is WalletUM.Locked -> { @@ -81,4 +85,20 @@ internal class SetTokenListErrorTransformer( isBalanceFlickering = false, ) } + + private fun WalletBalanceUM.toLoadedState(): WalletBalanceUM { + return WalletBalanceUM.Content( + id = id, + name = name, + balance = BigDecimal.ZERO.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { TangemTheme.typography2.headingRegular28.toSpanStyle() }, + ) + }, + isZeroBalance = true, + isBalanceFlickering = false, + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index d50fe9e0bb..4175d36c43 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -6,6 +6,8 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletBalanceUMTransformer +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMTransformer @@ -49,7 +51,9 @@ internal class SetTokenListTransformer( return when (walletUM) { is WalletUM.Content -> { walletUM.copy( + walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState2(), tokensListUM = toLoadedState(), + buttons = walletUM.enableButtons(), ) } is WalletUM.Locked -> { @@ -71,6 +75,17 @@ internal class SetTokenListTransformer( ).convert(value = this) } + private fun WalletBalanceUM.toLoadedState2(): WalletBalanceUM { + val fiatBalance = when (params) { + is TokenConverterParams.Account -> params.accountList.totalFiatBalance + is TokenConverterParams.Wallet -> params.tokenList.totalFiatBalance + } + return MultiWalletBalanceUMTransformer( + fiatBalance = fiatBalance, + appCurrency = appCurrency, + ).transform(prevState = this) + } + private fun WalletTokensListState.toLoadedState(): WalletTokensListState { return TokenListStateConverter( params = params, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 4e87f77c40..126d447ac9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -6,8 +6,10 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletLoadingStateFactory import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.toPersistentList import timber.log.Timber internal class UnlockWalletTransformer( @@ -31,6 +33,18 @@ internal class UnlockWalletTransformer( if (unlockedWallet == null) state else createLoadingState(state, unlockedWallet) } .toImmutableList(), + wallets2 = prevState.wallets2 + .map { walletUM -> + val unlockedWallet = getUnlockedWallet(walletUM.walletsBalanceUM.id) + if (unlockedWallet == null) { + walletUM + } else { + createLoadingState2( + walletUM = walletUM, + unlockedWallet = unlockedWallet, + ) + } + }.toPersistentList(), ) } @@ -53,4 +67,16 @@ internal class UnlockWalletTransformer( } } } + + private fun createLoadingState2(walletUM: WalletUM, unlockedWallet: UserWallet): WalletUM { + return when (walletUM) { + is WalletUM.Locked -> walletLoadingStateFactory.create2( + userWallet = unlockedWallet, + ) + is WalletUM.Content -> { + Timber.e("Impossible to unlock wallet with not locked state") + walletUM + } + } + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt new file mode 100644 index 0000000000..69a82da93c --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt @@ -0,0 +1,57 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM +import com.tangem.utils.extensions.isZero +import com.tangem.utils.transformer.Transformer + +internal class MultiWalletBalanceUMTransformer( + private val fiatBalance: TotalFiatBalance, + private val appCurrency: AppCurrency, +) : Transformer { + + override fun transform(prevState: WalletBalanceUM): WalletBalanceUM { + return when (fiatBalance) { + is TotalFiatBalance.Loading -> prevState.toLoadingState() + is TotalFiatBalance.Failed -> prevState.toErrorState() + is TotalFiatBalance.Loaded -> prevState.toWalletCardState(fiatBalance) + } + } + + private fun WalletBalanceUM.toLoadingState(): WalletBalanceUM { + return WalletBalanceUM.Loading( + id = id, + name = name, + ) + } + + private fun WalletBalanceUM.toErrorState(): WalletBalanceUM { + return WalletBalanceUM.Error( + id = id, + name = name, + ) + } + + private fun WalletBalanceUM.toWalletCardState(fiatBalance: TotalFiatBalance.Loaded): WalletBalanceUM { + return WalletBalanceUM.Content( + id = id, + name = name, + balance = fiatBalance.amount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { + TangemTheme.typography2.headingRegular28.toSpanStyle() + }, + ) + }, + isZeroBalance = fiatBalance.amount.isZero(), + isBalanceFlickering = fiatBalance.source == StatusSource.CACHE, + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt index b89808ca72..f5506e2b29 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt @@ -1,7 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletManageButton import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.toPersistentList @@ -13,6 +15,14 @@ internal fun WalletState.MultiCurrency.Content.disableButtons(): PersistentList< return changeAvailability(enabled = false) } +internal fun WalletUM.Content.enableButtons(): PersistentList { + return buttons.map { it.copy(isEnabled = true) }.toPersistentList() +} + +internal fun WalletUM.Content.disableButtons(): PersistentList { + return buttons.map { it.copy(isEnabled = false) }.toPersistentList() +} + private fun WalletState.MultiCurrency.Content.changeAvailability(enabled: Boolean): PersistentList { return buttons .map { action -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt index 14685c549d..f762997767 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt @@ -17,4 +17,8 @@ internal inline fun UserWallet.createStateByWalletType( private fun UserWallet.Cold.isWalletWithTokens(): Boolean { return isMultiCurrency || scanResponse.cardTypesResolver.isSingleWalletWithToken() +} + +internal fun UserWallet.isSingleWallet(): Boolean { + return this is UserWallet.Cold && scanResponse.cardTypesResolver.isSingleWallet() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index a7c1027dab..673b914f18 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -4,6 +4,7 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent.Companion import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState +import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet @@ -14,9 +15,11 @@ import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.utils.extensions.addIf import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow /** @@ -43,6 +46,26 @@ internal class WalletLoadingStateFactory( } } + fun create2(userWallet: UserWallet): WalletUM { + return WalletUM.Content( + pullToRefreshConfig = createPullToRefreshConfig(), + walletsBalanceUM = WalletBalanceUM.Loading( + id = userWallet.walletId, + name = userWallet.name, + ), + buttons = createWalletActions(userWallet), + notifications = persistentListOf(), + notificationsCarousel = persistentListOf(), + tokensListUM = WalletTokensListUM.Loading, + nftState = WalletNFTItemUM.Hidden, + type = when (userWallet) { + is UserWallet.Cold -> WalletType.Cold + is UserWallet.Hot -> WalletType.Hot + }, + tangemPayState = TangemPayState.Empty, + ) + } + private fun createLoadingHotWalletContent(userWallet: UserWallet.Hot): WalletState.MultiCurrency.Content { return WalletState.MultiCurrency.Content( pullToRefreshConfig = createPullToRefreshConfig(), @@ -144,6 +167,39 @@ internal class WalletLoadingStateFactory( ) } + private fun createWalletActions(userWallet: UserWallet): PersistentList { + return buildList { + add( + WalletActionButtons.Buy( + isEnabled = false, + onClick = { + clickIntents.onMultiWalletBuyClick( + userWalletId = userWallet.walletId, + screenType = WALLET_TYPE, + ) + }, + ).buttonUM, + ) + addIf( + condition = !userWallet.isSingleWallet(), + element = WalletActionButtons.Swap( + isEnabled = false, + onClick = { + clickIntents.onMultiWalletSwapClick(userWalletId = userWallet.walletId) + }, + ).buttonUM, + ) + add( + WalletActionButtons.Sell( + isEnabled = false, + onClick = { + clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) + }, + ).buttonUM, + ) + }.toPersistentList() + } + private fun createDimmedButtons(): PersistentList { return persistentListOf( WalletManageButton.Receive( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicSingleWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicSingleWalletSubscriber.kt index a89f459bdc..6ebcbb2878 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicSingleWalletSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicSingleWalletSubscriber.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.mapNotNull * [REDACTED_AUTHOR] */ +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal abstract class BasicSingleWalletSubscriber : BasicWalletSubscriber() { /** Account ID for the main crypto portfolio of the user wallet */ diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt index 8cc069470a..83b44bf068 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriber.kt @@ -16,6 +16,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class MultiWalletWarningsSubscriber @AssistedInject constructor( @Assisted private val userWallet: UserWallet, private val stateController: WalletStateController, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt index bd063d00a9..9e389948bd 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/PrimaryCurrencySubscriber.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onEach import java.math.BigDecimal +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class PrimaryCurrencySubscriber @AssistedInject constructor( @Assisted override val userWallet: UserWallet, override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt index f477c4d790..dfeb7d4b35 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.onEach +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class SingleWalletButtonsSubscriber @AssistedInject constructor( @Assisted override val userWallet: UserWallet, override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt index 6a8d7f382b..c8b4e2fd21 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletExpressStatusesSubscriber.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @Suppress("LongParameterList") internal class SingleWalletExpressStatusesSubscriber @AssistedInject constructor( @Assisted override val userWallet: UserWallet, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt index 42839a3e87..ac34381df5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletNotificationsSubscriber.kt @@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.onEach /** [REDACTED_AUTHOR] */ +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") internal class SingleWalletNotificationsSubscriber @AssistedInject constructor( @Assisted private val userWallet: UserWallet, private val stateController: WalletStateController, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletSubscriber.kt new file mode 100644 index 0000000000..88e0697c7e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletSubscriber.kt @@ -0,0 +1,35 @@ +package com.tangem.feature.wallet.presentation.wallet.subscribers + +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.presentation.account.AccountDependencies +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +internal class SingleWalletSubscriber @AssistedInject constructor( + @Assisted override val userWallet: UserWallet.Cold, + override val accountDependencies: AccountDependencies, + override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + override val stateController: WalletStateController, + override val clickIntents: WalletClickIntents, +) : BasicAccountListSubscriber() { + + override fun create(coroutineScope: CoroutineScope): Flow = combine( + flow = getAccountStatusListFlow(), + flow2 = getAppCurrencyFlow(), + flow3 = accountDependencies.expandedAccountsHolder.expandedAccounts(userWallet), + flow4 = accountDependencies.isAccountsModeEnabledUseCase(), + transform = ::updateState2, + ) + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet.Cold): SingleWalletSubscriber + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt similarity index 88% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriber.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt index 44957d2db0..1a31f1f1b4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt @@ -12,7 +12,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -internal class SingleWalletWithTokenSubscriber @AssistedInject constructor( +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") +internal class SingleWalletWithTokenSubscriberLegacy @AssistedInject constructor( @Assisted override val userWallet: UserWallet.Cold, override val accountDependencies: AccountDependencies, override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -30,6 +31,6 @@ internal class SingleWalletWithTokenSubscriber @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenSubscriber + fun create(userWallet: UserWallet.Cold): SingleWalletWithTokenSubscriberLegacy } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberLegacy.kt similarity index 96% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberLegacy.kt index 3bd4244ab3..ba0efd30e8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TxHistorySubscriberLegacy.kt @@ -30,8 +30,9 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @Suppress("LongParameterList") -internal class TxHistorySubscriber @AssistedInject constructor( +internal class TxHistorySubscriberLegacy @AssistedInject constructor( @Assisted override val userWallet: UserWallet.Cold, @Assisted private val isRefresh: Boolean, override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, @@ -126,6 +127,6 @@ internal class TxHistorySubscriber @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): TxHistorySubscriber + fun create(userWallet: UserWallet.Cold, isRefresh: Boolean): TxHistorySubscriberLegacy } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNotificationsSubscriber.kt similarity index 85% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNotificationsSubscriber.kt index f7e10a9ecf..57f51e06d1 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletWarningsSubscriberV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNotificationsSubscriber.kt @@ -5,10 +5,13 @@ import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.analytics.utils.WalletWarningsSingleEventSender import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletNotificationsCarouselFactory -import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletWarningsFactory +import com.tangem.feature.wallet.presentation.wallet.domain.GetWalletNotificationsFactory import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotificationUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetWarningsTransformer +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList @@ -16,11 +19,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.* @Suppress("LongParameterList") -internal class MultiWalletWarningsSubscriberV2( - private val userWallet: UserWallet, +internal class WalletNotificationsSubscriber @AssistedInject constructor( + @Assisted private val userWallet: UserWallet, private val stateHolder: WalletStateController, private val clickIntents: WalletClickIntents, - private val getWalletWarningsFactory: GetWalletWarningsFactory, + private val getWalletNotificationsFactory: GetWalletNotificationsFactory, private val getWalletNotificationsCarouselFactory: GetWalletNotificationsCarouselFactory, private val walletWarningsAnalyticsSender: WalletWarningsAnalyticsSender, private val walletWarningsSingleEventSender: WalletWarningsSingleEventSender, @@ -28,7 +31,7 @@ internal class MultiWalletWarningsSubscriberV2( override fun create(coroutineScope: CoroutineScope): Flow> { return combine( - flow = getWalletWarningsFactory.create(userWallet, clickIntents).conflate().distinctUntilChanged(), + flow = getWalletNotificationsFactory.create(userWallet, clickIntents).conflate().distinctUntilChanged(), flow2 = getWalletNotificationsCarouselFactory.create(userWallet, clickIntents).conflate() .distinctUntilChanged(), ) { notifications, notificationsCarousel -> @@ -68,4 +71,9 @@ internal class MultiWalletWarningsSubscriberV2( totalNotifications } } + + @AssistedFactory + interface Factory { + fun create(userWallet: UserWallet): WalletNotificationsSubscriber + } } \ No newline at end of file From cb1cfcf29649df4c3f95e1d3af6839ca553d1c2d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 13:18:11 +0500 Subject: [PATCH 63/97] Updated on 2026-08-14 --- .../converters/BlockchainInfoConverter.kt | 37 ++++++++++++++----- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt index 8c430ced24..9575c86449 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/converters/BlockchainInfoConverter.kt @@ -3,6 +3,7 @@ package com.tangem.data.feedback.converters import com.tangem.blockchain.common.Wallet import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.address.Address +import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.domain.feedback.models.BlockchainInfo import com.tangem.domain.feedback.models.BlockchainInfo.Addresses.Multiple.AddressInfo import com.tangem.utils.converter.Converter @@ -16,22 +17,38 @@ import com.tangem.domain.feedback.models.BlockchainInfo.Addresses as BlockchainA internal object BlockchainInfoConverter : Converter { override fun convert(value: WalletManager): BlockchainInfo { - val derivationPath = value.wallet.publicKey.derivationPath + val wallet = value.wallet + val blockchain = wallet.blockchain + val derivationPath = wallet.publicKey.derivationPath return BlockchainInfo( - blockchain = value.wallet.blockchain.fullName, + blockchain = blockchain.fullName, derivationPath = derivationPath?.rawPath.orEmpty(), outputsCount = value.outputsCount?.toString(), host = value.currentHost, - addresses = value.wallet.mapAddresses(Address::value), - explorerLinks = value.wallet.mapAddresses { value.wallet.getExploreUrl(it.value) }, - tokens = value.cardTokens.map { token -> - BlockchainInfo.TokenInfo( - id = token.id, - name = token.name, - contractAddress = token.contractAddress, - decimals = token.decimals.toString(), + addresses = wallet.mapAddresses(Address::value), + explorerLinks = wallet.mapAddresses { value.wallet.getExploreUrl(it.value) }, + tokens = buildList { + // add coin + add( + BlockchainInfo.TokenInfo( + id = blockchain.toCoinId(), + name = blockchain.getCoinName(), + contractAddress = wallet.address, + decimals = blockchain.decimals().toString(), + ), ) + // add other tokens + value.cardTokens.forEach { token -> + add( + BlockchainInfo.TokenInfo( + id = token.id, + name = token.name, + contractAddress = token.contractAddress, + decimals = token.decimals.toString(), + ), + ) + } }, ) } From 01d83b53cf96dc807b081049b9b62c1a5f775192 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 10:38:44 +0100 Subject: [PATCH 64/97] Updated on 2026-08-14 --- .../features/feed/model/feed/FeedComponentModel.kt | 2 +- .../feed/model/feed/state/FeedStateController.kt | 2 +- .../state/transformers/UpdateEarnStateTransformer.kt | 4 +++- .../com/tangem/features/feed/ui/earn/EarnContent.kt | 9 +++++---- .../com/tangem/features/feed/ui/earn/state/EarnListUM.kt | 1 + .../tangem/features/feed/ui/feed/components/EarnBlock.kt | 5 +++-- .../com/tangem/features/feed/ui/feed/state/FeedListUM.kt | 2 +- 7 files changed, 15 insertions(+), 10 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt index fde61b8825..bd7ea3d8c3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/FeedComponentModel.kt @@ -271,7 +271,7 @@ internal class FeedComponentModel @Inject constructor( earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) { EarnListUM.Loading } else { - null + EarnListUM.Empty }, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt index 99440c5949..7100cff366 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/FeedStateController.kt @@ -70,7 +70,7 @@ internal class FeedStateController @Inject constructor( earnListUM = if (feedFeatureToggle.isEarnBlockEnabled) { EarnListUM.Loading } else { - null + EarnListUM.Empty }, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt index fd81cc6428..d95b950076 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/feed/state/transformers/UpdateEarnStateTransformer.kt @@ -47,7 +47,7 @@ internal class UpdateEarnStateTransformer( } private fun handleEmptyState(currentState: FeedListUM): FeedListUM { - return currentState.copy(earnListUM = null) + return currentState.copy(earnListUM = EarnListUM.Empty) } private fun handleErrorState(currentState: FeedListUM, result: EarnError): FeedListUM { @@ -69,6 +69,8 @@ internal class UpdateEarnStateTransformer( currentState: FeedListUM, earnTokensWithCurrency: List, ): FeedListUM { + if (earnTokensWithCurrency.isEmpty()) return currentState.copy(earnListUM = EarnListUM.Empty) + val newItems = earnTokensWithCurrency .sortedWith( compareByDescending { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt index fb0a94723b..9ff14d6bc1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/EarnContent.kt @@ -108,8 +108,8 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { AnimatedContent( targetState = state, contentKey = { it::class.java }, - ) { st -> - when (st) { + ) { animatedState -> + when (animatedState) { is EarnListUM.Loading -> { MostlyUsedPlaceholder() } @@ -122,7 +122,7 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { horizontalArrangement = Arrangement.spacedBy(8.dp), ) { itemsIndexed( - items = st.items, + items = animatedState.items, key = { _, item -> "${item.tokenName}-${item.network}" }, ) { index, item -> val cardModifier = Modifier.conditional( @@ -154,9 +154,10 @@ private fun MostlyUsedContent(state: EarnListUM, onScroll: () -> Unit) { .padding(vertical = 32.dp, horizontal = 12.dp), contentAlignment = Alignment.Center, ) { - UnableToLoadData(onRetryClick = st.onRetryClicked) + UnableToLoadData(onRetryClick = animatedState.onRetryClicked) } } + EarnListUM.Empty -> Unit // no need to handle } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt index c4506586bc..72ba54139d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/state/EarnListUM.kt @@ -10,6 +10,7 @@ internal sealed interface EarnListUM { data object Loading : EarnListUM data class Content(val items: ImmutableList) : EarnListUM data class Error(val onRetryClicked: () -> Unit) : EarnListUM + data object Empty : EarnListUM } @Immutable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt index 08e6849b20..d97e6bd7a3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/EarnBlock.kt @@ -24,8 +24,8 @@ import com.tangem.features.feed.ui.earn.state.EarnListUM import kotlinx.collections.immutable.ImmutableList @Composable -internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modifier: Modifier = Modifier) { - if (earnListUM == null) return +internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM, modifier: Modifier = Modifier) { + if (earnListUM is EarnListUM.Empty) return Column(modifier = modifier) { Header( @@ -57,6 +57,7 @@ internal fun EarnBlock(onSeeAllClick: () -> Unit, earnListUM: EarnListUM?, modif is EarnListUM.Content -> EarnContentBlock(items = earnListUM.items) is EarnListUM.Error -> EarnErrorBlock(onRetryClick = earnListUM.onRetryClicked) EarnListUM.Loading -> EarnListPlaceholder(placeholderCount = PLACEHOLDER_ITEM_COUNT) + EarnListUM.Empty -> Unit } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt index 956cfde84f..8ba119a2d1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/state/FeedListUM.kt @@ -18,7 +18,7 @@ internal data class FeedListUM( val trendingArticle: ArticleConfigUM?, val marketChartConfig: MarketChartConfig, val globalState: GlobalFeedState = GlobalFeedState.Content, - val earnListUM: EarnListUM?, + val earnListUM: EarnListUM, ) internal data class FeedListCallbacks( From 4c0e46ce3523456fa091fe0c7ef7e3232c0df087 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 09:46:21 +0000 Subject: [PATCH 65/97] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index b1002f2997..6934faad61 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.34-1430" +tangemBlockchainSdk = "develop-1437" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-584" +tangemCardSdk = "develop-582" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 73bf0b7617ec32b993583b3c6b68bb7f33d20072 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 13:22:44 +0300 Subject: [PATCH 66/97] Updated on 2026-08-14 --- .../core/ui/ds/button/GhostTangemButton.kt | 6 +- .../core/ui/ds/button/TangemButtonInternal.kt | 151 ++++++------ features/tester/STORYBOOK.md | 218 ++++++++++++++++++ .../storybook/entity/StoryBookPage.kt | 2 + .../storybook/page/buttons/Build.kt | 6 + .../storybook/page/buttons/ButtonsStory.kt | 200 ++++++++++++++++ .../storybook/ui/StoryBookListScreen.kt | 4 +- .../storybook/ui/StoryBookScreen.kt | 3 + 8 files changed, 522 insertions(+), 68 deletions(-) create mode 100644 features/tester/STORYBOOK.md create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt index 0fece5aae0..1bd3696db3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -36,6 +37,7 @@ fun GhostTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) { enabled = buttonUM.isEnabled, size = buttonUM.size, state = buttonUM.state, + shape = buttonUM.shape, ) } @@ -63,6 +65,7 @@ fun GhostTangemButton( size: TangemButtonSize = TangemButtonSize.X15, state: TangemButtonState = TangemButtonState.Default, iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start, + shape: TangemButtonShape = TangemButtonShape.Default, ) { val contentColor = when (state) { TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled @@ -70,7 +73,8 @@ fun GhostTangemButton( } TangemButtonInternal( onClick = onClick, - modifier = modifier, + modifier = modifier + .clip(shape = shape.toShape(size)), text = text, contentColor = contentColor, enabled = enabled, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt index 4d6f5dc9d9..53166d0416 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt @@ -7,10 +7,10 @@ import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.Text +import androidx.compose.material.ripple.RippleAlpha +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -60,72 +60,91 @@ internal fun TangemButtonInternal( size: TangemButtonSize = TangemButtonSize.X15, state: TangemButtonState = TangemButtonState.Default, ) { - Row( - modifier = modifier - .testTag(BaseButtonTestTags.BUTTON) - .height(size.toHeightDp()) - .conditionalCompose(text == null) { - width(size.toHeightDp()) + ProvideButtonRippleConfiguration { + Row( + modifier = modifier + .testTag(BaseButtonTestTags.BUTTON) + .clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button) + .height(size.toHeightDp()) + .conditionalCompose(text == null) { + width(size.toHeightDp()) + } + .conditionalCompose(text != null) { + padding(horizontal = size.toPaddingDp()) + } + .animateContentSize(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + ) { + AnimatedVisibility( + visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start, + modifier = Modifier.size(size = size.toContentSize()), + ) { + val wrappedIconRes = remember(this) { requireNotNull(iconRes) } + TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size) } - .clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button) - .conditionalCompose(text != null) { - padding(horizontal = size.toPaddingDp()) + + AnimatedVisibility(text != null && state != TangemButtonState.Loading) { + val wrappedText = remember(this) { requireNotNull(text) } + val textStyle = size.toTextStyle() + Text( + text = wrappedText.resolveReference(), + style = textStyle, + color = contentColor, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = 12.sp, + maxFontSize = textStyle.fontSize, + ), + modifier = Modifier.testTag(BaseButtonTestTags.TEXT), + ) } - .animateContentSize(), - horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), - verticalAlignment = Alignment.CenterVertically, + + AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) { + val wrappedText = remember(this) { requireNotNull(descriptionText) } + val textStyle = TangemTheme.typography2.captionSemibold12 + Text( + text = wrappedText.resolveReference(), + style = textStyle, + color = TangemTheme.colors2.text.status.disabled, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = 12.sp, + maxFontSize = textStyle.fontSize, + ), + modifier = Modifier.testTag(BaseButtonTestTags.TEXT), + ) + } + + AnimatedVisibility( + visible = iconRes != null && iconPosition == TangemButtonIconPosition.End, + modifier = Modifier.size(size = size.toContentSize()), + ) { + val wrappedIconRes = remember(this) { requireNotNull(iconRes) } + TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size) + } + } + } +} + +@Composable +private inline fun ProvideButtonRippleConfiguration(crossinline content: @Composable () -> Unit) { + CompositionLocalProvider( + LocalRippleConfiguration provides RippleConfiguration( + color = TangemTheme.colors2.overlay.overlaySecondary, + RippleAlpha( + pressedAlpha = 0.4f, + focusedAlpha = 0.4f, + draggedAlpha = 0.4f, + hoveredAlpha = 0.4f, + ), + ), ) { - AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start, - modifier = Modifier.size(size = size.toContentSize()), - ) { - val wrappedIconRes = remember(this) { requireNotNull(iconRes) } - TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size) - } - - AnimatedVisibility(text != null && state != TangemButtonState.Loading) { - val wrappedText = remember(this) { requireNotNull(text) } - val textStyle = size.toTextStyle() - Text( - text = wrappedText.resolveReference(), - style = textStyle, - color = contentColor, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - autoSize = TextAutoSize.StepBased( - minFontSize = 12.sp, - maxFontSize = textStyle.fontSize, - ), - modifier = Modifier.testTag(BaseButtonTestTags.TEXT), - ) - } - - AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) { - val wrappedText = remember(this) { requireNotNull(descriptionText) } - val textStyle = TangemTheme.typography2.captionSemibold12 - Text( - text = wrappedText.resolveReference(), - style = textStyle, - color = TangemTheme.colors2.text.status.disabled, - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - autoSize = TextAutoSize.StepBased( - minFontSize = 12.sp, - maxFontSize = textStyle.fontSize, - ), - modifier = Modifier.testTag(BaseButtonTestTags.TEXT), - ) - } - - AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemButtonIconPosition.End, - modifier = Modifier.size(size = size.toContentSize()), - ) { - val wrappedIconRes = remember(this) { requireNotNull(iconRes) } - TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size) - } + content() } } diff --git a/features/tester/STORYBOOK.md b/features/tester/STORYBOOK.md new file mode 100644 index 0000000000..f9142cdd1b --- /dev/null +++ b/features/tester/STORYBOOK.md @@ -0,0 +1,218 @@ +# Storybook — Adding New Design System Pages + +The storybook lives in `features/tester` and lets developers browse and validate +design system components at runtime on a device or emulator. + +--- + +## Architecture overview + +``` +storybook/ +├── entity/ +│ ├── StoryBookPage.kt ← sealed interface + all page state classes +│ ├── StoryBookUM.kt ← top-level UI model (current page, navigation) +│ └── StoryPageFactory.kt ← factory interface used by the list screen +├── page/ +│ └── / +│ ├── Build.kt ← creates the StoryPageFactory for this page +│ └── Story.kt ← the Composable that renders the showcase +├── ui/ +│ ├── StoryBookListScreen.kt ← list of all stories (add your entry here) +│ └── StoryBookScreen.kt ← routes currentPage → correct Composable +└── viewmodel/ + ├── StoryBookViewModel.kt + └── StateUpdater.kt ← helper for stateful pages +``` + +--- + +## Step-by-step: adding a new page + +### 1. Declare the page type in `StoryBookPage.kt` + +For a **stateless** showcase (no user interaction that mutates page state): +```kotlin +internal data object FooStory : StoryBookPage +``` + +For a **stateful** page (e.g. toggle between variants like NorthernLightsStory): +```kotlin +internal data class FooStory( + val selectedVariant: Variant, + val onVariantChange: (Variant) -> Unit, +) : StoryBookPage { + enum class Variant { A, B } +} +``` + +--- + +### 2. Create `page/foo/Build.kt` + +**Stateless:** +```kotlin +internal val fooStoryFactory: StoryPageFactory = StoryPageFactory { FooStory } +``` + +**Stateful** (use `storyPageFactory` + `StateUpdater`): +```kotlin +internal fun StateUpdater.build(): FooStory { + return FooStory( + selectedVariant = FooStory.Variant.A, + onVariantChange = { newVariant -> + updateStory { it.copy(selectedVariant = newVariant) } + }, + ) +} + +internal val fooStoryFactory + get() = storyPageFactory(StateUpdater::build) +``` + +--- + +### 3. Create `page/foo/FooStory.kt` + +Write a `@Composable internal fun FooStory(...)` that renders the showcase. +See [Design guidelines](#design-guidelines) below for layout advice. + +**Stateless example skeleton:** +```kotlin +@Composable +internal fun FooStory(modifier: Modifier = Modifier) { + LazyColumn(modifier = modifier.fillMaxSize()) { + item("section_a") { /* ... */ } + } +} +``` + +**Stateful example skeleton:** +```kotlin +@Composable +internal fun FooStory(state: FooStory, modifier: Modifier = Modifier) { + // use state.selectedVariant, state.onVariantChange +} +``` + +--- + +### 4. Register in `StoryBookScreen.kt` + +Add a branch to the `when` block. + +> **Naming note:** the entity type and the Composable function will share the +> same simple name (e.g. `FooStory`). Kotlin resolves them correctly — the +> entity import is used in the pattern position, the function import is used +> as a call. This is the same pattern used for `NorthernLightsStory` and +> `ButtonsStory`. + +```kotlin +import com.tangem.feature.tester.presentation.storybook.entity.FooStory +import com.tangem.feature.tester.presentation.storybook.page.foo.FooStory + +when (storyState) { + StoryList -> StoryBookListScreen(state = state) + is NorthernLightsStory -> NorthernLightsStory(state = storyState) + ButtonsStory -> ButtonsStory() + FooStory -> FooStory() // stateless + is FooStory -> FooStory(storyState) // stateful (note `is`) +} +``` + +--- + +### 5. Register in `StoryBookListScreen.kt` + +Add one entry to `buildStories()`. **Every title must start with an emoji** that +represents the component category — this makes the list easier to scan at a glance. + +```kotlin +private fun buildStories() = listOf( + StoryItem(title = "🃏 Foo Component", factory = fooStoryFactory), + // existing entries... +) +``` + +Pick an emoji that reflects the component's visual nature or purpose, e.g.: +- Buttons → 🔘 +- Background effects → 🌌 +- Typography → 🔤 +- Icons → 🎨 +- Cards → 🃏 +- Inputs / Text fields → ✏️ +- Navigation → 🧭 +- Loaders / Progress → ⏳ + +--- + +## Design guidelines + +### Layout + +Use a `LazyColumn` as the root for component showcases so the page scrolls +when content is taller than the screen. + +```kotlin +LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.fillMaxSize().background(TangemTheme.colors2.surface.level1), +) { /* items */ } +``` + +### Showing all variants + +Show every meaningful axis of variation in one place: + +| Axis | How to display | +|---|---| +| **States** (Default, Disabled, Pressed, Loading) | One row per state | +| **Shapes** (Default, Rounded) | One labeled group (`ShapeGroup`) per shape, iterate `TangemButtonShape.entries` | +| **Content** (text+icon vs icon-only) | Two columns per row | +| **Sizes** | Separate `LazyColumn` item per size group if needed | + +> **Prefer vertical stacking over horizontal.** A row should contain at most +> 2–3 components; more than that overflows on narrow screens. Use +> `Modifier.weight(1f)` on columns instead of fixed widths. + +### Section structure (component grids) + +Follow the pattern used in `ButtonsStory`: +- **Section title** — `TangemTheme.typography.subtitle1` +- **Group sub-header** (shape/size/variant name) — `TangemTheme.typography.body2` +- **Column headers** (Text + Icon, Icon only, etc.) — `TangemTheme.typography.caption2` +- **State label** (Default, Disabled…) — `TangemTheme.typography.caption2`, fixed width ~80 dp + +``` +Primary ← subtitle1 + Default ← body2 (shape/group sub-header) + Text + Icon Icon only ← caption2 column headers + Default [■ Continue] [■] ← state row + Disabled [■ Continue] [■] + Pressed [■ Continue] [■] + Loading [ ⟳ ] [⟳] + Rounded ← body2 + ... +``` + +### Colors + +- Page background: `TangemTheme.colors2.surface.level1` +- Sections that need a contrasting background (e.g. PrimaryInverse): + `TangemTheme.colors2.surface.level2` +- Section divider: `HorizontalDivider(color = TangemTheme.colors2.border.neutral.secondary)` + +### Realistic text + +Use representative text strings, not placeholders like "Btn". Pick labels that +match how the component would appear in the product (e.g. `"Continue"`, +`"Send payment"`, `"Confirm"`). + +### DS component imports + +All design system components (`PrimaryTangemButton`, `TangemButtonSize`, etc.) +live in `com.tangem.core.ui.ds.*` and are `public`, so they are directly +importable from the `features/tester` module. + +Use `com.tangem.core.ui.R` for drawable resources (e.g. `R.drawable.ic_tangem_24`). \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt index 1c730da1d5..5967dd9e21 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -4,6 +4,8 @@ internal sealed interface StoryBookPage internal data object StoryList : StoryBookPage +internal data object ButtonsStory : StoryBookPage + internal data class NorthernLightsStory( val variant: Variant, val onVariantChange: (Variant) -> Unit, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt new file mode 100644 index 0000000000..4ea6b7efbb --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.tester.presentation.storybook.page.buttons + +import com.tangem.feature.tester.presentation.storybook.entity.ButtonsStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal val buttonsStoryFactory: StoryPageFactory = StoryPageFactory { ButtonsStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt new file mode 100644 index 0000000000..d57ebfdd8a --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt @@ -0,0 +1,200 @@ +package com.tangem.feature.tester.presentation.storybook.page.buttons + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.button.* +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme + +private const val STATE_LABEL_WIDTH = 80 + +@Suppress("LongMethod") +@Composable +internal fun ButtonsStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("primary") { + ButtonSection(title = "Primary") { state, text, shape -> + PrimaryTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("secondary") { + ButtonSection(title = "Secondary") { state, text, shape -> + SecondaryTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("primary_inverse") { + ButtonSection( + title = "PrimaryInverse", + background = TangemTheme.colors2.surface.level2, + ) { state, text, shape -> + PrimaryInverseTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("outline") { + ButtonSection(title = "Outline") { state, text, shape -> + OutlineTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("accent") { + ButtonSection(title = "Accent") { state, text, shape -> + AccentTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + item("ghost") { + ButtonSection(title = "Ghost") { state, text, shape -> + GhostTangemButton( + onClick = {}, + text = if (text) stringReference("Continue") else null, + iconRes = R.drawable.ic_tangem_24, + size = TangemButtonSize.X10, + state = state, + shape = shape, + ) + } + } + } +} + +@Composable +private fun ButtonSection( + title: String, + background: Color = TangemTheme.colors2.surface.level1, + shapes: List = TangemButtonShape.entries, + button: @Composable (state: TangemButtonState, text: Boolean, shape: TangemButtonShape) -> Unit, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .background(background) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + shapes.forEach { shape -> + ShapeGroup(shape = shape, button = button) + } + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun ShapeGroup( + shape: TangemButtonShape, + button: @Composable (state: TangemButtonState, text: Boolean, shape: TangemButtonShape) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = shape.name, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + ColumnHeaderRow() + TangemButtonState.entries.forEach { state -> + StateRow(state = state, shape = shape, button = button) + } + } +} + +@Composable +private fun ColumnHeaderRow() { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(STATE_LABEL_WIDTH.dp)) + Text( + text = "Text + Icon", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Icon only", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun StateRow( + state: TangemButtonState, + shape: TangemButtonShape, + button: @Composable (state: TangemButtonState, text: Boolean, shape: TangemButtonShape) -> Unit, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.width(STATE_LABEL_WIDTH.dp), + ) + Box(modifier = Modifier.weight(1f)) { + button(state, true, shape) + } + Box(modifier = Modifier.weight(1f)) { + button(state, false, shape) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt index ac74e45a74..4dc59eac12 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt @@ -16,11 +16,13 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory import com.tangem.feature.tester.presentation.storybook.page.background.northernLightsStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.buttons.buttonsStoryFactory private data class StoryItem(val title: String, val factory: StoryPageFactory) private fun buildStories() = listOf( - StoryItem(title = "Northern Lights Background", factory = northernLightsStoryFactory), + StoryItem(title = "🔘 Buttons", factory = buttonsStoryFactory), + StoryItem(title = "🌌 Northern Lights Background", factory = northernLightsStoryFactory), ) @Composable diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt index 73e643e0ac..1806965ad7 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -4,10 +4,12 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.feature.tester.presentation.storybook.entity.ButtonsStory import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM import com.tangem.feature.tester.presentation.storybook.entity.StoryList import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.page.buttons.ButtonsStory @Composable internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) { @@ -20,6 +22,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) when (storyState) { StoryList -> StoryBookListScreen(state = state) is NorthernLightsStory -> NorthernLightsStory(state = storyState) + ButtonsStory -> ButtonsStory() } } } \ No newline at end of file From 128faaec8d0e9628a5b6a0cc4b45316d27d0d4b3 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 13:20:41 +0300 Subject: [PATCH 67/97] Updated on 2026-08-14 --- .../impl/model/MarketsPortfolioModel.kt | 17 ++++++++--------- .../model/AvailableSwapPairsModel.kt | 2 ++ .../com/tangem/feature/swap/model/SwapModel.kt | 2 ++ .../com/tangem/lib/crypto/BlockchainUtils.kt | 15 ++++++++++++--- 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt index 7c7818ad79..0401beb1e7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioModel.kt @@ -45,6 +45,14 @@ internal class MarketsPortfolioModel @Inject constructor( private val params = paramsContainer.require() + private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( + tokenSymbol = params.token.symbol, + source = params.analyticsParams?.source, + ) + + private val currentAppCurrency = createAppCurrencyFlow() + private val tokenActionsHandler = createTokenActionsHandler() + val addToPortfolioManager: AddToPortfolioManager = createAddToPortfolioManager() private val marketsPortfolioDelegate: MarketsPortfolioDelegate = createMarketsPortfolioDelegate() @@ -54,15 +62,6 @@ internal class MarketsPortfolioModel @Inject constructor( override fun onSuccess(addedToken: CryptoCurrency) = bottomSheetNavigation.dismiss() } - private val analyticsEventBuilder = PortfolioAnalyticsEvent.EventBuilder( - tokenSymbol = params.token.symbol, - source = params.analyticsParams?.source, - ) - - private val currentAppCurrency = createAppCurrencyFlow() - - private val tokenActionsHandler = createTokenActionsHandler() - init { marketsPortfolioDelegate.combineData() .onEach { state.value = it } 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 363f0712d5..72fd13b5bb 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 @@ -725,6 +725,8 @@ internal class AvailableSwapPairsModel @Inject constructor( val networks = tokenMarket.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( blockchainId = network.networkId, + coinId = tokenMarket.id.value, + contractAddress = network.contractAddress, excludedBlockchains = excludedBlockchains, hotExcludedBlockchains = hotWalletExcludedBlockchains, hasOnlyHotWallets = hasOnlyHotWallets, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index 3eb20abc33..e8a70ed5c4 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -2345,6 +2345,8 @@ internal class SwapModel @Inject constructor( val networks = tokenMarket.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( blockchainId = network.networkId, + coinId = tokenMarket.id.value, + contractAddress = network.contractAddress, excludedBlockchains = excludedBlockchains, hotExcludedBlockchains = hotWalletExcludedBlockchains, hasOnlyHotWallets = hasOnlyHotWallets, diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index 81838cfdbb..1be48e89c6 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt @@ -85,11 +85,20 @@ object BlockchainUtils { excludedBlockchains: ExcludedBlockchains, hotExcludedBlockchains: Set, hasOnlyHotWallets: Boolean = false, + coinId: String? = null, + contractAddress: String? = null, ): Boolean { - val blockchain = Blockchain.fromNetworkId(blockchainId) + val blockchain = Blockchain.fromNetworkId(blockchainId) ?: return false - return blockchain != null && blockchain !in excludedBlockchains && - (hasOnlyHotWallets.not() || blockchain !in hotExcludedBlockchains) + if (blockchain in excludedBlockchains) return false + if (hasOnlyHotWallets && blockchain in hotExcludedBlockchains) return false + + if (!contractAddress.isNullOrEmpty()) { + if (!blockchain.canHandleTokens()) return false + if (coinId != null && !isNotBlockedByTerraV1Filter(blockchainId, coinId)) return false + } + + return true } fun isArbitrum(blockchainId: String): Boolean { From c3f7d4ebcb831d96f6d48b9f3032f42bacce236b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 12:47:02 +0000 Subject: [PATCH 68/97] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index a3bc5cab8e..6934faad61 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,9 +5,9 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "releases-5.34-1430" +tangemBlockchainSdk = "develop-1437" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.34-587" +tangemCardSdk = "develop-582" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 31e0591ef08c4872d9c2e5556742942eefb401b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 17:52:48 +0500 Subject: [PATCH 69/97] Updated on 2026-08-14 --- .../core/ui/ds/button/TangemButtonUM.kt | 2 + .../tangem/core/ui/ds/topbar/TangemTopBar.kt | 11 +- .../collapsing/TangemCollapsingTopBar.kt | 126 +++++++++++ ...BalanceExitUntilCollapsedScrollBehavior.kt | 211 ++++++++++++++++++ .../entity/TangemCollapsingAppBarState.kt | 142 ++++++++++++ ...viewData.kt => WalletPreviewDataLegacy.kt} | 2 +- .../common/preview/WalletScreenPreviewData.kt | 2 +- .../presentation/preview/WalletPreviewData.kt | 22 ++ .../wallet/ui/components/MarketsHint.kt | 89 ++++++++ .../wallet/ui/components/MarketsTooltip.kt | 144 ++++++++++++ .../ui/components/TokenActionsBottomSheet.kt | 6 +- .../wallet/ui/components/WalletItemBlocks.kt | 49 ++++ .../wallet/ui/components/WalletsList.kt | 4 +- .../ui/components/common/WalletBalance.kt | 205 +++++++++++++++++ .../wallet/ui/components/common/WalletCard.kt | 15 +- .../ui/components/common/WalletContent.kt | 67 ++++++ .../components/common/WalletPagerIndicator.kt | 49 ++++ .../ui/components/common/WalletTopBar.kt | 76 ++++++- .../wallet/ui/utils/LazyListStateExt.kt | 31 +++ .../main/res/drawable/ic_magic_default_24.xml | 9 + 20 files changed, 1242 insertions(+), 20 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/{WalletPreviewData.kt => WalletPreviewDataLegacy.kt} (98%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt create mode 100644 features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt index 0b0e426629..a3bb16c4da 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonUM.kt @@ -1,6 +1,7 @@ package com.tangem.core.ui.ds.button import androidx.annotation.DrawableRes +import androidx.compose.runtime.Stable import com.tangem.core.ui.extensions.TextReference /** @@ -19,6 +20,7 @@ import com.tangem.core.ui.extensions.TextReference * [REDACTED_AUTHOR] */ +@Stable data class TangemButtonUM( val text: TextReference? = null, val descriptionText: TextReference? = null, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt index b96dd8d788..b38389acc5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt @@ -2,7 +2,7 @@ package com.tangem.core.ui.ds.topbar import android.content.res.Configuration import androidx.annotation.DrawableRes -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -55,6 +55,7 @@ fun TangemTopBar( modifier = modifier, content = { Column( + modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), ) { @@ -95,11 +96,17 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: AnimatedVisibility( visible = title != null, label = "Title Visibility", + enter = slideInVertically(initialOffsetY = { it / 2 }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it / 2 }) + fadeOut(), ) { val wrappedTitle = remember(this) { requireNotNull(title) } Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy( + space = TangemTheme.dimens2.x1, + alignment = Alignment.CenterHorizontally, + ), verticalAlignment = Alignment.CenterVertically, ) { AnimatedVisibility( diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt new file mode 100644 index 0000000000..1d91d223b5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/TangemCollapsingTopBar.kt @@ -0,0 +1,126 @@ +package com.tangem.core.ui.ds.topbar.collapsing + +import android.content.res.Configuration +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.DecayAnimationSpec +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import kotlin.math.max +import kotlin.math.roundToInt + + +@Composable +fun TangemCollapsingTopBar( + state: TangemCollapsingAppBarState, + collapsingPart: @Composable () -> Unit, + body: @Composable () -> Unit, +) { + Layout( + modifier = Modifier.fillMaxSize(), + content = { + collapsingPart() + body() + }, + ) { measurables, constraints -> + + val collapsingConstraints = constraints.copy( + minWidth = 0, + minHeight = 0, + ) + val collapsingPlaceable = measurables[0].measure(collapsingConstraints) + + val bodyConstraints = constraints.copy( + minWidth = 0, + minHeight = 0, + maxHeight = (constraints.maxHeight - collapsingConstraints.minHeight).coerceAtLeast(0), + ) + val bodyPlaceable = measurables[1].measure(bodyConstraints) + + val minHeight = 0.dp.roundToPx() + val maxHeight = collapsingPlaceable.height + minHeight + + val offset = state.heightOffset.roundToInt().coerceAtLeast(-maxHeight) + + val width = max( + collapsingPlaceable.width, + bodyPlaceable.width, + ).coerceIn(constraints.minWidth, constraints.maxWidth) + val height = max( + collapsingPlaceable.height, + bodyPlaceable.height, + ).coerceIn(constraints.minHeight, constraints.maxHeight) + + layout(width = width, height = height) { + bodyPlaceable.placeRelative(0, collapsingPlaceable.height + offset) + collapsingPlaceable.placeRelative(0, offset) + } + } +} + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * + * @property state The state of the collapsing app bar. + * @property snapAnimationSpec The animation spec used for snapping the app bar to its collapsed or + * expanded state after a fling. If null, no snapping will occur. + * @property flingAnimationSpec The decay animation spec used for fling gestures. + * If null, fling gestures will not be handled. + * @property nestedScrollConnection Nested scroll connection + */ +@Stable +data class TangemCollapsingAppBarBehavior( + val state: TangemCollapsingAppBarState, + val snapAnimationSpec: AnimationSpec?, + val flingAnimationSpec: DecayAnimationSpec?, + val nestedScrollConnection: NestedScrollConnection, +) + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemCollapsingTopBar_Preview() { + TangemThemePreviewRedesign { + val collapsingHeight = 200.dp + val behavior = rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight = collapsingHeight, + ) + TangemCollapsingTopBar( + state = behavior.state, + collapsingPart = { + Box( + modifier = Modifier + .fillMaxWidth() + .height(collapsingHeight) + .background(Color.Red), + ) + }, + body = { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Blue) + .nestedScroll(behavior.nestedScrollConnection) + .verticalScroll(rememberScrollState()), + ) + }, + ) + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt new file mode 100644 index 0000000000..c3e18a6df6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/WalletBalanceExitUntilCollapsedScrollBehavior.kt @@ -0,0 +1,211 @@ +package com.tangem.core.ui.ds.topbar.collapsing + +import androidx.compose.animation.core.* +import androidx.compose.animation.rememberSplineBasedDecay +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.draggable +import androidx.compose.foundation.gestures.rememberDraggableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState +import com.tangem.core.ui.ds.topbar.collapsing.entity.TopBapScrollDirection +import com.tangem.core.ui.ds.topbar.collapsing.entity.rememberTangemCollapsingAppBarState +import com.tangem.core.ui.utils.toPx +import kotlin.math.abs +import kotlin.math.absoluteValue + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + * + * @param expandedHeight The height of the app bar when it is fully expanded. + * @param partialCollapsedHeight The height of the app bar when it is partially collapsed. + * @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the + * user stops scrolling. If null, no snapping will occur. + * @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar. + * If null, no fling behavior will occur. + */ +@Composable +fun rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight: Dp = -Int.MAX_VALUE.dp, + partialCollapsedHeight: Dp = expandedHeight, + snapAnimationSpec: AnimationSpec? = spring(), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), +): TangemCollapsingAppBarBehavior { + val topBarState = rememberTangemCollapsingAppBarState( + heightOffsetLimit = -expandedHeight.toPx(), + partialHeightLimit = partialCollapsedHeight.toPx(), + ) + return exitUntilCollapsedScrollBehavior( + state = topBarState, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + ) +} + +/** + * A scroll behavior for a collapsing top app bar that collapses when scrolling up and expands when scrolling down. + * When the user stops scrolling, the app bar will settle to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + * + * @param state The state of the collapsing app bar, which controls the height offset and scroll behavior. + * @param snapAnimationSpec The animation spec for snapping the app bar to the collapsed or expanded state when the + * user stops scrolling. If null, no snapping will occur. + * @param flingAnimationSpec The decay animation spec for the fling behavior when the user flings the app bar. + * If null, no fling behavior will occur. + */ +@Composable +private fun exitUntilCollapsedScrollBehavior( + state: TangemCollapsingAppBarState = rememberTangemCollapsingAppBarState(), + snapAnimationSpec: AnimationSpec? = spring(), + flingAnimationSpec: DecayAnimationSpec? = rememberSplineBasedDecay(), +): TangemCollapsingAppBarBehavior { + val nestedScrollConnection = remember(state) { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + val dy = available.y + + val consume = if (dy < 0) { + state.direction = TopBapScrollDirection.Collapsing + state.dispatchRawDelta(dy) + } else { + 0f + } + + return Offset(0f, consume) + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + val dy = available.y + + val consume = if (dy > 0) { + state.direction = TopBapScrollDirection.Expanding + state.dispatchRawDelta(dy) + } else { + state.direction = TopBapScrollDirection.Collapsing + 0f + } + + return Offset(0f, consume) + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + val superConsumed = super.onPostFling(consumed, available) + return superConsumed + settleAppBar( + state = state, + velocity = available.y, + flingAnimationSpec = flingAnimationSpec, + snapAnimationSpec = snapAnimationSpec, + ) + } + } + } + + return remember(state, nestedScrollConnection, snapAnimationSpec, flingAnimationSpec) { + TangemCollapsingAppBarBehavior( + state = state, + snapAnimationSpec = snapAnimationSpec, + flingAnimationSpec = flingAnimationSpec, + nestedScrollConnection = nestedScrollConnection, + ) + } +} + +@Composable +fun Modifier.snapToExitUntilCollapsed(behavior: TangemCollapsingAppBarBehavior): Modifier { + return draggable( + orientation = Orientation.Vertical, + state = rememberDraggableState { delta -> + behavior.state.heightOffset += delta + }, + onDragStopped = { velocity -> + settleAppBar( + state = behavior.state, + velocity = velocity, + flingAnimationSpec = behavior.flingAnimationSpec, + snapAnimationSpec = behavior.snapAnimationSpec, + ) + }, + ) +} + +/** + * Settles the app bar to either fully collapsed or fully expanded state + * based on the current collapsed fraction and scroll direction. + */ +@Suppress("MagicNumber", "CyclomaticComplexMethod") +private suspend fun settleAppBar( + state: TangemCollapsingAppBarState, + velocity: Float, + flingAnimationSpec: DecayAnimationSpec?, + snapAnimationSpec: AnimationSpec?, + snapCollapseThreshold: Float = 0.3f, + snapExpandThreshold: Float = 0.7f, +): Velocity { + val partialLimit = state.heightOffsetLimit + state.partialHeightLimit + var remainingVelocity = velocity + + // Check if the app bar is completely collapsed/expanded. If so, no need to settle the app bar, + // and just return Zero Velocity. + // Note that we don't check for 0f due to float precision with the collapsedFraction + // calculation. + if (state.collapsedFraction < 0.01f || state.collapsedFraction == 1f) { + return Velocity.Zero + } + + // Fling + if (flingAnimationSpec != null && velocity.absoluteValue > 1f) { + var lastValue = 0f + AnimationState( + initialValue = 0f, + initialVelocity = velocity, + ).animateDecay(flingAnimationSpec) { + val delta = value - lastValue + val initialHeightOffset = state.heightOffset + + val availableDelta = partialLimit - initialHeightOffset + + state.heightOffset = if (delta < 0f && initialHeightOffset > partialLimit) { + (initialHeightOffset + delta).coerceAtLeast(partialLimit) + } else { + initialHeightOffset + delta + } + + val consumed = abs(initialHeightOffset - state.heightOffset) + lastValue = value + remainingVelocity = this.velocity + // avoid rounding errors and stop if anything is unconsumed + if (abs(maxOf(delta, availableDelta) - consumed) > 0.5f) this.cancelAnimation() + } + } + // Snap + if (snapAnimationSpec != null && state.heightOffset > partialLimit && state.heightOffset < 0f) { + AnimationState(initialValue = state.heightOffset).animateTo( + when (state.direction) { + TopBapScrollDirection.Collapsing -> if (state.collapsedFraction > snapCollapseThreshold) { + partialLimit + } else { + 0f + } + TopBapScrollDirection.Expanding -> if (state.collapsedFraction < snapExpandThreshold) { + 0f + } else { + partialLimit + } + TopBapScrollDirection.Idle -> 0f + }, + animationSpec = snapAnimationSpec, + ) { + state.heightOffset = value + } + } + return Velocity(0f, remainingVelocity) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt new file mode 100644 index 0000000000..a5d1a8892d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/collapsing/entity/TangemCollapsingAppBarState.kt @@ -0,0 +1,142 @@ +package com.tangem.core.ui.ds.topbar.collapsing.entity + +import androidx.compose.animation.core.AnimationState +import androidx.compose.animation.core.animateTo +import androidx.compose.animation.core.tween +import androidx.compose.foundation.MutatePriority +import androidx.compose.foundation.gestures.ScrollScope +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.listSaver +import androidx.compose.runtime.saveable.rememberSaveable +import com.tangem.core.ui.ds.topbar.collapsing.entity.TangemCollapsingAppBarState.Companion.Saver +import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min + +/** + * State of the collapsing top app bar. + * It contains the current height offset, the limits for collapsing and expanding, and the scroll direction. + * + * @property initialHeightOffset The initial height offset of the app bar. Default is 0f. + * @property heightOffsetLimit The height offset limit for full collapse. + * @property partialHeightLimit The height offset limit for partial collapse. Default is the same as [heightOffsetLimit] + */ +@Stable +class TangemCollapsingAppBarState( + val initialHeightOffset: Float = 0f, + val heightOffsetLimit: Float = 0f, + val partialHeightLimit: Float = heightOffsetLimit, +) : ScrollableState { + + private val _heightOffset = mutableFloatStateOf(initialHeightOffset) + private var deferredConsumption: Float = 0f + + /** + * The current height offset of the app bar. + * This value is updated as the user scrolls, and is constrained between [heightOffsetLimit] and 0f. + */ + var heightOffset: Float + get() = _heightOffset.floatValue + set(newOffset) { + _heightOffset.floatValue = + newOffset.coerceIn(minimumValue = heightOffsetLimit, maximumValue = 0f) + } + + /** + * The fraction of the app bar that is collapsed, calculated as the ratio of [heightOffset] to [heightOffsetLimit]. + */ + val collapsedFraction: Float + get() = + if (heightOffsetLimit != 0f) { + heightOffset / heightOffsetLimit + } else { + 0f + } + + /** + * The current scroll direction of the app bar, which can be Collapsing, Expanding, or Idle. + */ + var direction: TopBapScrollDirection = TopBapScrollDirection.Idle + + private val scrollableState = ScrollableState { value -> + val consume = if (value < 0) { + max(heightOffsetLimit - heightOffset, value) + } else { + min(0f - heightOffset, value) + } + + val current = consume + deferredConsumption + val currentInt = current.toInt() + + if (current.absoluteValue > 0) { + heightOffset += currentInt + deferredConsumption = current - currentInt + } + + consume + } + + override val isScrollInProgress: Boolean + get() = scrollableState.isScrollInProgress + + /** + * + */ + suspend fun collapse() { + AnimationState(initialValue = heightOffset).animateTo( + targetValue = heightOffsetLimit + partialHeightLimit, + animationSpec = tween(), + ) { + heightOffset = value + } + } + + override suspend fun scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit) = + scrollableState.scroll(scrollPriority, block) + + override fun dispatchRawDelta(delta: Float) = scrollableState.dispatchRawDelta(delta) + + companion object { + /** The default [Saver] implementation for [TangemCollapsingAppBarState]. */ + val Saver: Saver = + listSaver( + save = { state -> listOf(state.heightOffsetLimit, state.heightOffset, state.partialHeightLimit) }, + restore = { state -> + TangemCollapsingAppBarState( + heightOffsetLimit = state[0], + partialHeightLimit = state[2], + initialHeightOffset = state[1], + ) + }, + ) + } +} + +/** + * Remembers and saves the state of the collapsing top app bar across recompositions and configuration changes. + */ +@Composable +fun rememberTangemCollapsingAppBarState( + heightOffsetLimit: Float = -Float.MAX_VALUE, + partialHeightLimit: Float = -Float.MAX_VALUE, + initialHeightOffset: Float = 0f, +): TangemCollapsingAppBarState { + return rememberSaveable(saver = Saver) { + TangemCollapsingAppBarState( + initialHeightOffset = initialHeightOffset, + partialHeightLimit = partialHeightLimit, + heightOffsetLimit = heightOffsetLimit, + ) + } +} + +/** + * The scroll direction of the top app bar, which can be Collapsing, Expanding, or Idle. + */ +enum class TopBapScrollDirection { + Collapsing, Expanding, Idle +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt similarity index 98% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt index 1c48f40af5..e613117202 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewData.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt @@ -8,7 +8,7 @@ import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @Suppress("LargeClass") -internal object WalletPreviewData { +internal object WalletPreviewDataLegacy { val topBarConfig by lazy { WalletTopBarConfig(onDetailsClick = {}) } 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 fb85ffc73d..f3019be3a6 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 @@ -15,7 +15,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData.topBarConfig +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt new file mode 100644 index 0000000000..7a61716733 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletPreviewData.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.wallet.presentation.preview + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletActionButtons +import kotlinx.collections.immutable.persistentListOf + +internal object WalletPreviewData { + + val wallets by lazy { + mapOf( + UserWalletId(stringValue = "123") to WalletBalancePreview.content, + UserWalletId(stringValue = "321") to WalletBalancePreview.loading, + UserWalletId(stringValue = "24") to WalletBalancePreview.error, + ) + } + + val actionButtons = persistentListOf( + WalletActionButtons.Buy({}, false).buttonUM, + WalletActionButtons.Swap({}, false).buttonUM, + WalletActionButtons.Sell({}, false).buttonUM, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt new file mode 100644 index 0000000000..32c059a463 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsHint.kt @@ -0,0 +1,89 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.wallet.impl.R + +private const val STARS_INLINE_CONTENT_ID = "stars" + +@Composable +internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { + AnimatedVisibility( + modifier = modifier, + visible = isVisible, + enter = fadeIn(animationSpec = tween(durationMillis = 300)), + exit = fadeOut(animationSpec = tween(durationMillis = 300)), + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Swipe up to explore the market", // todo redesign main lokalise + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.primary, + textAlign = TextAlign.Center, + ) + Text( + text = buildAnnotatedString { + append("Find new hidden gems ") // todo redesign main lokalise + appendInlineContent( + STARS_INLINE_CONTENT_ID, + alternateText = "\uDBC0\uDDBF", + ) + }, + inlineContent = mapOf( + STARS_INLINE_CONTENT_ID to InlineTextContent( + placeholder = Placeholder( + width = TangemTheme.typography2.bodyRegular14.fontSize, + height = TangemTheme.typography2.bodyRegular14.fontSize, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + children = { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_magic_default_24), + tint = TangemTheme.colors2.text.neutral.tertiary, + contentDescription = null, + ) + }, + ), + ), + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.tertiary, + textAlign = TextAlign.Center, + ) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun MarketsHint_Preview() { + TangemThemePreviewRedesign { + MarketsHint( + isVisible = true, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt new file mode 100644 index 0000000000..7eeadda4db --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/MarketsTooltip.kt @@ -0,0 +1,144 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.VisibilityThreshold +import androidx.compose.animation.core.spring +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideIn +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.* +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.sheetscaffold.TangemSheetState +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.MarketTooltipTestTags +import com.tangem.core.ui.utils.lineTo +import com.tangem.core.ui.utils.moveTo +import com.tangem.core.ui.utils.toPx +import com.tangem.feature.wallet.impl.R +import kotlinx.coroutines.delay +import kotlin.math.roundToInt + +@Composable +internal fun MarketsTooltip( + availableHeight: Dp, + bottomSheetState: TangemSheetState, + isVisible: Boolean, + modifier: Modifier = Modifier, +) { + val density = LocalDensity.current + val tooltipOffset by remember { + derivedStateOf { + val bottomSheetOffset = try { + // Can throw exception during the first composition + with(density) { bottomSheetState.requireOffset().toDp() } + } catch (e: Exception) { + 0.dp + } + + bottomSheetOffset - availableHeight + } + } + + var isVisibleWrapped by remember { mutableStateOf(value = false) } + LaunchedEffect(isVisible) { + if (isVisible) { + delay(timeMillis = 300) + } + + isVisibleWrapped = isVisible + } + + val slideOffset = 40.dp.toPx() + AnimatedVisibility( + modifier = modifier + .offset { IntOffset(x = 0, y = tooltipOffset.roundToPx()) } + .testTag(MarketTooltipTestTags.CONTAINER), + visible = isVisibleWrapped, + enter = slideIn( + animationSpec = spring( + stiffness = Spring.StiffnessLow, + visibilityThreshold = IntOffset.VisibilityThreshold, + ), + initialOffset = { _ -> IntOffset(y = -slideOffset.roundToInt(), x = 0) }, + ) + fadeIn(), + exit = fadeOut(), + ) { + MarketsTooltipContent() + } +} + +@Composable +internal fun MarketsTooltipContent(modifier: Modifier = Modifier) { + val backgroundColor = TangemTheme.colors.background.action + val cornerRadius = CornerRadius(x = 14.dp.toPx()) + val tipDpSize = DpSize(width = 20.dp, height = 8.dp) + + Column( + modifier = modifier + .padding(bottom = tipDpSize.height) + .drawBehind { + val rect = size.toRect() + val tipSize = tipDpSize.toSize() + val tipRect = Rect( + offset = Offset( + x = rect.center.x - tipSize.center.x, + y = rect.bottom, + ), + size = tipSize, + ) + drawRoundRect(color = backgroundColor, cornerRadius = cornerRadius) + + val tipPath = Path().apply { + moveTo(tipRect.topLeft) + lineTo(tipRect.bottomCenter) + lineTo(tipRect.topRight) + } + drawPath(color = backgroundColor, path = tipPath) + } + .padding(all = 12.dp), + verticalArrangement = Arrangement.spacedBy(space = 4.dp), + horizontalAlignment = Alignment.Start, + ) { + Text( + text = stringResourceSafe(id = R.string.markets_tooltip_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResourceSafe(id = R.string.markets_tooltip_message), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.secondary, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun MarketsTooltip_Preview() { + TangemThemePreviewRedesign { + MarketsTooltipContent() + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index ea93bb097c..6916bf40c8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -9,14 +9,14 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet import com.tangem.core.ui.components.getDefaultRowColors import com.tangem.core.ui.components.getWarningRowColors import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig import kotlinx.collections.immutable.ImmutableList @@ -64,5 +64,5 @@ private fun ActionsBottomSheetContent_Light( } private class ActionsBottomSheetContentConfigProvider : CollectionPreviewParameterProvider( - collection = listOf(WalletPreviewData.actionsBottomSheet), + collection = listOf(WalletPreviewDataLegacy.actionsBottomSheet), ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt new file mode 100644 index 0000000000..443bd5f959 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletItemBlocks.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components + +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.ui.Modifier +import com.tangem.core.ui.ds.button.TangemButton +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.visa.TangemPayMainScreenBlock + +internal fun LazyListScope.nftCollections2(state: WalletUM, itemModifier: Modifier) { + (state as? WalletUM.Content)?.let { content -> + item(key = "NFTCollections", contentType = "NFTCollections") { + WalletNFTItem( + modifier = itemModifier, + state = content.nftState, + ) + } + } +} + +internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifier) { + val organizeButton = state.tokensListUM.organizeButtonUM + if (organizeButton != null) { + item( + key = "OrganizeTokensButton", + contentType = "OrganizeTokensButton", + ) { + TangemButton( + organizeButton, + modifier = itemModifier, + ) + } + } +} + +internal fun LazyListScope.tangemPay(walletUM: WalletUM, isBalanceHiding: Boolean, modifier: Modifier = Modifier) { + if (walletUM is WalletState.MultiCurrency) { + item( + key = "TangemPayMainScreenBlock", + contentType = walletUM.tangemPayState::class.java, + ) { + TangemPayMainScreenBlock( + state = walletUM.tangemPayState, + isBalanceHidden = isBalanceHiding, + modifier = modifier, + ) + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt index bddfb23fb1..2febad643b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/WalletsList.kt @@ -26,7 +26,7 @@ import com.tangem.core.ui.res.LocalWindowSize import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MainScreenTestTags -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletCard import kotlinx.collections.immutable.ImmutableList @@ -106,7 +106,7 @@ private fun Preview_WalletsList() { TangemThemePreview { WalletsList( lazyListState = rememberLazyListState(), - wallets = WalletPreviewData.wallets.values.toPersistentList(), + wallets = WalletPreviewDataLegacy.wallets.values.toPersistentList(), isBalanceHidden = false, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt new file mode 100644 index 0000000000..87d9cebc62 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt @@ -0,0 +1,205 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.vectorResource +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.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.text.applyBladeBrush +import com.tangem.core.ui.ds.button.SecondaryTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior +import com.tangem.core.ui.ds.topbar.collapsing.snapToExitUntilCollapsed +import com.tangem.core.ui.extensions.orEmpty +import com.tangem.core.ui.extensions.orMaskWithStars +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.MainScreenTestTags +import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview +import com.tangem.feature.wallet.presentation.preview.WalletPreviewData +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach +import kotlinx.collections.immutable.ImmutableList + +private const val MIN_SCALE = 0.75f +private const val MAX_SCALE = 1f + +@Composable +internal fun WalletBalance( + walletBalanceUM: WalletBalanceUM, + behavior: TangemCollapsingAppBarBehavior, + buttons: ImmutableList, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + val collapsedFraction = behavior.state.collapsedFraction + val alpha = 1f - collapsedFraction + val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .alpha(alpha) + .scale(scale) + .snapToExitUntilCollapsed(behavior) + .fillMaxWidth() + .padding(top = 64.dp) + .statusBarsPadding(), + ) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + ) { + Balance( + walletBalanceUM = walletBalanceUM, + isBalanceHidden = isBalanceHidden, + ) + SpacerH(TangemTheme.dimens2.x3) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + ) { + Text( + text = walletBalanceUM.name, + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.tertiary, + ) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + modifier = Modifier.size(TangemTheme.dimens2.x6), + ) + } + } + SpacerH(TangemTheme.dimens2.x2) + ActionButtons(buttons) + SpacerH(TangemTheme.dimens2.x6) + } +} + +@Composable +private fun Balance(walletBalanceUM: WalletBalanceUM, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { + AnimatedContent( + targetState = walletBalanceUM, + label = "Update the balance", + modifier = modifier.testTag(MainScreenTestTags.WALLET_BALANCE), + transitionSpec = { + fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith + fadeOut(animationSpec = tween(durationMillis = 90)) + }, + ) { balanceUM -> + when (balanceUM) { + is WalletBalanceUM.Content -> { + Text( + text = balanceUM.balance.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), + style = TangemTheme.typography2.titleRegular44.applyBladeBrush( + isEnabled = balanceUM.isBalanceFlickering, + textColor = TangemTheme.colors2.text.neutral.primary, + ), + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = TangemTheme.typography2.bodySemibold15.fontSize, + maxFontSize = TangemTheme.typography2.titleRegular44.fontSize, + ), + ) + } + is WalletBalanceUM.Error, + is WalletBalanceUM.Loading, + -> { + TextShimmer( + text = "123456", + style = TangemTheme.typography2.titleRegular44, + radius = TangemTheme.dimens2.x25, + textSizeHeight = true, + ) + } + } + } +} + +@Composable +private fun ActionButtons(buttons: ImmutableList) { + Row( + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + buttons.fastForEach { button -> + key(button.text) { + Column( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2_5), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SecondaryTangemButton( + iconRes = button.iconRes, + onClick = button.onClick, + shape = TangemButtonShape.Rounded, + ) + Text( + text = button.text.orEmpty().resolveReference(), + style = TangemTheme.typography2.bodySemibold15, + color = TangemTheme.colors2.text.neutral.primary, + ) + } + } + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WalletBalance_Preview(@PreviewParameter(WalletBalancePreviewProvider::class) params: WalletBalanceUM) { + TangemThemePreviewRedesign { + WalletBalance( + walletBalanceUM = params, + behavior = rememberTangemExitUntilCollapsedScrollBehavior(), + buttons = WalletPreviewData.actionButtons, + isBalanceHidden = false, + ) + } +} + +private class WalletBalancePreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + WalletBalancePreview.content, + WalletBalancePreview.content.copy(isBalanceFlickering = true), + WalletBalancePreview.loading, + WalletBalancePreview.error, + ) +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 01044070d2..651707a1c3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -12,7 +12,6 @@ import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.* -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.TextAutoSize import androidx.compose.material3.* @@ -45,7 +44,7 @@ import com.tangem.core.ui.res.TangemDimens import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MainScreenTestTags -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDropDownItems @@ -375,22 +374,22 @@ private fun Preview_WalletCard( private class WalletCardStateProvider : CollectionPreviewParameterProvider( collection = listOf( - WalletPreviewData.walletCardContentState, - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState, + WalletPreviewDataLegacy.walletCardContentState.copy( balance = "0.00", ), - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState.copy( title = "Title", additionalInfo = WalletAdditionalInfo( hideable = false, content = TextReference.Str("3 cards"), ), ), - WalletPreviewData.walletCardContentState.copy( + WalletPreviewDataLegacy.walletCardContentState.copy( isBalanceFlickering = true, ), - WalletPreviewData.walletCardLoadingState, - WalletPreviewData.walletCardErrorState, + WalletPreviewDataLegacy.walletCardLoadingState, + WalletPreviewDataLegacy.walletCardErrorState, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 5620f17592..066a167e8d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -1,12 +1,79 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.rememberOverscrollEffect +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.paging.compose.LazyPagingItems +import com.tangem.common.ui.notifications.notifications +import com.tangem.common.ui.notifications.notificationsCarousel import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems +import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems +import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.tokensListItems2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.nftCollections2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.organizeTokens2 +import com.tangem.feature.wallet.presentation.wallet.ui.components.tangemPay +import kotlinx.collections.immutable.toPersistentList + +@Composable +internal fun WalletListContent( + currentWallet: WalletUM, + isBalanceHidden: Boolean, + listState: LazyListState, + contentPadding: PaddingValues, + modifier: Modifier = Modifier, +) { + val containerColor = TangemTheme.colors2.surface.level1 + + val movableItemModifier = Modifier.padding(horizontal = TangemTheme.dimens2.x3) + val itemModifier = movableItemModifier.padding(top = TangemTheme.dimens2.x3) + + LazyColumn( + modifier = modifier, + state = listState, + contentPadding = contentPadding, + horizontalAlignment = Alignment.CenterHorizontally, + overscrollEffect = rememberOverscrollEffect(), + ) { + notifications( + notifications = currentWallet.notifications.map { it.messageUM } + .toPersistentList(), + contentColor = containerColor, + modifier = movableItemModifier, + ) + notificationsCarousel( + containerColor = containerColor, + modifier = movableItemModifier, + notifications = currentWallet.notifications.map { it.messageUM } + .toPersistentList(), + ) + + tangemPay( + walletUM = currentWallet, + isBalanceHiding = isBalanceHidden, + modifier = itemModifier, + ) + + tokensListItems2( + walletTokensListUM = currentWallet.tokensListUM, + modifier = movableItemModifier, + isBalanceHidden = isBalanceHidden, + ) + + nftCollections2(state = currentWallet, itemModifier = itemModifier) + + organizeTokens2(state = currentWallet, itemModifier = itemModifier) + } +} /** * Wallet content diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt new file mode 100644 index 0000000000..d047533bb3 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletPagerIndicator.kt @@ -0,0 +1,49 @@ +package com.tangem.feature.wallet.presentation.wallet.ui.components.common + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.PagerState +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.TangemPagerIndicator +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior + +private const val MIN_SCALE = 0.75f +private const val MAX_SCALE = 1f + +@Composable +internal fun WalletPagerIndicator(pagerState: PagerState, behavior: TangemCollapsingAppBarBehavior) { + val collapsedFraction = behavior.state.collapsedFraction + val alpha = MAX_SCALE - collapsedFraction + val scale = alpha.coerceIn(MIN_SCALE, MAX_SCALE) + + Box( + modifier = Modifier + .graphicsLayer { + scaleY = scale + translationY = behavior.state.heightOffset + } + .fillMaxWidth() + .height( + with(LocalDensity.current) { + behavior.state.heightOffsetLimit.toDp().unaryMinus() + }, + ) + .alpha(alpha), + ) { + TangemPagerIndicator( + pagerState = pagerState, + modifier = Modifier + .padding(top = 248.dp) + .scale(scaleY = 1f, scaleX = scale) + .fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index cb76a6809e..8438399294 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -3,22 +3,77 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import android.content.res.Configuration import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.LocalPowerSavingState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.test.MainScreenTestTags import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.WalletPreviewData +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig +import dev.chrisbanes.haze.HazeProgressive + +private const val VISIBILITY_THRESHOLD = 0.5f + +/** + * Wallet screen collapsing top bar + * + * @param topBarConfig top bar config + * @param walletBalance wallet balance text reference + * @param behavior collapsing behavior + */ +@Composable +internal fun WalletTopBar( + topBarConfig: WalletTopBarConfig, + walletBalance: TextReference?, + behavior: TangemCollapsingAppBarBehavior, +) { + Surface( + color = Color.Unspecified, + contentColor = Color.Unspecified, + modifier = Modifier.hazeEffectTangem { + progressive = + HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) + }, + ) { + val isPowerSaving by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsStateWithLifecycle() + + val wrappedBalance = remember(behavior.state.collapsedFraction) { + if (behavior.state.collapsedFraction > VISIBILITY_THRESHOLD) walletBalance else null + } + + TangemTopBar( + title = wrappedBalance, + startIconRes = R.drawable.ic_tangem_24, + endIconRes = R.drawable.ic_more_default_24, + onEndContentClick = topBarConfig.onDetailsClick, + isGhostButtons = !isPowerSaving, + modifier = Modifier + .testTag(MainScreenTestTags.TOP_BAR), + ) + } +} /** * Wallet screen top bar * * @param config component config */ +@Deprecated("Remove with main toggle [DesignFeatureToggles.isRedesignEnabled]") @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun WalletTopBar(config: WalletTopBarConfig) { @@ -46,6 +101,21 @@ internal fun WalletTopBar(config: WalletTopBarConfig) { @Composable private fun Preview_WalletTopBar() { TangemThemePreview { - WalletTopBar(config = WalletPreviewData.topBarConfig) + WalletTopBar(config = WalletPreviewDataLegacy.topBarConfig) } -} \ No newline at end of file +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun WalletTopBar_Preview() { + TangemThemePreviewRedesign { + WalletTopBar( + topBarConfig = WalletTopBarConfig(onDetailsClick = {}), + walletBalance = stringReference("$ 8923,05"), + behavior = rememberTangemExitUntilCollapsedScrollBehavior(), + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt index d1b702c3b9..d4ff2fd0cb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/utils/LazyListStateExt.kt @@ -4,6 +4,9 @@ import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.lazy.LazyListLayoutInfo import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.mapSaver +import com.tangem.utils.extensions.mapNotNullValues /** * Animate scroll [LazyListState]. @@ -28,4 +31,32 @@ private fun calculateOffset(layoutInfo: LazyListLayoutInfo, prevIndex: Int, newI private fun LazyListLayoutInfo.getItemSizeWithSpacing(): Int { return viewportSize.width - afterContentPadding - beforeContentPadding + mainAxisItemSpacing +} + +/** + * Saver for [LazyListState] map, where key is page index, and value is [LazyListState] of this page. + */ +internal fun lazyListStateMapSaver(pageCount: Int): Saver, Any> { + return mapSaver( + save = { map -> + map.mapKeys { it.key.toString() } + .mapValues { listState -> + listState.value.firstVisibleItemIndex to listState.value.firstVisibleItemScrollOffset + } + }, + restore = { restoredMap -> + @Suppress("UNCHECKED_CAST") + val typedMap = restoredMap as? Map> ?: return@mapSaver null + + typedMap.mapKeys { it.key.toInt() } + .mapNotNullValues { (_, value) -> + val (index, offset) = value + LazyListState(index, offset) + } + .toMutableMap() + .apply { + repeat(pageCount) { putIfAbsent(it, LazyListState()) } + } + }, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml b/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml new file mode 100644 index 0000000000..62d66ecd28 --- /dev/null +++ b/features/wallet/impl/src/main/res/drawable/ic_magic_default_24.xml @@ -0,0 +1,9 @@ + + + From 6cfb8213f02850755f3039fb4b46f463fc437793 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 15:54:11 +0300 Subject: [PATCH 70/97] Updated on 2026-08-14 --- core/ui/build.gradle.kts | 2 +- .../core/ui/ds/row/TangemRowContainer.kt | 2 +- .../core/ui/ds/row/token/TangemTokenRow.kt | 5 +- features/tester/STORYBOOK.md | 45 ++++ .../storybook/entity/StoryBookPage.kt | 31 ++- .../storybook/page/background/Build.kt | 1 + .../page/background/NorthernLightsStory.kt | 2 +- .../storybook/page/badge/Build.kt | 19 ++ .../storybook/page/badge/TangemBadgeStory.kt | 222 +++++++++++++++++ .../storybook/page/buttons/Build.kt | 1 + .../storybook/page/buttons/ButtonsStory.kt | 2 + .../storybook/page/checkbox/Build.kt | 22 ++ .../page/checkbox/TangemCheckboxStory.kt | 137 ++++++++++ .../storybook/page/message/Build.kt | 19 ++ .../page/message/TangemMessageStory.kt | 233 ++++++++++++++++++ .../storybook/page/opportunities/Build.kt | 7 + .../opportunities/OpportunitiesBGStory.kt | 122 +++++++++ .../presentation/storybook/page/tabs/Build.kt | 7 + .../page/tabs/TangemSegmentedPickerStory.kt | 171 +++++++++++++ .../storybook/page/tokenrow/Build.kt | 16 ++ .../page/tokenrow/TangemTokenRowStory.kt | 72 ++++++ .../storybook/ui/StoryBookListScreen.kt | 12 + .../storybook/ui/StoryBookScreen.kt | 19 ++ 23 files changed, 1163 insertions(+), 6 deletions(-) create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/TangemCheckboxStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/TangemMessageStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/OpportunitiesBGStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/TangemTokenRowStory.kt diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 482fb4ddd7..f9b0a562a0 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.navigation) implementation(deps.compose.navigation.hilt) - implementation(deps.compose.reorderable) + api(deps.compose.reorderable) /** Other libraries */ implementation(deps.compose.accompanist.systemUiController) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt index d917b39b7f..026dc41e70 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/TangemRowContainer.kt @@ -46,7 +46,7 @@ fun TangemRowContainer( content = content, modifier = modifier, ) { measurables, constraints -> - val layoutWidth = constraints.maxWidth - contentStartPadding - contentEndPadding + val layoutWidth = max(0, constraints.maxWidth - contentStartPadding - contentEndPadding) val startTopMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt() val startBottomMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt() diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt index da31f1d96a..6aaafb85b9 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/TangemTokenRow.kt @@ -226,7 +226,7 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) private fun TangemTokenRow_Preview( - @PreviewParameter(TangemTokenRowPreviewProvider::class) tokenRowUM: TangemTokenRowUM, + @PreviewParameter(TangemTokenRow_PreviewProvider::class) tokenRowUM: TangemTokenRowUM, ) { TangemThemePreviewRedesign { TangemTokenRow( @@ -238,7 +238,8 @@ private fun TangemTokenRow_Preview( } } -private class TangemTokenRowPreviewProvider : CollectionPreviewParameterProvider( +@Suppress("ClassNaming") +class TangemTokenRow_PreviewProvider : CollectionPreviewParameterProvider( collection = listOf( TangemTokenRowPreviewData.defaultState, TangemTokenRowPreviewData.defaultEllipsisState, diff --git a/features/tester/STORYBOOK.md b/features/tester/STORYBOOK.md index f9142cdd1b..c8dfee51c3 100644 --- a/features/tester/STORYBOOK.md +++ b/features/tester/STORYBOOK.md @@ -171,11 +171,56 @@ Show every meaningful axis of variation in one place: | **Shapes** (Default, Rounded) | One labeled group (`ShapeGroup`) per shape, iterate `TangemButtonShape.entries` | | **Content** (text+icon vs icon-only) | Two columns per row | | **Sizes** | Separate `LazyColumn` item per size group if needed | +| **Styles / Effects** (e.g. `TangemMessageEffect`) | Chip toggle — see below | > **Prefer vertical stacking over horizontal.** A row should contain at most > 2–3 components; more than that overflows on narrow screens. Use > `Modifier.weight(1f)` on columns instead of fixed widths. +### Toggle for style/effect axes + +When a discrete axis (e.g. a visual effect enum) would produce too many full-width +components on one screen, use a **sticky chip-picker** instead of stacking all values. +Make the page **stateful** and store the selected value in the `StoryBookPage` data class. + +``` +┌─────────────────────────────────┐ ← stickyHeader +│ Magic │ Card │ Warning │ None│ ← chip row (EffectToggle) +└─────────────────────────────────┘ + No icon, no buttons + [ message with selected effect ] + With icon + [ message with selected effect ] + … +``` + +**Pattern:** + +1. Add the selected value + callback to the `StoryBookPage` data class: + ```kotlin + internal data class FooStory( + val selectedVariant: Variant, + val onVariantChange: (Variant) -> Unit, + ) : StoryBookPage + ``` +2. Use a stateful `Build.kt` (see [Step 2](#2-create-pagefoobuildk)). +3. In the story composable, add a `stickyHeader` with a chip row: + ```kotlin + stickyHeader("toggle") { + VariantToggle( + selected = state.selectedVariant, + onSelect = state.onVariantChange, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + ``` +4. Each `item` below uses `state.selectedVariant` for the component under test. + +See `TangemMessageStory` for a complete example. + ### Section structure (component grids) Follow the pattern used in `ButtonsStory`: diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt index 5967dd9e21..3fb899ac8e 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -1,11 +1,35 @@ package com.tangem.feature.tester.presentation.storybook.entity +import com.tangem.core.ui.ds.badge.TangemBadgeColor +import com.tangem.core.ui.ds.message.TangemMessageEffect + internal sealed interface StoryBookPage internal data object StoryList : StoryBookPage internal data object ButtonsStory : StoryBookPage +internal data class TangemBadgeStory( + val selectedColor: TangemBadgeColor, + val onColorChange: (TangemBadgeColor) -> Unit, +) : StoryBookPage + +internal data object OpportunitiesBGStory : StoryBookPage + +internal data class TangemCheckboxStory( + val isRoundedChecked: Boolean, + val onRoundedCheckedChange: (Boolean) -> Unit, + val isCircleChecked: Boolean, + val onCircleCheckedChange: (Boolean) -> Unit, +) : StoryBookPage + +internal data object TangemSegmentedPickerStory : StoryBookPage + +internal data class TangemMessageStory( + val selectedEffect: TangemMessageEffect, + val onEffectChange: (TangemMessageEffect) -> Unit, +) : StoryBookPage + internal data class NorthernLightsStory( val variant: Variant, val onVariantChange: (Variant) -> Unit, @@ -14,4 +38,9 @@ internal data class NorthernLightsStory( Shader, Simple, } -} \ No newline at end of file +} + +internal data class TangemTokenRowStory( + val isBalanceHidden: Boolean, + val onBalanceHiddenToggle: () -> Unit, +) : StoryBookPage \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt index 522c3d4ae1..e78069bc35 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/Build.kt @@ -1,3 +1,4 @@ +@file:Suppress("MagicNumber", "LongMethod") package com.tangem.feature.tester.presentation.storybook.page.background import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt index a5764e2be5..3f96ca9d74 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt @@ -1,4 +1,4 @@ -@file:Suppress("MagicNumber") +@file:Suppress("MagicNumber", "LongMethod") package com.tangem.feature.tester.presentation.storybook.page.background import androidx.compose.foundation.background diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/Build.kt new file mode 100644 index 0000000000..28eba8975c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/Build.kt @@ -0,0 +1,19 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.badge + +import com.tangem.core.ui.ds.badge.TangemBadgeColor +import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemBadgeStory { + return TangemBadgeStory( + selectedColor = TangemBadgeColor.Blue, + onColorChange = { color -> + updateStory { it.copy(selectedColor = color) } + }, + ) +} + +internal val tangemBadgeStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt new file mode 100644 index 0000000000..2caf62dae0 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt @@ -0,0 +1,222 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.badge + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory + +private const val STATE_LABEL_WIDTH = 80 + +@Composable +internal fun TangemBadgeStory(state: TangemBadgeStory, modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("color_toggle") { + ColorToggle( + selected = state.selectedColor, + onSelect = state.onColorChange, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + TangemBadgeSize.entries.forEach { size -> + item(size.name) { + BadgeSizeSection(size = size, color = state.selectedColor) + } + } + } +} + +@Composable +private fun ColorToggle( + selected: TangemBadgeColor, + onSelect: (TangemBadgeColor) -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(shape) + .background(TangemTheme.colors2.surface.level2) + .border(width = 1.dp, color = TangemTheme.colors2.border.neutral.secondary, shape = shape) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + TangemBadgeColor.entries.forEach { color -> + ColorChip( + label = color.name, + selected = color == selected, + onClick = { onSelect(color) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun ColorChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background(if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2) + .clickable(onClick = onClick) + .padding(vertical = 8.dp, horizontal = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + ) + } +} + +@Composable +private fun BadgeSizeSection(size: TangemBadgeSize, color: TangemBadgeColor) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = size.name, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + TangemBadgeShape.entries.forEach { shape -> + BadgeShapeGroup(size = size, shape = shape, color = color) + } + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun BadgeShapeGroup(size: TangemBadgeSize, shape: TangemBadgeShape, color: TangemBadgeColor) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = shape.name, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + ColumnHeaderRow() + TangemBadgeType.entries.forEach { type -> + BadgeTypeRow(size = size, shape = shape, color = color, type = type) + } + } +} + +@Composable +private fun ColumnHeaderRow() { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(STATE_LABEL_WIDTH.dp)) + Text( + text = "Text + Icon", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Text", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Icon", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun BadgeTypeRow( + size: TangemBadgeSize, + shape: TangemBadgeShape, + color: TangemBadgeColor, + type: TangemBadgeType, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = type.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.width(STATE_LABEL_WIDTH.dp), + ) + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemBadge( + text = stringReference("New"), + iconRes = R.drawable.ic_information_24, + size = size, + shape = shape, + color = color, + type = type, + iconPosition = TangemBadgeIconPosition.Start, + ) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemBadge( + text = stringReference("New"), + size = size, + shape = shape, + color = color, + type = type, + ) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemBadge( + iconRes = R.drawable.ic_information_24, + size = size, + shape = shape, + color = color, + type = type, + iconPosition = TangemBadgeIconPosition.Start, + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt index 4ea6b7efbb..00af9f20b0 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/Build.kt @@ -1,3 +1,4 @@ +@file:Suppress("MagicNumber", "LongMethod") package com.tangem.feature.tester.presentation.storybook.page.buttons import com.tangem.feature.tester.presentation.storybook.entity.ButtonsStory diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt index d57ebfdd8a..9d9a6a9c09 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/buttons/ButtonsStory.kt @@ -1,3 +1,4 @@ +@file:Suppress("MagicNumber", "LongMethod") package com.tangem.feature.tester.presentation.storybook.page.buttons import androidx.compose.foundation.background @@ -24,6 +25,7 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { contentPadding = PaddingValues(vertical = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier + .statusBarsPadding() .fillMaxSize() .background(TangemTheme.colors2.surface.level1), ) { diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/Build.kt new file mode 100644 index 0000000000..cc54b661b4 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/Build.kt @@ -0,0 +1,22 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.checkbox + +import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemCheckboxStory { + return TangemCheckboxStory( + isRoundedChecked = false, + onRoundedCheckedChange = { checked -> + updateStory { it.copy(isRoundedChecked = checked) } + }, + isCircleChecked = false, + onCircleCheckedChange = { checked -> + updateStory { it.copy(isCircleChecked = checked) } + }, + ) +} + +internal val tangemCheckboxStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/TangemCheckboxStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/TangemCheckboxStory.kt new file mode 100644 index 0000000000..fbba0e7b64 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/checkbox/TangemCheckboxStory.kt @@ -0,0 +1,137 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.checkbox + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.checkbox.TangemCheckbox +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory + +private const val STATE_LABEL_WIDTH = 80 + +@Composable +internal fun TangemCheckboxStory(state: TangemCheckboxStory, modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("grid") { + CheckboxGrid(state = state) + } + } +} + +@Composable +private fun CheckboxGrid(state: TangemCheckboxStory) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + ColumnHeaderRow() + CheckboxRow( + label = "Rounded", + isChecked = state.isRoundedChecked, + onCheckedChange = state.onRoundedCheckedChange, + isRounded = true, + ) + CheckboxRow( + label = "Circle", + isChecked = state.isCircleChecked, + onCheckedChange = state.onCircleCheckedChange, + isRounded = false, + ) + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun ColumnHeaderRow() { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(Modifier.width(STATE_LABEL_WIDTH.dp)) + Text( + text = "Enabled", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Disabled", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Disabled (on)", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + } +} + +@Composable +private fun CheckboxRow(label: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, isRounded: Boolean) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.width(STATE_LABEL_WIDTH.dp), + ) + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemCheckbox( + isChecked = isChecked, + isRounded = isRounded, + isEnabled = true, + onCheckedChange = onCheckedChange, + ) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemCheckbox( + isChecked = false, + isRounded = isRounded, + isEnabled = false, + onCheckedChange = {}, + ) + } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemCheckbox( + isChecked = true, + isRounded = isRounded, + isEnabled = false, + onCheckedChange = {}, + ) + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/Build.kt new file mode 100644 index 0000000000..8f357d4941 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/Build.kt @@ -0,0 +1,19 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.message + +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemMessageStory { + return TangemMessageStory( + selectedEffect = TangemMessageEffect.None, + onEffectChange = { newEffect -> + updateStory { it.copy(selectedEffect = newEffect) } + }, + ) +} + +internal val tangemMessageStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/TangemMessageStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/TangemMessageStory.kt new file mode 100644 index 0000000000..f43e8ec903 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/message/TangemMessageStory.kt @@ -0,0 +1,233 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.message + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +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.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.message.* +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun TangemMessageStory(state: TangemMessageStory, modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxSize() + .statusBarsPadding() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("effect_toggle") { + EffectToggle( + selected = state.selectedEffect, + onSelect = state.onEffectChange, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + + item("plain") { + VariantSection(label = "No icon, no buttons") { + TangemMessage( + messageUM = TangemMessageUM( + id = "plain", + title = stringReference("Update available"), + subtitle = stringReference("A new firmware version is ready to install on your card."), + messageEffect = state.selectedEffect, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("icon") { + VariantSection(label = "With icon") { + TangemMessage( + messageUM = TangemMessageUM( + id = "icon", + title = stringReference("Wallet backup missing"), + subtitle = stringReference("To protect your assets, complete the backup process."), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("centered") { + VariantSection(label = "Centered") { + TangemMessage( + messageUM = TangemMessageUM( + id = "centered", + title = stringReference("Scan your card"), + subtitle = stringReference("Hold the card to the back of your phone."), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + isCentered = true, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("1btn") { + VariantSection(label = "With 1 button") { + TangemMessage( + messageUM = TangemMessageUM( + id = "1btn", + title = stringReference("Generate addresses"), + subtitle = stringReference("Generate addresses for 2 new networks using your card."), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = stringReference("Generate"), + type = TangemButtonType.Primary, + iconRes = R.drawable.ic_tangem_24, + onClick = {}, + ), + ), + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("2btn") { + VariantSection(label = "With 2 buttons") { + TangemMessage( + messageUM = TangemMessageUM( + id = "2btn", + title = stringReference("Rate the app"), + subtitle = stringReference("How do you like Tangem so far?"), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + buttonsUM = persistentListOf( + TangemMessageButtonUM( + text = stringReference("Love it!"), + type = TangemButtonType.PrimaryInverse, + onClick = {}, + ), + TangemMessageButtonUM( + text = stringReference("Can be better"), + type = TangemButtonType.Primary, + onClick = {}, + ), + ), + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("close") { + VariantSection(label = "With close button") { + TangemMessage( + messageUM = TangemMessageUM( + id = "close", + title = stringReference("Note top up"), + subtitle = stringReference("To activate the card, top it up with at least 1 XLM."), + messageEffect = state.selectedEffect, + iconUM = TangemIconUM.Icon(R.drawable.ic_attention_default_24), + onCloseClick = {}, + ), + modifier = Modifier.fillMaxWidth(), + ) + } + } + } +} + +@Composable +private fun EffectToggle( + selected: TangemMessageEffect, + onSelect: (TangemMessageEffect) -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(shape) + .background(TangemTheme.colors2.surface.level2) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.secondary, + shape = shape, + ) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + TangemMessageEffect.entries.forEach { effect -> + EffectChip( + label = effect.name, + selected = effect == selected, + onClick = { onSelect(effect) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun EffectChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background( + if (selected) { + TangemTheme.colors2.surface.level3 + } else { + TangemTheme.colors2.surface.level2 + }, + ) + .clickable(onClick = onClick) + .padding(vertical = 8.dp, horizontal = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = if (selected) { + TangemTheme.colors.text.primary1 + } else { + TangemTheme.colors.text.secondary + }, + ) + } +} + +@Composable +private fun VariantSection(label: String, content: @Composable ColumnScope.() -> Unit) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + content() + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/Build.kt new file mode 100644 index 0000000000..12f5d224e7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/Build.kt @@ -0,0 +1,7 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.opportunities + +import com.tangem.feature.tester.presentation.storybook.entity.OpportunitiesBGStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal val opportunitiesBGStoryFactory: StoryPageFactory = StoryPageFactory { OpportunitiesBGStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/OpportunitiesBGStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/OpportunitiesBGStory.kt new file mode 100644 index 0000000000..ca09305652 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/opportunities/OpportunitiesBGStory.kt @@ -0,0 +1,122 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.opportunities + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +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.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.opportunities.OpportunitiesBG +import com.tangem.core.ui.res.TangemTheme + +private data class IconVariant( + val label: String, + val icon: TangemIconUM, +) + +private val variants = listOf( + IconVariant( + label = "Bitcoin", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_btc_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), + IconVariant( + label = "Solana", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_solana_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), + IconVariant( + label = "Avalanche", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_avalanche_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), + IconVariant( + label = "BNB Smart Chain", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_bsc_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), + IconVariant( + label = "Cardano", + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = R.drawable.img_cardano_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), + ), +) + +@Composable +internal fun OpportunitiesBGStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + variants.forEach { variant -> + item(variant.label) { + OpportunitiesBG( + icon = variant.icon, + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 24.dp), + ) { + TangemIcon( + tangemIconUM = variant.icon, + modifier = Modifier.size(40.dp), + ) + Text( + text = variant.label, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/Build.kt new file mode 100644 index 0000000000..e8dd4f694f --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/Build.kt @@ -0,0 +1,7 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.tabs + +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import com.tangem.feature.tester.presentation.storybook.entity.TangemSegmentedPickerStory + +internal val tangemSegmentedPickerStoryFactory: StoryPageFactory = StoryPageFactory { TangemSegmentedPickerStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt new file mode 100644 index 0000000000..df03dcb6d4 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tabs/TangemSegmentedPickerStory.kt @@ -0,0 +1,171 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.tabs + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.tabs.TangemSegmentUM +import com.tangem.core.ui.ds.tabs.TangemSegmentedPicker +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +private val items2 = persistentListOf( + TangemSegmentUM(id = "all", title = stringReference("All")), + TangemSegmentUM(id = "tokens", title = stringReference("Tokens")), +) + +private val items3 = persistentListOf( + TangemSegmentUM(id = "1d", title = stringReference("1D")), + TangemSegmentUM(id = "1w", title = stringReference("1W")), + TangemSegmentUM(id = "1m", title = stringReference("1M")), +) + +private val items4 = persistentListOf( + TangemSegmentUM(id = "1d", title = stringReference("1D")), + TangemSegmentUM(id = "1w", title = stringReference("1W")), + TangemSegmentUM(id = "1m", title = stringReference("1M")), + TangemSegmentUM(id = "1y", title = stringReference("1Y")), +) + +private val items5 = persistentListOf( + TangemSegmentUM(id = "send", title = stringReference("Send")), + TangemSegmentUM(id = "receive", title = stringReference("Receive")), + TangemSegmentUM(id = "swap", title = stringReference("Swap")), + TangemSegmentUM(id = "buy", title = stringReference("Buy")), + TangemSegmentUM(id = "sell", title = stringReference("Sell")), +) + +private data class PickerConfig( + val label: String, + val hasSeparator: Boolean, + val isFixed: Boolean, +) + +private val configs = listOf( + PickerConfig("Default", hasSeparator = false, isFixed = false), + PickerConfig("Separator", hasSeparator = true, isFixed = false), + PickerConfig("Fixed", hasSeparator = false, isFixed = true), + PickerConfig("Fixed + Separator", hasSeparator = true, isFixed = true), +) + +@Composable +internal fun TangemSegmentedPickerStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("default_surface") { + PickerSection( + title = "Default surface", + isAltSurface = false, + background = TangemTheme.colors2.surface.level1, + ) + } + item("alt_surface") { + PickerSection( + title = "Alt surface", + isAltSurface = true, + background = TangemTheme.colors2.surface.level2, + ) + } + item("segment_count") { + SegmentCountSection() + } + } +} + +@Composable +private fun PickerSection(title: String, isAltSurface: Boolean, background: Color) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .background(background) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + configs.forEach { config -> + PickerRow(config = config, isAltSurface = isAltSurface) + } + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun PickerRow(config: PickerConfig, isAltSurface: Boolean) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + text = config.label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + TangemSegmentedPicker( + items = items4, + hasSeparator = config.hasSeparator, + isFixed = config.isFixed, + isAltSurface = isAltSurface, + onClick = {}, + modifier = if (config.isFixed) Modifier.fillMaxWidth() else Modifier, + ) + } +} + +@Composable +private fun SegmentCountSection() { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = "Segment count", + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + SegmentCountRow(label = "2 segments", items = items2) + SegmentCountRow(label = "3 segments", items = items3) + SegmentCountRow(label = "4 segments", items = items4) + SegmentCountRow(label = "5 segments", items = items5) + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun SegmentCountRow(label: String, items: ImmutableList) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + TangemSegmentedPicker( + items = items, + isFixed = true, + onClick = {}, + modifier = Modifier.fillMaxWidth(), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/Build.kt new file mode 100644 index 0000000000..988f75af8a --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/Build.kt @@ -0,0 +1,16 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.tokenrow + +import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemTokenRowStory { + return TangemTokenRowStory( + isBalanceHidden = false, + onBalanceHiddenToggle = { updateStory { it.copy(isBalanceHidden = !it.isBalanceHidden) } }, + ) +} + +internal val tangemTokenRowStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/TangemTokenRowStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/TangemTokenRowStory.kt new file mode 100644 index 0000000000..65c44d281d --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tokenrow/TangemTokenRowStory.kt @@ -0,0 +1,72 @@ +@file:Suppress("MagicNumber", "LongMethod") +package com.tangem.feature.tester.presentation.storybook.page.tokenrow + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.row.token.TangemTokenRow +import com.tangem.core.ui.ds.row.token.TangemTokenRow_PreviewProvider +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory + +@Composable +internal fun TangemTokenRowStory(state: TangemTokenRowStory, modifier: Modifier = Modifier) { + val rows = remember { TangemTokenRow_PreviewProvider().values.toList() } + + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("balance_toggle") { + BalanceToggle( + isHidden = state.isBalanceHidden, + onToggle = state.onBalanceHiddenToggle, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + + items(rows, key = { it.id }) { um -> + TangemTokenRow( + tokenRowUM = um, + isBalanceHidden = state.isBalanceHidden, + reorderableTokenListState = null, + modifier = Modifier.background(TangemTheme.colors2.surface.level1), + ) + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(start = 16.dp), + ) + } + } +} + +@Composable +private fun BalanceToggle(isHidden: Boolean, onToggle: () -> Unit, modifier: Modifier = Modifier) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier, + ) { + Text( + text = "isBalanceHidden", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Switch(checked = isHidden, onCheckedChange = { onToggle() }) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt index 4dc59eac12..0aa6cb3681 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt @@ -16,13 +16,25 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory import com.tangem.feature.tester.presentation.storybook.page.background.northernLightsStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.badge.tangemBadgeStoryFactory import com.tangem.feature.tester.presentation.storybook.page.buttons.buttonsStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.opportunities.opportunitiesBGStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.message.tangemMessageStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.checkbox.tangemCheckboxStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.tabs.tangemSegmentedPickerStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.tokenrow.tangemTokenRowStoryFactory private data class StoryItem(val title: String, val factory: StoryPageFactory) private fun buildStories() = listOf( StoryItem(title = "🔘 Buttons", factory = buttonsStoryFactory), + StoryItem(title = "🏷️ Badge", factory = tangemBadgeStoryFactory), + StoryItem(title = "✨ Opportunities BG", factory = opportunitiesBGStoryFactory), StoryItem(title = "🌌 Northern Lights Background", factory = northernLightsStoryFactory), + StoryItem(title = "💬 Message", factory = tangemMessageStoryFactory), + StoryItem(title = "🗂️ Segmented Picker", factory = tangemSegmentedPickerStoryFactory), + StoryItem(title = "☑️ Checkbox", factory = tangemCheckboxStoryFactory), + StoryItem(title = "🪙 Token Row", factory = tangemTokenRowStoryFactory), ) @Composable diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt index 1806965ad7..11a78b7e28 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -6,10 +6,22 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.tangem.feature.tester.presentation.storybook.entity.ButtonsStory import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.entity.OpportunitiesBGStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM import com.tangem.feature.tester.presentation.storybook.entity.StoryList +import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemSegmentedPickerStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.page.badge.TangemBadgeStory import com.tangem.feature.tester.presentation.storybook.page.buttons.ButtonsStory +import com.tangem.feature.tester.presentation.storybook.page.opportunities.OpportunitiesBGStory +import com.tangem.feature.tester.presentation.storybook.page.checkbox.TangemCheckboxStory +import com.tangem.feature.tester.presentation.storybook.page.message.TangemMessageStory +import com.tangem.feature.tester.presentation.storybook.page.tabs.TangemSegmentedPickerStory +import com.tangem.feature.tester.presentation.storybook.page.tokenrow.TangemTokenRowStory @Composable internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) { @@ -17,12 +29,19 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) AnimatedContent( targetState = state.currentPage, + contentKey = { it::class }, modifier = modifier, ) { storyState -> when (storyState) { StoryList -> StoryBookListScreen(state = state) is NorthernLightsStory -> NorthernLightsStory(state = storyState) ButtonsStory -> ButtonsStory() + is TangemBadgeStory -> TangemBadgeStory(state = storyState) + OpportunitiesBGStory -> OpportunitiesBGStory() + is TangemMessageStory -> TangemMessageStory(state = storyState) + is TangemCheckboxStory -> TangemCheckboxStory(state = storyState) + TangemSegmentedPickerStory -> TangemSegmentedPickerStory() + is TangemTokenRowStory -> TangemTokenRowStory(state = storyState) } } } \ No newline at end of file From 0858748471cacf3f5c59a525c58b65abd5d5ea29 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 16:05:34 +0300 Subject: [PATCH 71/97] Updated on 2026-08-14 --- .../com/tangem/scenarios/BaseScenarios.kt | 10 ----- .../kotlin/com/tangem/tests/DetailsTest.kt | 2 +- .../kotlin/com/tangem/tests/FeedbackTest.kt | 44 +++---------------- .../kotlin/com/tangem/tests/OnboardingTest.kt | 10 +---- .../kotlin/com/tangem/tests/ResetCardTest.kt | 2 +- .../kotlin/com/tangem/tests/ScanCardTest.kt | 8 ++-- .../kotlin/com/tangem/tests/WarningTest.kt | 2 +- 7 files changed, 14 insertions(+), 64 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index bc4f4593d7..09f4b7bd1e 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -5,7 +5,6 @@ import com.tangem.common.BaseTestCase import com.tangem.common.extensions.clickWithAssertion import com.tangem.domain.models.scan.ProductType import com.tangem.screens.* -import com.tangem.screens.AlreadyUsedWalletDialogPageObject.thisIsMyWalletButton import com.tangem.tap.domain.sdk.mocks.MockContent import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.utils.StringsSigns.DASH_SIGN @@ -14,7 +13,6 @@ import io.qameta.allure.kotlin.Allure.step fun BaseTestCase.scanCard( productType: ProductType? = null, mockContent: MockContent? = null, - alreadyActivatedDialogIsShown: Boolean = false, isTwinsCard: Boolean = false, ) { if (productType != null) { @@ -32,12 +30,6 @@ fun BaseTestCase.scanCard( step("Click on 'Scan card or ring' button") { onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() } } - if (alreadyActivatedDialogIsShown) { - step("Click on 'This is my wallet' button") { - waitForIdle() - AlreadyUsedWalletDialogPageObject { thisIsMyWalletButton.click() } - } - } if (isTwinsCard) { step("Click on 'Continue' button") { onOnboardingScreen { continueButton.clickWithAssertion() } @@ -51,14 +43,12 @@ fun BaseTestCase.scanCard( fun BaseTestCase.openMainScreen( productType: ProductType? = null, mockContent: MockContent? = null, - alreadyActivatedDialogIsShown: Boolean = false, isTwinsCard: Boolean = false, ) { step("Scan card") { scanCard( productType = productType, mockContent = mockContent, - alreadyActivatedDialogIsShown = alreadyActivatedDialogIsShown, isTwinsCard = isTwinsCard, ) } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt index c084d381c8..54604ff5cf 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/DetailsTest.kt @@ -64,7 +64,7 @@ class DetailsTest : BaseTestCase() { fun wallet2DetailsTest() = setupHooks().run { step("Open 'Main Screen'") { - openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true) + openMainScreen(productType = ProductType.Wallet2) } onTopBar { step("Open wallet details") { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt index 0a20f32754..a1cc2319c9 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/FeedbackTest.kt @@ -6,11 +6,12 @@ import com.tangem.common.constants.TestConstants.RECIPIENT_ADDRESS import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT import com.tangem.common.core.TangemSdkError import com.tangem.common.extensions.clickWithAssertion -import com.tangem.domain.models.scan.ProductType import com.tangem.domain.redux.StateDialog -import com.tangem.scenarios.* +import com.tangem.scenarios.checkFailedTransactionDialog +import com.tangem.scenarios.checkScanWarningDialog +import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.synchronizeAddresses import com.tangem.screens.* -import com.tangem.screens.AlreadyUsedWalletDialogPageObject.requestSupportButton import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.domain.sdk.mocks.MockProvider import com.tangem.tap.store @@ -96,7 +97,7 @@ class FeedbackTest : BaseTestCase() { step("Click 'Next' button") { onSendAddressScreen { nextButton.clickWithAssertion() } } - step("Assert sanding text is displayed") { + step("Assert sеnding text is displayed") { onSendConfirmScreen { sendingText.assertIsDisplayed() } } step("Click 'Send' button") { @@ -163,39 +164,4 @@ class FeedbackTest : BaseTestCase() { } } } - - @AllureId("3986") - @DisplayName("Send feedback: from scan already used wallet alert dialog") - @Test - fun sendFeedbackAfterScanAlreadyUsedWalletTest() { - val gmailText = "Welcome to Gmail" - - setupHooks( - additionalAfterSection = { - device.uiDevice.pressBack() - } - ).run { - step("Set mocks for Wallet2") { - MockProvider.setMocks(ProductType.Wallet2) - } - step("Click on 'Accept' button") { - onDisclaimerScreen { acceptButton.clickWithAssertion() } - } - step("Click on 'Get started' button") { - onStoriesScreen { getStartedButton.clickWithAssertion() } - } - step("Click on 'Scan card or ring' button") { - onCreateWalletStartScreen { scanCardOrRingButton.clickWithAssertion() } - } - step("Check 'Already used Wallet' dialog") { - checkAlreadyUsedWalletDialog() - } - step("Click on 'Request support' button") { - AlreadyUsedWalletDialogPageObject { requestSupportButton.click() } - } - step("Assert 'Gmail' app is open") { - ThirdPartyAppPageObject { assertElementWithTextExists(gmailText) } - } - } - } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt index c1e7d608de..26238b9789 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OnboardingTest.kt @@ -30,10 +30,7 @@ class OnboardingTest : BaseTestCase() { @Test fun shibaBackupScreenTest() { setupHooks().run { - scanCard( - mockContent = ShibaNoBackupMockContent, - alreadyActivatedDialogIsShown = true - ) + scanCard(mockContent = ShibaNoBackupMockContent) checkBackupScreen() } } @@ -54,10 +51,7 @@ class OnboardingTest : BaseTestCase() { @Test fun wallet2BackupScreenTest() { setupHooks().run { - scanCard( - mockContent = Wallet2NoBackupMockContent, - alreadyActivatedDialogIsShown = true - ) + scanCard(mockContent = Wallet2NoBackupMockContent) checkBackupScreen() } } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt index 50a5f44b6d..60c2157e1a 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/ResetCardTest.kt @@ -64,7 +64,7 @@ class ResetCardTest : BaseTestCase() { fun resetWallet2CardWithBackupTest() { setupHooks().run { step("Open 'Main Screen'") { - openMainScreen(productType = ProductType.Wallet2, alreadyActivatedDialogIsShown = true) + openMainScreen(productType = ProductType.Wallet2) } step("Open 'Device settings' screen") { openDeviceSettingsScreen() diff --git a/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt index 856a3963b3..1b83533fcb 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/ScanCardTest.kt @@ -73,7 +73,7 @@ class ScanCardTest : BaseTestCase() { setupHooks().run { step("Open 'Main Screen' on $card") { - openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true) + openMainScreen(mockContent = cardType) } step("Check 'Main' screen for $card curve with devices count = '$devicesCount'") { checkMultiCurrencyMainScreen( @@ -119,7 +119,7 @@ class ScanCardTest : BaseTestCase() { setupHooks().run { step("Open 'Main Screen' on '$card' card") { - openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true) + openMainScreen(mockContent = cardType) } step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") { checkMultiCurrencyMainScreen(devicesCount, cardName) @@ -138,7 +138,7 @@ class ScanCardTest : BaseTestCase() { setupHooks().run { step("Open 'Main Screen' on '$ring'") { - openMainScreen(productType = cardType, alreadyActivatedDialogIsShown = true) + openMainScreen(productType = cardType) } step("Check 'Main' screen for '$ring' with devices count = '$devicesCount'") { checkMultiCurrencyMainScreen(devicesCount, cardName) @@ -175,7 +175,7 @@ class ScanCardTest : BaseTestCase() { setupHooks().run { step("Open 'Main Screen' on '$card' card") { - openMainScreen(mockContent = cardType, alreadyActivatedDialogIsShown = true) + openMainScreen(mockContent = cardType) } step("Check 'Main' screen for '$card' card with devices count = '$devicesCount'") { checkMultiCurrencyMainScreen(devicesCount, cardName) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt index cf0024f8bb..8bbaf0ed91 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/WarningTest.kt @@ -75,7 +75,7 @@ class WarningTest : BaseTestCase() { } ).run { step("Open 'Main' screen") { - openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent, alreadyActivatedDialogIsShown = true) + openMainScreen(mockContent = Wallet2WithSeedPhraseMockContent) } step("Assert 'Seed phrase' notification icon is displayed") { onMainScreen { seedPhraseNotificationIcon.assertIsDisplayed() } From 2de45591bd784bd5b69aff06bad3955a4d3d95b0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 16:14:42 +0300 Subject: [PATCH 72/97] Updated on 2026-08-14 --- .../model/AvailableSwapPairsModel.kt | 24 +++++++++++++++++++ .../tokenlist/model/OnrampTokenListModel.kt | 22 +++++++++++++++-- .../onramp/utils/ClearSearchBarTransformer.kt | 17 +++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ClearSearchBarTransformer.kt 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 72fd13b5bb..5457e2c0fe 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 @@ -63,6 +63,7 @@ import com.tangem.features.onramp.tokenlist.entity.TokenListUMController import com.tangem.features.onramp.tokenlist.entity.TokenListUMTransformer import com.tangem.features.onramp.tokenlist.entity.transformer.* import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory +import com.tangem.features.onramp.utils.ClearSearchBarTransformer import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -163,6 +164,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } initializeSearchBarCallbacks() + subscribeOnSelectedStatusChange() subscribeOnAvailablePairsUpdates() if (swapFeatureToggles.isMarketListFeatureEnabled) { @@ -195,6 +197,13 @@ internal class AvailableSwapPairsModel @Inject constructor( .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) } + private fun subscribeOnSelectedStatusChange() { + params.selectedStatus + .filter { it == null } + .onEach { clearSearchState() } + .launchIn(modelScope) + } + private fun initializeSearchBarCallbacks() { tokenListUMController.update( transformer = UpdateSearchBarCallbacksTransformer( @@ -575,9 +584,22 @@ internal class AvailableSwapPairsModel @Inject constructor( isSearched = state.value.searchBarUM.query.isNotEmpty(), ), ) + clearSearchState() params.onTokenClick(tokenItem, status) } + private fun clearSearchState() { + tokenListUMController.update( + transformer = ClearSearchBarTransformer( + placeHolder = resourceReference(id = R.string.common_search), + ), + ) + modelScope.launch { + searchManager.update("") + } + searchQueryStateForMarkets.value = "" + } + private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", @@ -695,6 +717,8 @@ internal class AvailableSwapPairsModel @Inject constructor( ), ) + clearSearchState() + // Trigger re-fetch of available pairs (clears cache + re-enters collectLatest) refreshPairsTrigger.emit(Unit) 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 a90f9e6e25..9d1a10ff99 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 @@ -5,6 +5,7 @@ import com.tangem.common.ui.notifications.NotificationUM 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.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles @@ -36,6 +37,7 @@ import com.tangem.features.onramp.swap.entity.AccountCurrencyUM import com.tangem.features.onramp.tokenlist.OnrampTokenListComponent import com.tangem.features.onramp.tokenlist.entity.* import com.tangem.features.onramp.tokenlist.entity.transformer.* +import com.tangem.features.onramp.utils.ClearSearchBarTransformer import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer import com.tangem.features.onramp.utils.UpdateSearchQueryTransformer @@ -120,7 +122,7 @@ internal class OnrampTokenListModel @Inject constructor( UpdateTokenItemsTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onTokenClick, statuses = filterByQueryTokenList.let { statuses -> if (hasRestrictionForSell || isInsufficientBalanceForSell) { mapOf(false to statuses) @@ -174,7 +176,7 @@ internal class OnrampTokenListModel @Inject constructor( updateTokenListUM( UpdateAccountTokenListTransformer( appCurrency = appCurrency, - onItemClick = params.onTokenClick, + onItemClick = ::onTokenClick, accountList = filterByQueryAccountList.filterByAvailability(), isBalanceHidden = isBalanceHidden, unavailableErrorText = getUnavailableTokensHeaderReference(), @@ -275,6 +277,22 @@ internal class OnrampTokenListModel @Inject constructor( } } + private fun onTokenClick(tokenItemState: TokenItemState, status: CryptoCurrencyStatus) { + clearSearchState() + params.onTokenClick(tokenItemState, status) + } + + private fun clearSearchState() { + tokenListUMController.update( + transformer = ClearSearchBarTransformer( + placeHolder = resourceReference(id = R.string.common_search), + ), + ) + modelScope.launch { + searchManager.update("") + } + } + private fun onSearchQueryChange(newQuery: String) { val searchBar = state.value.searchBarUM if (searchBar.query == newQuery) return diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ClearSearchBarTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ClearSearchBarTransformer.kt new file mode 100644 index 0000000000..e38f56cf73 --- /dev/null +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/utils/ClearSearchBarTransformer.kt @@ -0,0 +1,17 @@ +package com.tangem.features.onramp.utils + +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.extensions.TextReference + +internal class ClearSearchBarTransformer( + private val placeHolder: TextReference, +) : SearchBarUMTransformer() { + + override fun transform(prevState: SearchBarUM): SearchBarUM { + return prevState.copy( + query = "", + isActive = false, + placeholderText = placeHolder, + ) + } +} \ No newline at end of file From 385c853d0c6a6aa7f7ef9a340b0304f8196e1b1b Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 16:15:26 +0300 Subject: [PATCH 73/97] Updated on 2026-08-14 --- .../feature/swap/converters/TokensDataConverterV2.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt index 668b30aa88..1804474ea2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt @@ -39,10 +39,14 @@ internal class TokensDataConverterV2( tokensListData = if (isAccountsMode) { val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() val totalTokensCount = portfolioList.sumOf { it.tokens.size } - TokenListUMData.AccountList( - tokensList = portfolioList, - totalTokensCount = totalTokensCount, - ) + if (totalTokensCount > 0) { + TokenListUMData.AccountList( + tokensList = portfolioList, + totalTokensCount = totalTokensCount, + ) + } else { + TokenListUMData.EmptyList + } } else { val tokensList = accountList.flatMap { (_, currencyList) -> currencyList.asSequence().map { accountSwapCurrency -> From b4ce6c0c2477847a964050eb8165f0b25896bcfb Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 16:15:46 +0300 Subject: [PATCH 74/97] Updated on 2026-08-14 --- .../account/createedit/AccountCreateEditModel.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 4b69fc806d..5870bd4851 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -1,6 +1,7 @@ package com.tangem.features.account.createedit import androidx.annotation.StringRes +import com.tangem.common.routing.AppRoute import com.tangem.common.ui.account.AccountNameUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toDomain @@ -23,6 +24,7 @@ import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase +import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId @@ -134,10 +136,14 @@ internal class AccountCreateEditModel @Inject constructor( result .onLeft { error -> handleAddAccountError(error, derivationIndex.value) } - .onRight { + .onRight { account -> analyticsEventHandler.send(WalletSettingsAccountAnalyticEvents.AccountCreated()) showMessage(R.string.account_create_success_message) - router.pop() + val route = AppRoute.ManageTokens( + source = AppRoute.ManageTokens.Source.ACCOUNT, + portfolioId = PortfolioId(account.accountId), + ) + router.replaceCurrent(route) } } From 8971ebef55487364121f65401e51c059faafc731 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 15:25:39 +0200 Subject: [PATCH 75/97] Updated on 2026-08-14 --- app/build.gradle.kts | 9 --------- .../extension/AppExtensionConfigurations.kt | 6 ++++++ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 3c4c234aa7..9644d056cb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,15 +73,6 @@ android { } } - buildTypes { - debug { - buildConfigField("String", "BUILD_TYPE", "\"debug\"") - } - release { - buildConfigField("String", "BUILD_TYPE", "\"release\"") - } - } - } configurations.all { diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt index e60739fd69..96995ee158 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/AppExtensionConfigurations.kt @@ -24,6 +24,8 @@ private fun AppExtension.configureDefaultConfig(project: Project) { minSdk = AppConfig.minSdkVersion targetSdk = AppConfig.targetSdkVersion + ndk.abiFilters += listOf("armeabi-v7a", "arm64-v8a") + versionCode = if (project.hasProperty("versionCode")) { (project.property("versionCode") as String).toInt() } else { @@ -70,6 +72,7 @@ private fun AppExtension.configureBuildTypes() { } private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, buildType: BuildType) { + val x86_64 = "x86_64" when (buildType) { BuildType.Release -> { isDebuggable = false @@ -77,6 +80,7 @@ private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, b BuildType.Debug -> { isDebuggable = true signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) + ndk.abiFilters += x86_64 } BuildType.Internal, BuildType.External @@ -84,12 +88,14 @@ private fun AndroidBuildType.configureBuildVariant(appExtension: AppExtension, b initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) + ndk.abiFilters += x86_64 } BuildType.Mocked -> { initWith(appExtension.buildTypes.getByName(BuildType.Release.id)) matchingFallbacks.add(BuildType.Release.id) signingConfig = appExtension.signingConfigs.getByName(BuildType.Debug.id) isDebuggable = true + ndk.abiFilters += x86_64 } } From fe935fdba083312cab126737aac54a1e9c09ee73 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 18:13:45 +0500 Subject: [PATCH 76/97] Updated on 2026-08-14 --- .../intents/WalletContentClickIntents.kt | 1 + .../common/preview/WalletScreenPreviewData.kt | 262 +++----------- .../preview/WalletScreenPreviewDataLegacy.kt | 245 +++++++++++++ .../preview/WalletBalancePreview.kt | 9 + .../wallet/state/model/WalletBalanceUM.kt | 1 + .../wallet/state/model/WalletEvent.kt | 2 + .../SetTokenListErrorTransformer.kt | 8 + .../transformers/SetTokenListTransformer.kt | 4 +- .../MultiWalletBalanceUMTransformer.kt | 8 + .../WalletTokenAccountItemConverter.kt | 130 +++++++ ...kt => WalletTokenCurrencyItemConverter.kt} | 267 ++------------ .../converter/WalletTokensListUMConverter.kt | 166 +++++++++ .../state/utils/UserWalletConverterExt.kt | 4 + .../wallet/ui/WalletEventEffect.kt | 50 ++- .../presentation/wallet/ui/WalletScreen.kt | 10 +- .../presentation/wallet/ui/WalletScreen2.kt | 338 ++++++++++++------ 16 files changed, 933 insertions(+), 572 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenAccountItemConverter.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/{WalletTokensListUMTransformer.kt => WalletTokenCurrencyItemConverter.kt} (53%) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt 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 f4769cd5bf..4e6ddfac2c 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 @@ -201,6 +201,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onAccountExpandClick(account: Account) { analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountShowTokens()) accountDependencies.expandedAccountsHolder.expandAccount(account.accountId) + walletEventSender.send(WalletEvent.CollapseBalance) } override fun onAccountCollapseClick(account: Account) { 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 f3019be3a6..3f563dbb3d 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 @@ -2,243 +2,97 @@ package com.tangem.feature.wallet.presentation.common.preview import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.marketprice.PriceChangeType -import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState -import com.tangem.core.ui.components.token.AccountItemPreviewData -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM -import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.event.consumedEvent -import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig +import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview +import com.tangem.feature.wallet.presentation.preview.WalletPreviewData 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( + + private val tokenRowDefault = TangemTokenRowUM.Content( id = "1", - iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), - subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( - price = "34 496,75 \$", - priceChangePercent = "0,43 %", - type = PriceChangeType.DOWN, + headIconUM = TangemIconUM.Currency(CurrencyIconState.Loading), + titleUM = TangemTokenRowUM.TitleUM.Content( + text = stringReference("Bitcoin"), ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Content( + text = stringReference("Bitcoin"), + ), + topEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("1 234,56 \$"), + ), + bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content( + text = stringReference("0,12345678 BTC"), + ), + promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty, + tailUM = TangemTokenRowUM.TailUM.Empty, onItemClick = {}, onItemLongClick = {}, ) - private val textContentTokensState = WalletTokensListState.ContentState.Content( - items = persistentListOf( - TokensListItemUM.GroupTitle(id = 1, text = stringReference("Network Bitcoin")), - TokensListItemUM.Token(state = tokenItemState), - TokensListItemUM.GroupTitle(id = 2, text = stringReference("Network Ethereum")), - TokensListItemUM.Token( - state = tokenItemState.copy( - id = "2", - titleState = TokenItemState.TitleState.Content(text = stringReference("Ethereum")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "1,856660295 ETH"), - subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( - price = "1 799,41 \$", - priceChangePercent = "5,16 %", - type = PriceChangeType.UP, - ), - ), - ), - TokensListItemUM.Token( - state = TokenItemState.Unreachable( - id = "3", - iconState = CurrencyIconState.Locked, - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), - onItemClick = {}, - onItemLongClick = {}, - ), - ), - TokensListItemUM.Token( - state = tokenItemState.copy( - id = "4", - titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Shiba Inu")), - fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), - subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "6 200 220,00 SHIB"), - subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( - price = "0.01 \$", - priceChangePercent = "1,34 %", - type = PriceChangeType.DOWN, - ), - ), - ), + private val tokenListDefault = WalletTokensListUM.Content( + tokenList = persistentListOf( + TokensListItemUM2.Token(tokenRowDefault.copy(id = "0")), + TokensListItemUM2.Token(tokenRowDefault.copy(id = "1")), + TokensListItemUM2.Token(tokenRowDefault.copy(id = "2")), ), - organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( - isEnabled = true, + organizeButtonUM = TangemButtonUM( + text = resourceReference(R.string.organize_tokens_title), + type = TangemButtonType.Secondary, onClick = {}, ), ) - private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent( - items = persistentListOf( - TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), - isExpanded = false, - isCollapsable = true, - tokenItemUM = AccountItemPreviewData.accountItem - .copy(iconState = AccountItemPreviewData.accountLetterIcon), - ), - TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), - isExpanded = true, - isCollapsable = true, - tokenItemUM = AccountItemPreviewData.accountItem, - ), - ), - organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( - isEnabled = true, - onClick = {}, - ), + private val walletLocked = WalletUM.Locked( + walletsBalanceUM = WalletBalancePreview.content, + buttons = WalletPreviewData.actionButtons, + type = WalletType.Cold, + notifications = persistentListOf(), ) - private val emptyPortfolioContentState = WalletTokensListState.ContentState.PortfolioContent( - items = persistentListOf( - TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), - isExpanded = false, - isCollapsable = true, - tokenItemUM = AccountItemPreviewData.accountItem - .copy(iconState = AccountItemPreviewData.accountLetterIcon), - ), - TokensListItemUM.Portfolio( - tokens = persistentListOf(), - isExpanded = true, - isCollapsable = true, - tokenItemUM = AccountItemPreviewData.accountItem, - ), + private val walletDefault = WalletUM.Content( + walletsBalanceUM = WalletBalancePreview.content, + buttons = WalletPreviewData.actionButtons, + type = WalletType.Cold, + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, ), - organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( - isEnabled = true, - onClick = {}, + notifications = persistentListOf(), + notificationsCarousel = persistentListOf(), + tokensListUM = tokenListDefault, + nftState = WalletNFTItemUM.Content( + previews = persistentListOf(), + collectionsCount = 0, + allAssetsCount = 0, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = {}, ), + tangemPayState = TangemPayState.Loading, ) - private val noteLockedCard by lazy { - WalletCardState.LockedContent( - id = UserWalletId(stringValue = "1"), - title = "Note", - additionalInfo = WalletAdditionalInfo( - hideable = false, - content = TextReference.Str("Locked"), - ), - imageResId = R.drawable.ill_note_btc_120_106, - dropDownItems = persistentListOf(), - ) - } - private val miltiUnreachableCard by lazy { - WalletCardState.Content( - id = UserWalletId(stringValue = "2"), - title = "Wallet 1", - additionalInfo = WalletAdditionalInfo( - hideable = false, - content = TextReference.Str("Seed phrase"), - ), - imageResId = R.drawable.ill_wallet2_cards3_120_106, - cardCount = 3, - balance = DASH_SIGN, - dropDownItems = persistentListOf(), - isZeroBalance = false, - isBalanceFlickering = false, - ) - } - private val multiWalletState by lazy { - WalletState.MultiCurrency.Content( - pullToRefreshConfig = PullToRefreshConfig( - isRefreshing = false, - onRefresh = {}, - ), - walletCardState = miltiUnreachableCard, - buttons = persistentListOf(buyButton), - warnings = persistentListOf( - WalletNotification.Warning.SomeNetworksUnreachable, - WalletNotification.FinishWalletActivation( - type = WalletActivationBannerType.Attention, - buttonsState = ButtonsState.SecondaryButtonConfig( - text = resourceReference(R.string.hw_activation_need_finish), - onClick = { }, - ), - isBackupExists = false, - ), - ), - bottomSheetConfig = null, - tokensListState = textContentTokensState, - nftState = WalletNFTItemUM.Content( - previews = persistentListOf(WalletNFTItemUM.Content.CollectionPreview.Image("img1")), - collectionsCount = 1, - allAssetsCount = 3, - noCollectionAssetsCount = 0, - isFlickering = false, - onItemClick = { }, - ), - tangemPayState = TangemPayState.Empty, - type = WalletType.Cold, - ) - } - - private val buyButton = WalletManageButton.Buy(enabled = false, dimContent = true, onClick = {}) - private val sendButton = WalletManageButton.Send(enabled = false, dimContent = true, onClick = {}) - private val receiveButton = WalletManageButton.Receive( - enabled = false, - dimContent = true, - onClick = {}, - onLongClick = null, - ) - - private val singleWalletLockedState = WalletState.SingleCurrency.Locked( - walletCardState = noteLockedCard, - buttons = persistentListOf( - buyButton, - sendButton, - receiveButton, - ), - bottomSheetConfig = null, - onUnlockNotificationClick = {}, - onExploreClick = {}, - ) - - internal val walletScreenState = WalletScreenState( + internal val defaultState = WalletScreenState( topBarConfig = topBarConfig, selectedWalletIndex = 0, - wallets = persistentListOf( - singleWalletLockedState, - multiWalletState, + wallets = persistentListOf(), + wallets2 = persistentListOf( + walletLocked, + walletDefault, ), - wallets2 = persistentListOf(), onWalletChange = { _, _ -> }, event = consumedEvent(), isHidingMode = false, showMarketsOnboarding = false, onDismissMarketsTooltip = {}, ) - - internal val accountScreenState = - walletScreenState.copy( - wallets = persistentListOf( - singleWalletLockedState, - multiWalletState.copy(tokensListState = portfolioContentState), - ), - ) - - internal val accountScreenWithEmptyTokensState = - walletScreenState.copy( - wallets = persistentListOf( - singleWalletLockedState, - multiWalletState.copy(tokensListState = emptyPortfolioContentState), - ), - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt new file mode 100644 index 0000000000..ab76474be4 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -0,0 +1,245 @@ +package com.tangem.feature.wallet.presentation.common.preview + +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState +import com.tangem.core.ui.components.token.AccountItemPreviewData +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.event.consumedEvent +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy.topBarConfig +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 WalletScreenPreviewDataLegacy { + + private val buyButton = WalletManageButton.Buy(enabled = false, dimContent = true, onClick = {}) + private val sendButton = WalletManageButton.Send(enabled = false, dimContent = true, onClick = {}) + private val receiveButton = WalletManageButton.Receive( + enabled = false, + dimContent = true, + onClick = {}, + onLongClick = null, + ) + + private val tokenItemState = TokenItemState.Content( + id = "1", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Bitcoin")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "12 368,14 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "0,35853044 BTC"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "34 496,75 \$", + priceChangePercent = "0,43 %", + type = PriceChangeType.DOWN, + ), + onItemClick = {}, + onItemLongClick = {}, + ) + + private val textContentTokensState = WalletTokensListState.ContentState.Content( + items = persistentListOf( + TokensListItemUM.GroupTitle(id = 1, text = stringReference("Network Bitcoin")), + TokensListItemUM.Token(state = tokenItemState), + TokensListItemUM.GroupTitle(id = 2, text = stringReference("Network Ethereum")), + TokensListItemUM.Token( + state = tokenItemState.copy( + id = "2", + titleState = TokenItemState.TitleState.Content(text = stringReference("Ethereum")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "3 340,79 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "1,856660295 ETH"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "1 799,41 \$", + priceChangePercent = "5,16 %", + type = PriceChangeType.UP, + ), + ), + ), + TokensListItemUM.Token( + state = TokenItemState.Unreachable( + id = "3", + iconState = CurrencyIconState.Locked, + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Polygon")), + onItemClick = {}, + onItemLongClick = {}, + ), + ), + TokensListItemUM.Token( + state = tokenItemState.copy( + id = "4", + titleState = TokenItemState.TitleState.Content(text = stringReference(value = "Shiba Inu")), + fiatAmountState = TokenItemState.FiatAmountState.Content(text = "48,64 \$"), + subtitle2State = TokenItemState.Subtitle2State.TextContent(text = "6 200 220,00 SHIB"), + subtitleState = TokenItemState.SubtitleState.CryptoPriceContent( + price = "0.01 \$", + priceChangePercent = "1,34 %", + type = PriceChangeType.DOWN, + ), + ), + ), + ), + organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + isEnabled = true, + onClick = {}, + ), + ) + + private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent( + items = persistentListOf( + TokensListItemUM.Portfolio( + tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), + isExpanded = false, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem + .copy(iconState = AccountItemPreviewData.accountLetterIcon), + ), + TokensListItemUM.Portfolio( + tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), + isExpanded = true, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem, + ), + ), + organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + isEnabled = true, + onClick = {}, + ), + ) + + private val emptyPortfolioContentState = WalletTokensListState.ContentState.PortfolioContent( + items = persistentListOf( + TokensListItemUM.Portfolio( + tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), + isExpanded = false, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem + .copy(iconState = AccountItemPreviewData.accountLetterIcon), + ), + TokensListItemUM.Portfolio( + tokens = persistentListOf(), + isExpanded = true, + isCollapsable = true, + tokenItemUM = AccountItemPreviewData.accountItem, + ), + ), + organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + isEnabled = true, + onClick = {}, + ), + ) + + private val noteLockedCard by lazy { + WalletCardState.LockedContent( + id = UserWalletId(stringValue = "1"), + title = "Note", + additionalInfo = WalletAdditionalInfo( + hideable = false, + content = TextReference.Str("Locked"), + ), + imageResId = R.drawable.ill_note_btc_120_106, + dropDownItems = persistentListOf(), + ) + } + private val miltiUnreachableCard by lazy { + WalletCardState.Content( + id = UserWalletId(stringValue = "2"), + title = "Wallet 1", + additionalInfo = WalletAdditionalInfo( + hideable = false, + content = TextReference.Str("Seed phrase"), + ), + imageResId = R.drawable.ill_wallet2_cards3_120_106, + cardCount = 3, + balance = DASH_SIGN, + dropDownItems = persistentListOf(), + isZeroBalance = false, + isBalanceFlickering = false, + ) + } + private val multiWalletState by lazy { + WalletState.MultiCurrency.Content( + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + walletCardState = miltiUnreachableCard, + buttons = persistentListOf(buyButton), + warnings = persistentListOf( + WalletNotification.Warning.SomeNetworksUnreachable, + WalletNotification.FinishWalletActivation( + type = WalletActivationBannerType.Attention, + buttonsState = ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.hw_activation_need_finish), + onClick = { }, + ), + isBackupExists = false, + ), + ), + bottomSheetConfig = null, + tokensListState = textContentTokensState, + nftState = WalletNFTItemUM.Content( + previews = persistentListOf(WalletNFTItemUM.Content.CollectionPreview.Image("img1")), + collectionsCount = 1, + allAssetsCount = 3, + noCollectionAssetsCount = 0, + isFlickering = false, + onItemClick = { }, + ), + tangemPayState = TangemPayState.Empty, + type = WalletType.Cold, + ) + } + + private val singleWalletLockedState = WalletState.SingleCurrency.Locked( + walletCardState = noteLockedCard, + buttons = persistentListOf( + buyButton, + sendButton, + receiveButton, + ), + bottomSheetConfig = null, + onUnlockNotificationClick = {}, + onExploreClick = {}, + ) + + internal val walletScreenState = WalletScreenState( + topBarConfig = topBarConfig, + selectedWalletIndex = 0, + wallets = persistentListOf( + singleWalletLockedState, + multiWalletState, + ), + wallets2 = persistentListOf(), + onWalletChange = { _, _ -> }, + event = consumedEvent(), + isHidingMode = false, + showMarketsOnboarding = false, + onDismissMarketsTooltip = {}, + ) + + internal val accountScreenState = + walletScreenState.copy( + wallets = persistentListOf( + singleWalletLockedState, + multiWalletState.copy(tokensListState = portfolioContentState), + ), + ) + + internal val accountScreenWithEmptyTokensState = + walletScreenState.copy( + wallets = persistentListOf( + singleWalletLockedState, + multiWalletState.copy(tokensListState = emptyPortfolioContentState), + ), + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt index 0d83d630b8..f71de661de 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.preview +import androidx.compose.ui.text.SpanStyle import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.styledStringReference @@ -12,6 +13,14 @@ internal object WalletBalancePreview { val content: WalletBalanceUM.Content = WalletBalanceUM.Content( id = UserWalletId("0"), name = "My Wallet", + balanceInAppBar = combinedReference( + stringReference("1,234"), + styledStringReference( + ".56", + { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ), + stringReference(" $"), + ), balance = combinedReference( stringReference("1,234"), styledStringReference( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt index cdbe103b75..a081ffe922 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt @@ -35,6 +35,7 @@ internal sealed interface WalletBalanceUM { override val id: UserWalletId, override val name: String, val balance: TextReference, + val balanceInAppBar: TextReference, val isBalanceFlickering: Boolean, val isZeroBalance: Boolean?, ) : WalletBalanceUM diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt index 4e3b74f4cf..d26b02f2f7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletEvent.kt @@ -31,4 +31,6 @@ internal sealed class WalletEvent { val onAllow: () -> Unit, val onDeny: () -> Unit, ) : WalletEvent() + + data object CollapseBalance : WalletEvent() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index 130c44f114..aacaf7a5d2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import androidx.compose.ui.text.SpanStyle import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.formatStyled @@ -90,6 +91,13 @@ internal class SetTokenListErrorTransformer( return WalletBalanceUM.Content( id = id, name = name, + balanceInAppBar = BigDecimal.ZERO.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, balance = BigDecimal.ZERO.formatStyled { fiat( fiatCurrencyCode = appCurrency.code, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt index 4175d36c43..57be4ec68c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListTransformer.kt @@ -10,7 +10,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.converte import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCardStateConverter import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.TokenListStateConverter -import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMTransformer +import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.WalletTokensListUMConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons import timber.log.Timber import java.math.BigDecimal @@ -101,7 +101,7 @@ internal class SetTokenListTransformer( private fun toLoadedState(): WalletTokensListUM { if (params !is TokenConverterParams.Account) return WalletTokensListUM.Empty - return WalletTokensListUMTransformer( + return WalletTokensListUMConverter( selectedWallet = userWallet, appCurrency = appCurrency, clickIntents = clickIntents, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt index 69a82da93c..8369a81071 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter +import androidx.compose.ui.text.SpanStyle import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.formatStyled import com.tangem.core.ui.res.TangemTheme @@ -41,6 +42,13 @@ internal class MultiWalletBalanceUMTransformer( return WalletBalanceUM.Content( id = id, name = name, + balanceInAppBar = fiatBalance.amount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, balance = fiatBalance.amount.formatStyled { fiat( fiatCurrencyCode = appCurrency.code, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenAccountItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenAccountItemConverter.kt new file mode 100644 index 0000000000..75a56e8010 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenAccountItemConverter.kt @@ -0,0 +1,130 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import androidx.compose.ui.text.SpanStyle +import com.tangem.common.ui.account.AccountIconItemStateConverter +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.token.TangemTokenRowUM +import com.tangem.core.ui.extensions.pluralReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.styledResourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.formatStyled +import com.tangem.core.ui.format.bigdecimal.percent +import com.tangem.core.ui.res.TangemTheme +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.account.AccountId +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.quote.PriceChange +import com.tangem.feature.wallet.impl.R +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.persistentListOf + +internal class WalletTokenAccountItemConverter( + private val appCurrency: AppCurrency, + private val expandedAccounts: Set, + private val onAccountCollapseClick: (account: Account) -> Unit, + private val onAccountExpandClick: (account: Account) -> Unit, +) : Converter { + override fun convert(value: AccountStatus.CryptoPortfolio): TangemTokenRowUM { + val account = value.account + val isExpanded = expandedAccounts.contains(account.accountId) + + return TangemTokenRowUM.Content( + id = account.accountId.value, + headIconUM = TangemIconUM.Currency( + currencyIconState = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall).convert(account), + ), + titleUM = TangemTokenRowUM.TitleUM.Content( + text = account.accountName.toUM().value, + ), + subtitleUM = TangemTokenRowUM.SubtitleUM.Content( + text = pluralReference( + R.plurals.common_tokens_count, + count = account.tokensCount, + formatArgs = wrappedList(account.tokensCount), + ), + ), + topEndContentUM = getTopEndContent(value.tokenList.totalFiatBalance), + bottomEndContentUM = getBottomEndContent( + value.tokenList.totalFiatBalance, + value.priceChangeLce.getOrNull(), + ), + onItemClick = { + if (isExpanded) { + onAccountCollapseClick(account) + } else { + onAccountExpandClick(account) + } + }, + onItemLongClick = null, + ) + } + + private fun getTopEndContent(accountBalance: TotalFiatBalance): TangemTokenRowUM.EndContentUM { + return when (accountBalance) { + TotalFiatBalance.Failed -> TangemTokenRowUM.EndContentUM.Content( + text = stringReference(StringsSigns.DASH_SIGN), + ) + is TotalFiatBalance.Loaded -> TangemTokenRowUM.EndContentUM.Content( + text = accountBalance.amount.formatStyled { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, + ) + }, + ) + TotalFiatBalance.Loading -> TangemTokenRowUM.EndContentUM.Loading + } + } + + private fun getBottomEndContent( + accountBalance: TotalFiatBalance, + priceChange: PriceChange?, + ): TangemTokenRowUM.EndContentUM { + return when (accountBalance) { + TotalFiatBalance.Failed -> TangemTokenRowUM.EndContentUM.Content( + text = styledResourceReference( + id = R.string.common_unreachable, + spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, + ), + endIcons = persistentListOf( + TangemIconUM.Icon( + iconRes = R.drawable.ic_attention_default_24, + tintReference = { TangemTheme.colors2.graphic.status.attention }, + ), + ), + ) + is TotalFiatBalance.Loaded -> if (priceChange != null) { + val priceChangeType = PriceChangeType.fromBigDecimal(priceChange.value) + + TangemTokenRowUM.EndContentUM.Content( + text = stringReference( + priceChange.value.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + ), + priceChangeUM = PriceChangeState.Content( + type = priceChangeType, + valueInPercent = priceChange.value.format { percent() }, + ), + ) + } else { + TangemTokenRowUM.EndContentUM.Empty + } + TotalFiatBalance.Loading -> TangemTokenRowUM.EndContentUM.Loading + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt similarity index 53% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt index 652591e155..0828c8e871 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt @@ -3,252 +3,50 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import androidx.compose.ui.text.SpanStyle import com.tangem.common.getTotalCryptoAmount import com.tangem.common.getTotalFiatAmount -import com.tangem.common.ui.account.AccountIconItemStateConverter -import com.tangem.common.ui.account.toUM -import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.ds.badge.* -import com.tangem.core.ui.ds.button.TangemButtonShape -import com.tangem.core.ui.ds.button.TangemButtonSize -import com.tangem.core.ui.ds.button.TangemButtonType -import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.ds.image.TangemIconUM -import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM -import com.tangem.core.ui.ds.row.token.TangemTokenRowUM.EndContentUM -import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.styledResourceReference +import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.* import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.StatusSource -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.account.AccountId -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.network.Network -import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.staking.model.StakingAvailability -import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.child.wallet.model.intents.WalletContentClickIntents import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.addIf import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.collections.immutable.toPersistentList import java.math.BigDecimal -@Suppress("LargeClass", "LongParameterList") -internal class WalletTokensListUMTransformer( +internal class WalletTokenCurrencyItemConverter( private val appCurrency: AppCurrency, private val selectedWallet: UserWallet, - private val clickIntents: WalletClickIntents, private val yieldModuleApyMap: Map, - private val isAccountsModeEnabled: Boolean, - private val expandedAccounts: Set, + private val clickIntents: WalletContentClickIntents, stakingAvailabilityMap: Map, - shouldShowMainPromo: Boolean, -) : Converter { +) : Converter, TangemTokenRowUM> { - private val yieldSupplyPromoBannerConverter = YieldSupplyPromoBannerConverter( - yieldModuleApyMap, - shouldShowMainPromo, - ) private val currencyToIconStateConverter = CryptoCurrencyToIconStateConverter() private val earnApyConverter = EarnApyConverter( yieldModuleApyMap = yieldModuleApyMap, stakingApyMap = stakingAvailabilityMap, ) - override fun convert(value: AccountStatusList): WalletTokensListUM { - val promoCryptoCurrency = yieldSupplyPromoBannerConverter.convert2(value = value) - return if (value.accountStatuses.isEmpty()) { - WalletTokensListUM.Empty - } else { - val isCollapsable = value.accountStatuses.count { - it is AccountStatus.CryptoPortfolio && it.account.tokensCount > 0 - } > 1 - - val tokenListUM = value.accountStatuses - .filterIsInstance() - .asSequence() - .flatMap { accountStatus -> - if (isAccountsModeEnabled) { - val isExpanded = expandedAccounts.contains(accountStatus.account.accountId) - sequenceOf( - TokensListItemUM2.Portfolio( - tokenRowUM = toAccountRow(accountStatus, isExpanded), - isExpanded = isExpanded || !isCollapsable, - isCollapsable = isCollapsable, - tokenList = getTokenListItems( - accountStatus.tokenList, - promoCryptoCurrency, - ).toPersistentList(), - ), - ) - } else { - getTokenListItems(accountStatus.tokenList, promoCryptoCurrency) - } - }.toPersistentList() - - WalletTokensListUM.Content( - tokenList = tokenListUM, - organizeButtonUM = getOrganizeButtonUM(value), - ) - } - } - - private fun getTokenListItems( - tokenList: TokenList, - promoCryptoCurrency: CryptoCurrencyStatus?, - ): Sequence { - return when (tokenList) { - TokenList.Empty -> emptySequence() - is TokenList.GroupedByNetwork -> { - tokenList.groups.asSequence().flatMap { (network, currencies) -> - buildList { - add( - TokensListItemUM2.GroupTitle( - tokenRowUM = toGroupRow(network), - ), - ) - addAll( - currencies.asSequence().map { currencyStatus -> - val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id - TokensListItemUM2.Token( - tokenRowUM = toCurrencyRow( - currencyStatus = currencyStatus, - shouldShowPromo = shouldShowPromo, - ), - ) - }.toList(), - ) - } - } - } - is TokenList.Ungrouped -> { - tokenList.currencies.asSequence().map { currencyStatus -> - TokensListItemUM2.Token( - toCurrencyRow( - currencyStatus = currencyStatus, - shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id, - ), - ) - } - } - } - } - - private fun toAccountRow(accountStatus: AccountStatus.CryptoPortfolio, isExpanded: Boolean): TangemTokenRowUM { - val account = accountStatus.account - - val (topEndContent, bottomEndContent) = when (val accountBalance = accountStatus.tokenList.totalFiatBalance) { - TotalFiatBalance.Failed -> toFailedAccountRow() - is TotalFiatBalance.Loaded -> toLoadedAccountRow(accountStatus, accountBalance) - TotalFiatBalance.Loading -> EndContentUM.Loading to EndContentUM.Loading - } - - return TangemTokenRowUM.Content( - id = accountStatus.account.accountId.value, - headIconUM = TangemIconUM.Currency( - currencyIconState = AccountIconItemStateConverter(size = AccountIconSize.ExtraSmall).convert(account), - ), - titleUM = TangemTokenRowUM.TitleUM.Content( - text = account.accountName.toUM().value, - ), - subtitleUM = TangemTokenRowUM.SubtitleUM.Content( - text = pluralReference( - R.plurals.common_tokens_count, - count = account.tokensCount, - formatArgs = wrappedList(account.tokensCount), - ), - ), - topEndContentUM = topEndContent, - bottomEndContentUM = bottomEndContent, - onItemClick = { - if (isExpanded) { - clickIntents.onAccountCollapseClick(account) - } else { - clickIntents.onAccountExpandClick(account) - } - }, - onItemLongClick = null, - ) - } - - private fun toFailedAccountRow(): Pair { - return EndContentUM.Content( - text = stringReference(StringsSigns.DASH_SIGN), - ) to EndContentUM.Content( - text = styledResourceReference( - id = R.string.common_unreachable, - spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, - ), - endIcons = persistentListOf( - TangemIconUM.Icon( - iconRes = R.drawable.ic_attention_default_24, - tintReference = { TangemTheme.colors2.graphic.status.attention }, - ), - ), - ) - } - - private fun toLoadedAccountRow( - accountStatus: AccountStatus.CryptoPortfolio, - accountBalance: TotalFiatBalance.Loaded, - ): Pair { - val priceChange = accountStatus.priceChangeLce.getOrNull() - - return EndContentUM.Content( - text = accountBalance.amount.formatStyled { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.neutral.secondary) }, - ) - }, - ) to if (priceChange != null) { - val priceChangeType = PriceChangeType.fromBigDecimal(priceChange.value) - - EndContentUM.Content( - text = stringReference( - priceChange.value.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - }, - ), - priceChangeUM = PriceChangeState.Content( - type = priceChangeType, - valueInPercent = priceChange.value.format { percent() }, - ), - ) - } else { - EndContentUM.Empty - } - } - - private fun toGroupRow(network: Network): TangemHeaderRowUM { - return TangemHeaderRowUM( - id = network.hashCode().toString(), - title = resourceReference( - id = R.string.wallet_network_group_title, - formatArgs = wrappedList(network.name), - ), - ) - } - - private fun toCurrencyRow(currencyStatus: CryptoCurrencyStatus, shouldShowPromo: Boolean): TangemTokenRowUM { + override fun convert(value: Pair): TangemTokenRowUM { + val (currencyStatus, shouldShowPromo) = value val earnApyInfo = earnApyConverter.convert(currencyStatus) return TangemTokenRowUM.Content( @@ -362,7 +160,7 @@ internal class WalletTokensListUMTransformer( } } - private fun toCurrencyRowTopEnd(currencyStatus: CryptoCurrencyStatus): EndContentUM { + private fun toCurrencyRowTopEnd(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM.EndContentUM { val yieldSupply = currencyStatus.value.yieldSupplyStatus return when (currencyStatus.value) { is CryptoCurrencyStatus.Loaded, @@ -370,7 +168,7 @@ internal class WalletTokensListUMTransformer( is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.NoAccount, -> { - EndContentUM.Content( + TangemTokenRowUM.EndContentUM.Content( text = currencyStatus.getTotalFiatAmount().formatStyled { fiat( fiatCurrencyCode = appCurrency.code, @@ -397,11 +195,11 @@ internal class WalletTokensListUMTransformer( }.toImmutableList(), ) } - is CryptoCurrencyStatus.Loading -> EndContentUM.Loading - is CryptoCurrencyStatus.MissedDerivation -> EndContentUM.Content( + is CryptoCurrencyStatus.Loading -> TangemTokenRowUM.EndContentUM.Loading + is CryptoCurrencyStatus.MissedDerivation -> TangemTokenRowUM.EndContentUM.Content( text = stringReference(StringsSigns.DASH_SIGN), ) - is CryptoCurrencyStatus.Unreachable -> EndContentUM.Content( + is CryptoCurrencyStatus.Unreachable -> TangemTokenRowUM.EndContentUM.Content( text = styledResourceReference( id = R.string.common_unreachable, spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, @@ -414,17 +212,17 @@ internal class WalletTokensListUMTransformer( ), ) is CryptoCurrencyStatus.NoAmount, - -> EndContentUM.Empty + -> TangemTokenRowUM.EndContentUM.Empty } } - private fun toCurrencyRowBottomEnd(currencyStatus: CryptoCurrencyStatus): EndContentUM { + private fun toCurrencyRowBottomEnd(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM.EndContentUM { return when (currencyStatus.value) { is CryptoCurrencyStatus.Loaded, is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.NoAccount, - -> EndContentUM.Content( + -> TangemTokenRowUM.EndContentUM.Content( text = stringReference( currencyStatus.getTotalCryptoAmount().format { crypto(cryptoCurrency = currencyStatus.currency) @@ -432,8 +230,8 @@ internal class WalletTokensListUMTransformer( ), isFlickering = currencyStatus.value.isFlickering(), ) - is CryptoCurrencyStatus.Loading -> EndContentUM.Loading - is CryptoCurrencyStatus.MissedDerivation -> EndContentUM.Content( + is CryptoCurrencyStatus.Loading -> TangemTokenRowUM.EndContentUM.Loading + is CryptoCurrencyStatus.MissedDerivation -> TangemTokenRowUM.EndContentUM.Content( text = styledResourceReference( id = R.string.common_no_address, spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, @@ -445,7 +243,7 @@ internal class WalletTokensListUMTransformer( ), ), ) - is CryptoCurrencyStatus.Unreachable -> EndContentUM.Content( + is CryptoCurrencyStatus.Unreachable -> TangemTokenRowUM.EndContentUM.Content( text = styledResourceReference( id = R.string.common_unreachable, spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) }, @@ -458,7 +256,7 @@ internal class WalletTokensListUMTransformer( ), ) is CryptoCurrencyStatus.NoAmount, - -> EndContentUM.Empty + -> TangemTokenRowUM.EndContentUM.Empty } } @@ -496,26 +294,5 @@ internal class WalletTokensListUMTransformer( ) } - private fun getOrganizeButtonUM(accountList: AccountStatusList): TangemButtonUM? { - return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) { - TangemButtonUM( - text = resourceReference(R.string.organize_tokens_title), - isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, - size = TangemButtonSize.X9, - shape = TangemButtonShape.Rounded, - type = TangemButtonType.PrimaryInverse, - iconRes = R.drawable.ic_filter_default_24, - onClick = clickIntents::onOrganizeTokensClick, - ) - } else { - null - } - } - private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = sources.total == StatusSource.CACHE - - private fun isSingleCurrencyWalletWithToken(): Boolean { - return selectedWallet is UserWallet.Cold && - selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt new file mode 100644 index 0000000000..b4cf26bf7e --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt @@ -0,0 +1,166 @@ +package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter + +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.ds.button.TangemButtonType +import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.account.models.AccountStatusList +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance +import com.tangem.domain.models.account.AccountId +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.network.Network +import com.tangem.domain.models.tokenlist.TokenList +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.staking.model.StakingAvailability +import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents +import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.TokensListItemUM2 +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListUM +import com.tangem.feature.wallet.presentation.wallet.state.utils.isSingleWalletWithToken +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toPersistentList +import java.math.BigDecimal + +@Suppress("LongParameterList") +internal class WalletTokensListUMConverter( + private val appCurrency: AppCurrency, + private val selectedWallet: UserWallet, + private val clickIntents: WalletClickIntents, + private val yieldModuleApyMap: Map, + private val isAccountsModeEnabled: Boolean, + private val expandedAccounts: Set, + stakingAvailabilityMap: Map, + shouldShowMainPromo: Boolean, +) : Converter { + + private val accountRowConverter by lazy(LazyThreadSafetyMode.NONE) { + WalletTokenAccountItemConverter( + appCurrency = appCurrency, + expandedAccounts = expandedAccounts, + onAccountCollapseClick = clickIntents::onAccountCollapseClick, + onAccountExpandClick = clickIntents::onAccountExpandClick, + ) + } + + private val currencyRowConverter by lazy(LazyThreadSafetyMode.NONE) { + WalletTokenCurrencyItemConverter( + appCurrency = appCurrency, + selectedWallet = selectedWallet, + yieldModuleApyMap = yieldModuleApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + clickIntents = clickIntents, + ) + } + private val yieldSupplyPromoBannerConverter by lazy(LazyThreadSafetyMode.NONE) { + YieldSupplyPromoBannerConverter( + yieldModuleApyMap, + shouldShowMainPromo, + ) + } + + override fun convert(value: AccountStatusList): WalletTokensListUM { + val promoCryptoCurrency = yieldSupplyPromoBannerConverter.convert2(value = value) + return if (value.accountStatuses.isEmpty()) { + WalletTokensListUM.Empty + } else { + val isCollapsable = value.accountStatuses.count { + it is AccountStatus.CryptoPortfolio && it.account.tokensCount > 0 + } > 1 + + val tokenListUM = value.accountStatuses + .filterIsInstance() + .asSequence() + .flatMap { accountStatus -> + if (isAccountsModeEnabled) { + val isExpanded = expandedAccounts.contains(accountStatus.account.accountId) + sequenceOf( + TokensListItemUM2.Portfolio( + tokenRowUM = accountRowConverter.convert(accountStatus), + isExpanded = isExpanded || !isCollapsable, + isCollapsable = isCollapsable, + tokenList = getTokenListItems( + accountStatus.tokenList, + promoCryptoCurrency, + ).toPersistentList(), + ), + ) + } else { + getTokenListItems(accountStatus.tokenList, promoCryptoCurrency) + } + }.toPersistentList() + + WalletTokensListUM.Content( + tokenList = tokenListUM, + organizeButtonUM = getOrganizeButtonUM(value), + ) + } + } + + private fun getTokenListItems( + tokenList: TokenList, + promoCryptoCurrency: CryptoCurrencyStatus?, + ): Sequence { + return when (tokenList) { + TokenList.Empty -> emptySequence() + is TokenList.GroupedByNetwork -> { + tokenList.groups.asSequence().flatMap { (network, currencies) -> + buildList { + add( + TokensListItemUM2.GroupTitle( + tokenRowUM = toGroupRow(network), + ), + ) + addAll( + currencies.asSequence().map { currencyStatus -> + val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id + TokensListItemUM2.Token( + tokenRowUM = currencyRowConverter.convert(currencyStatus to shouldShowPromo), + ) + }.toList(), + ) + } + } + } + is TokenList.Ungrouped -> { + tokenList.currencies.asSequence().map { currencyStatus -> + val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id + TokensListItemUM2.Token( + tokenRowUM = currencyRowConverter.convert(currencyStatus to shouldShowPromo), + ) + } + } + } + } + + private fun toGroupRow(network: Network): TangemHeaderRowUM { + return TangemHeaderRowUM( + id = network.hashCode().toString(), + title = resourceReference( + id = R.string.wallet_network_group_title, + formatArgs = wrappedList(network.name), + ), + ) + } + + private fun getOrganizeButtonUM(accountList: AccountStatusList): TangemButtonUM? { + return if (accountList.flattenCurrencies().size > 1 && !selectedWallet.isSingleWalletWithToken()) { + TangemButtonUM( + text = resourceReference(R.string.organize_tokens_title), + isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, + size = TangemButtonSize.X9, + shape = TangemButtonShape.Rounded, + type = TangemButtonType.PrimaryInverse, + iconRes = R.drawable.ic_filter_default_24, + onClick = clickIntents::onOrganizeTokensClick, + ) + } else { + null + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt index f762997767..8e3ebc996c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/UserWalletConverterExt.kt @@ -21,4 +21,8 @@ private fun UserWallet.Cold.isWalletWithTokens(): Boolean { internal fun UserWallet.isSingleWallet(): Boolean { return this is UserWallet.Cold && scanResponse.cardTypesResolver.isSingleWallet() +} + +internal fun UserWallet.isSingleWalletWithToken(): Boolean { + return this is UserWallet.Cold && scanResponse.cardTypesResolver.isSingleWalletWithToken() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt index 9f4df0e402..838acf9e43 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletEventEffect.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.* @@ -17,7 +18,7 @@ import com.tangem.feature.wallet.presentation.wallet.ui.utils.demonstrateScrolli import com.tangem.features.pushnotifications.api.utils.PUSH_PERMISSION @Composable -internal fun WalletEventEffect( +internal fun WalletEventEffectLegacy( walletsListState: LazyListState, snackbarHostState: SnackbarHostState, event: StateEvent, @@ -59,6 +60,53 @@ internal fun WalletEventEffect( is WalletEvent.RequestPushPermissions -> { showPermissionRequest = value.onAllow to value.onDeny } + is WalletEvent.CollapseBalance -> { /* no-op */ } + } + }, + ) +} + +@Composable +internal fun WalletEventEffect( + walletsPagerState: PagerState, + snackbarHostState: SnackbarHostState, + event: StateEvent, + onCollapseBalance: () -> Unit, +) { + val resources = LocalContext.current.resources + + var showPermissionRequest by remember { mutableStateOf Unit, () -> Unit>?>(null) } + HandlePermissionRequest( + permissionRequestParams = showPermissionRequest, + onPermissionRequestResult = { showPermissionRequest = null }, + ) + + EventEffect( + event = event, + onTrigger = { value -> + when (value) { + is WalletEvent.ChangeWallet -> { + walletsPagerState.animateScrollToPage(page = value.newIndex) + } + is WalletEvent.ChangeWalletWithoutScroll -> { + walletsPagerState.scrollToPage(page = value.newIndex) + } + is WalletEvent.ShowError -> { + snackbarHostState.showSnackbar(message = value.text.resolveReference(resources)) + } + is WalletEvent.CopyAddress -> { + snackbarHostState.showSnackbar( + message = resources.getStringSafe(R.string.wallet_notification_address_copied), + duration = SnackbarDuration.Short, + ) + } + is WalletEvent.DemonstrateWalletsScrollPreview -> { + /* no-op */ + } + is WalletEvent.RequestPushPermissions -> { + showPermissionRequest = value.onAllow to value.onDeny + } + is WalletEvent.CollapseBalance -> onCollapseBalance() } }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 324c26f234..114a71988b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -68,9 +68,9 @@ import com.tangem.core.ui.utils.lineTo import com.tangem.core.ui.utils.moveTo import com.tangem.core.ui.utils.toPx import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenWithEmptyTokensState -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewDataLegacy.accountScreenState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewDataLegacy.accountScreenWithEmptyTokensState +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewDataLegacy.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet @@ -111,7 +111,7 @@ internal fun WalletScreen( onBottomSheetStateChange = onBottomSheetStateChange, ) - WalletEventEffect( + WalletEventEffectLegacy( walletsListState = walletsListState, snackbarHostState = snackbarHostState, event = state.event, @@ -499,7 +499,7 @@ private fun MarketsTooltip( } @Composable -internal fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { +private fun MarketsHint(isVisible: Boolean, modifier: Modifier = Modifier) { AnimatedVisibility( modifier = modifier, visible = isVisible, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index e368898477..c2c0b04c1a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -3,7 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.ui import android.content.res.Configuration import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.Canvas import androidx.compose.foundation.background @@ -12,51 +14,66 @@ import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material3.* +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.ExperimentalDecomposeApi import com.tangem.core.ui.components.atoms.Hand import com.tangem.core.ui.components.atoms.handComposableComponentHeight import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState -import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.components.haze.hazeSourceTangem import com.tangem.core.ui.components.rememberIsKeyboardVisible import com.tangem.core.ui.components.sheetscaffold.* import com.tangem.core.ui.components.snackbar.CopiedTextSnackbar import com.tangem.core.ui.components.snackbar.TangemSnackbar -import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior import com.tangem.core.ui.event.StateEvent -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.* -import com.tangem.core.ui.test.MainScreenTestTags -import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenState -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.accountScreenWithEmptyTokensState -import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData.walletScreenState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalWindowSize +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState -import dev.chrisbanes.haze.HazeProgressive +import com.tangem.feature.wallet.presentation.wallet.ui.components.MarketsHint +import com.tangem.feature.wallet.presentation.wallet.ui.components.MarketsTooltip +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletBalance +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletListContent +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletPagerIndicator +import com.tangem.feature.wallet.presentation.wallet.ui.components.common.WalletTopBar +import com.tangem.feature.wallet.presentation.wallet.ui.utils.lazyListStateMapSaver import kotlinx.coroutines.launch +import kotlin.math.abs + +private const val MARKET_HINT_THRESHOLD = 0.5f @OptIn(ExperimentalDecomposeApi::class) @Composable @@ -69,98 +86,201 @@ internal fun WalletScreen2( // It means that screen is still initializing if (state.selectedWalletIndex == NOT_INITIALIZED_WALLET_INDEX) return - val walletsListState = rememberLazyListState(initialFirstVisibleItemIndex = state.selectedWalletIndex) + val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() } + val snackbarHostState = remember(::SnackbarHostState) - val isAutoScroll = remember { mutableStateOf(value = false) } + val walletsPagerState = rememberPagerState( + initialPage = state.selectedWalletIndex, + pageCount = { state.wallets2.size }, + ) + + val partialCollapsedHeight = 64.dp + statusBarHeight + val balanceBlockHeight = 320.dp + partialCollapsedHeight + val behavior = rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight = balanceBlockHeight, + partialCollapsedHeight = partialCollapsedHeight, + snapAnimationSpec = spring(stiffness = Spring.StiffnessMedium), + ) + + val coroutineScope = rememberCoroutineScope() WalletContent2( state = state, - walletsListState = walletsListState, + walletsPagerState = walletsPagerState, snackbarHostState = snackbarHostState, - isAutoScroll = isAutoScroll, - onAutoScrollReset = { isAutoScroll.value = false }, + behavior = behavior, bottomSheetContent = bottomSheetContent, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, onBottomSheetStateChange = onBottomSheetStateChange, ) WalletEventEffect( - walletsListState = walletsListState, + walletsPagerState = walletsPagerState, snackbarHostState = snackbarHostState, event = state.event, - onAutoScrollSet = { isAutoScroll.value = true }, + onCollapseBalance = { + if (behavior.state.collapsedFraction < 1f) { + coroutineScope.launch { + behavior.state.collapse() + } + } + }, ) } -@OptIn(ExperimentalMaterial3Api::class, ExperimentalDecomposeApi::class) -@Suppress("LongMethod", "LongParameterList", "UnusedPrivateMember") +@Suppress("LongMethod", "LongParameterList", "CyclomaticComplexMethod") @Composable private fun WalletContent2( state: WalletScreenState, - walletsListState: LazyListState, + walletsPagerState: PagerState, + behavior: TangemCollapsingAppBarBehavior, snackbarHostState: SnackbarHostState, - isAutoScroll: State, - onAutoScrollReset: () -> Unit, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, bottomSheetContent: @Composable (() -> Unit), ) { - /* - * Don't pass key to remember, because it will brake scroll animation. - * selectedWalletIndex will be changed in WalletsListEffects. - */ - // val selectedWalletIndex by remember(state.selectedWalletIndex) { mutableIntStateOf(state.selectedWalletIndex) } - // val selectedWallet = state.wallets2.getOrElse(selectedWalletIndex) { state.wallets2[state.selectedWalletIndex] } + val density = LocalDensity.current + val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getBottom(this).toDp() } - - val listState = rememberLazyListState() - - val partialCollapsedHeight = 64.dp + statusBarHeight - - val scaffoldContent: @Composable (PaddingValues?) -> Unit = { _ -> - Box(Modifier.fillMaxSize()) { - NorthernLightsBackground(Modifier.matchParentSize()) - } - - val pagerState = rememberPagerState( - initialPage = state.selectedWalletIndex, - pageCount = { state.wallets2.size }, - ) - - LaunchedEffect(pagerState.currentPage) { - if (pagerState.currentPage != state.selectedWalletIndex) { - state.onWalletChange(pagerState.currentPage, false) - } - } - } + var walletBalance by remember { mutableStateOf(TextReference.EMPTY) } BaseScaffoldWithMarkets( state = state, - listState = listState, snackbarHostState = snackbarHostState, bottomSheetHeaderHeightProvider = bottomSheetHeaderHeightProvider, onBottomSheetStateChange = onBottomSheetStateChange, bottomSheetContent = bottomSheetContent, - content = scaffoldContent, - ) + appBarContent = { + WalletTopBar( + topBarConfig = state.topBarConfig, + walletBalance = walletBalance, + behavior = behavior, + ) + }, + ) { paddingValues, bottomSheetState -> + val marketHintApproxHeight = 140.dp + + val contentPadding = PaddingValues( + bottom = paddingValues.calculateBottomPadding() + marketHintApproxHeight, + ) + + LaunchedEffect(walletsPagerState.currentPage) { + if (walletsPagerState.currentPage != state.selectedWalletIndex) { + state.onWalletChange(walletsPagerState.currentPage, false) + } + } + + val listStates = rememberSaveable(saver = lazyListStateMapSaver(walletsPagerState.pageCount)) { + mutableMapOf().apply { + repeat(walletsPagerState.pageCount) { index -> put(index, LazyListState()) } + } + } + + val canPagerScroll by remember { derivedStateOf { behavior.state.heightOffset == 0f } } + + Box( + modifier = Modifier + .fillMaxSize() + .hazeSourceTangem(-1f), + ) { + NorthernLightsBackground(Modifier.matchParentSize()) + + WalletPagerIndicator( + pagerState = walletsPagerState, + behavior = behavior, + ) + + HorizontalPager( + state = walletsPagerState, + userScrollEnabled = canPagerScroll, + beyondViewportPageCount = 1, + ) { currentWalletIndex -> + val listState = listStates[currentWalletIndex] ?: rememberLazyListState() + + val currentWallet = state.wallets2.getOrElse(currentWalletIndex) { + state.wallets2[state.selectedWalletIndex] + } + + LaunchedEffect(walletsPagerState.currentPage) { + if (walletsPagerState.currentPage == currentWalletIndex) { + walletBalance = (currentWallet.walletsBalanceUM as? WalletBalanceUM.Content)?.balanceInAppBar + } + } + + val isShowMarketsHint by remember { + derivedStateOf { + behavior.state.collapsedFraction > MARKET_HINT_THRESHOLD && + listState.layoutInfo.totalItemsCount > 0 && + !listState.canScrollBackward && !listState.canScrollForward || + listState.canScrollBackward && !listState.canScrollForward + } + } + + val pageSlideAlpha by rememberPageAlpha(walletsPagerState, currentWalletIndex) + + Box( + modifier = Modifier.alpha(pageSlideAlpha), + ) { + TangemCollapsingTopBar( + state = behavior.state, + collapsingPart = { + WalletBalance( + behavior = behavior, + walletBalanceUM = currentWallet.walletsBalanceUM, + buttons = currentWallet.buttons, + isBalanceHidden = state.isHidingMode, + ) + }, + body = { + WalletListContent( + currentWallet = currentWallet, + listState = listState, + isBalanceHidden = state.isHidingMode, + contentPadding = contentPadding, + modifier = Modifier + .fillMaxSize() + .nestedScroll(behavior.nestedScrollConnection), + ) + }, + ) + + val peekHeight = bottomSheetHeaderHeightProvider() + handComposableComponentHeight + bottomBarHeight + MarketsHint( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = peekHeight + TangemTheme.dimens2.x7), + isVisible = isShowMarketsHint, + ) + } + } + + MarketsTooltip( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 24.dp) + .fillMaxWidth(fraction = 0.7f), + isVisible = state.showMarketsOnboarding, + availableHeight = LocalWindowSize.current.height, + bottomSheetState = bottomSheetState, + ) + } + } } -@Suppress("LongParameterList", "LongMethod", "CyclomaticComplexMethod", "UnusedPrivateMember") +@Suppress("LongParameterList", "LongMethod") @OptIn(ExperimentalMaterial3Api::class) @Composable private inline fun BaseScaffoldWithMarkets( state: WalletScreenState, snackbarHostState: SnackbarHostState, - listState: LazyListState, bottomSheetHeaderHeightProvider: () -> Dp, modifier: Modifier = Modifier, noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, + crossinline appBarContent: @Composable () -> Unit, crossinline bottomSheetContent: @Composable () -> Unit, - crossinline content: @Composable (PaddingValues) -> Unit, + crossinline content: @Composable (PaddingValues, TangemSheetState) -> Unit, ) { val bottomSheetState = rememberTangemStandardBottomSheetState() - val isPowerSaving by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsStateWithLifecycle() val isKeyboardVisible by rememberIsKeyboardVisible() @@ -176,7 +296,7 @@ private inline fun BaseScaffoldWithMarkets( val maxHeight = LocalWindowSize.current.height val coroutineScope = rememberCoroutineScope() - val background = TangemTheme.colors2.surface.level2 + val background = TangemTheme.colors2.surface.level3 CompositionLocalProvider( LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) }, @@ -194,20 +314,12 @@ private inline fun BaseScaffoldWithMarkets( Box(modifier = modifier) { TangemBottomSheetScaffold( - modifier = Modifier.background( - brush = Brush.verticalGradient( - listOf( - TangemTheme.colors2.surface.level1, - TangemTheme.colors2.surface.level2, - ), - ), - ), snackbarHost = { snackbarHostState -> WalletSnackbarHost( snackbarHostState = snackbarHostState, event = state.event, modifier = Modifier - .padding(bottom = TangemTheme.dimens.spacing4) + .padding(bottom = TangemTheme.dimens2.x1) .navigationBarsPadding(), ) }, @@ -250,47 +362,22 @@ private inline fun BaseScaffoldWithMarkets( } }, content = { paddingValues -> - Box { - Column( - modifier = Modifier.hazeSourceTangem(-1f), - ) { - content(paddingValues) - } + content(paddingValues, bottomSheetState) + appBarContent() - Surface( - color = Color.Unspecified, - contentColor = Color.Unspecified, - modifier = Modifier - .hazeEffectTangem { - progressive = - HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) - }, - ) { - TangemTopBar( - title = stringReference(""), // todo balance - startIconRes = R.drawable.ic_tangem_24, - endIconRes = R.drawable.ic_more_default_24, - onEndContentClick = state.topBarConfig.onDetailsClick, - isGhostButtons = !isPowerSaving, - modifier = Modifier - .testTag(MainScreenTestTags.TOP_BAR), - ) - } - - BottomSheetScrim( - color = if (state.showMarketsOnboarding) { - Color.Black.copy(alpha = .65f) - } else { - Color.Black.copy(alpha = .40f) - }, - visible = bottomSheetState.targetValue == TangemSheetValue.Expanded || - state.showMarketsOnboarding, - onDismissRequest = { - coroutineScope.launch { bottomSheetState.partialExpand() } - state.onDismissMarketsTooltip() - }, - ) - } + BottomSheetScrim( + color = if (state.showMarketsOnboarding) { + Color.Black.copy(alpha = .65f) + } else { + Color.Black.copy(alpha = .40f) + }, + visible = bottomSheetState.targetValue == TangemSheetValue.Expanded || + state.showMarketsOnboarding, + onDismissRequest = { + coroutineScope.launch { bottomSheetState.partialExpand() } + state.onDismissMarketsTooltip() + }, + ) }, ) @@ -410,6 +497,29 @@ private fun WalletSnackbarHost( } } +@Composable +private fun rememberPageAlpha(pagerState: PagerState, currentPageIndex: Int): State { + return remember { + derivedStateOf { + val pageOffset = pagerState.currentPageOffsetFraction + val currentPage = pagerState.currentPage + + when { + // Current page is being swiped away + currentPageIndex == currentPage -> { + 1f - abs(pageOffset) * 2f + } + // Target page is being swiped in + currentPageIndex == pagerState.targetPage -> { + (abs(pageOffset) * 2f - 1f).coerceAtLeast(0f) + } + // Other pages remain invisible + else -> 0f + }.coerceIn(0f, 1f) + } + } +} + // region Preview @OptIn(ExperimentalDecomposeApi::class) @Preview(showBackground = true, widthDp = 360) @@ -431,10 +541,8 @@ private fun WalletScreen2_Preview(@PreviewParameter(WalletScreen2PreviewProvider private class WalletScreen2PreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - walletScreenState, - walletScreenState.copy(selectedWalletIndex = 1), - accountScreenState.copy(selectedWalletIndex = 1), - accountScreenWithEmptyTokensState.copy(selectedWalletIndex = 1), + WalletScreenPreviewData.defaultState, + WalletScreenPreviewData.defaultState.copy(selectedWalletIndex = 1), ) } // endregion \ No newline at end of file From 96478230a0d8e8d7292c684877620ec044764cec Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 18:52:02 +0400 Subject: [PATCH 77/97] Updated on 2026-08-14 --- .../customerio/CustomerIoAnalyticsHandler.kt | 5 +- .../config/environment/EnvironmentConfig.kt | 4 +- features/tester/impl/build.gradle.kts | 1 + .../tester/presentation/TesterActivity.kt | 46 +++++++++++++++- .../presentation/menu/state/TesterMenuUM.kt | 1 + .../presentation/navigation/TesterScreen.kt | 1 + .../surveysparrow/SurveySparrowManager.kt | 55 +++++++++++++++++++ .../impl/src/main/res/values/strings.xml | 1 + gradle/dependencies.toml | 2 + 9 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt diff --git a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt index 2c34c4518b..a86b651af3 100644 --- a/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt +++ b/app/src/main/java/com/tangem/tap/common/analytics/handlers/customerio/CustomerIoAnalyticsHandler.kt @@ -33,13 +33,14 @@ class CustomerIoAnalyticsHandler( class Builder : AnalyticsHandlerBuilder { override fun build(data: AnalyticsHandlerBuilder.Data): AnalyticsHandler? { + val cdpApiKey = data.config.customerIoCdpApiKey return if (data.logConfig.isCustomerIoLogEnabled) { CustomerIoAnalyticsHandler(client = CustomerIoLogClient()) - } else if (data.config.customerIoCdpApiKey.isNotBlank()) { + } else if (!cdpApiKey.isNullOrBlank()) { CustomerIoAnalyticsHandler( client = CustomerIoClient( application = data.application, - cdpApiKey = data.config.customerIoCdpApiKey, + cdpApiKey = cdpApiKey, ), ) } else { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt index b8624c9ec8..e513869876 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/config/environment/EnvironmentConfig.kt @@ -28,5 +28,7 @@ data class EnvironmentConfig( val bffStaticTokenDev: String? = null, val gaslessTxApiKeyDev: String? = null, val gaslessTxApiKey: String? = null, - val customerIoCdpApiKey: String = "", + val customerIoCdpApiKey: String? = null, + val surveySparrowDomain: String? = null, + val surveySparrowToken: String? = null, ) \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 68cab10425..6554b8a54f 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.kotlin.immutable.collections) implementation(deps.timber) + implementation(deps.surveysparrow) /** Core modules */ implementation(projects.core.datasource) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt index a5dfc60808..f6467fef58 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/TesterActivity.kt @@ -1,5 +1,6 @@ package com.tangem.feature.tester.presentation +import android.widget.Toast import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -17,6 +18,7 @@ import com.tangem.core.navigation.finisher.AppFinisher import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.screen.ComposeActivity +import com.tangem.datasource.local.config.environment.EnvironmentConfig import com.tangem.feature.tester.presentation.accounts.ui.AccountsScreen import com.tangem.feature.tester.presentation.accounts.viewmodel.TesterAccountsViewModel import com.tangem.feature.tester.presentation.actions.TesterActionsScreen @@ -35,9 +37,10 @@ import com.tangem.feature.tester.presentation.menu.ui.TesterMenuScreen import com.tangem.feature.tester.presentation.navigation.InnerTesterRouter import com.tangem.feature.tester.presentation.navigation.TesterScreen import com.tangem.feature.tester.presentation.providers.ui.BlockchainProvidersScreen +import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel import com.tangem.feature.tester.presentation.storybook.ui.StoryBookScreen import com.tangem.feature.tester.presentation.storybook.viewmodel.StoryBookViewModel -import com.tangem.feature.tester.presentation.providers.viewmodel.BlockchainProvidersViewModel +import com.tangem.feature.tester.presentation.surveysparrow.SurveySparrowManager import com.tangem.feature.tester.presentation.testpush.ui.TestPushScreen import com.tangem.feature.tester.presentation.testpush.viewmodel.TestPushViewModel import dagger.hilt.android.AndroidEntryPoint @@ -60,6 +63,9 @@ internal class TesterActivity : ComposeActivity() { @Inject lateinit var appRouter: AppRouter + @Inject + lateinit var environmentConfig: EnvironmentConfig + @Composable override fun ScreenContent(modifier: Modifier) { val systemBarsColor = TangemTheme.colors.background.secondary @@ -89,6 +95,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.ACCOUNTS, ButtonUM.ADDRESSES_INFO, ButtonUM.STORY_BOOK, + ButtonUM.SURVEY_SPARROW, ), onButtonClick = { buttonUM -> val route = when (buttonUM) { @@ -101,6 +108,7 @@ internal class TesterActivity : ComposeActivity() { ButtonUM.ACCOUNTS -> TesterScreen.ACCOUNTS ButtonUM.ADDRESSES_INFO -> TesterScreen.ADDRESSES_INFO ButtonUM.STORY_BOOK -> TesterScreen.STORY_BOOK + ButtonUM.SURVEY_SPARROW -> TesterScreen.SURVEY_SPARROW } innerTesterRouter.open(route) @@ -192,6 +200,42 @@ internal class TesterActivity : ComposeActivity() { StoryBookScreen(state) } + + composable(route = TesterScreen.SURVEY_SPARROW.name) { + LaunchedEffect(Unit) { + val isSuccess = startSurveySparrow() + if (isSuccess) { + innerTesterRouter.back() + } + } + } } } + + private fun startSurveySparrow(): Boolean { + val domain = environmentConfig.surveySparrowDomain + val token = environmentConfig.surveySparrowToken + + if (domain.isNullOrEmpty() || token.isNullOrEmpty()) { + val toast = Toast.makeText( + this, + "Survey Sparrow is not configured. Domain or token is missing.", + Toast.LENGTH_LONG, + ) + + toast.show() + return false + } + + SurveySparrowManager(domain = domain, token = token).startSurveyForResult( + activity = this, + requestCode = SURVEY_SPARROW_REQUEST_CODE, + ) + + return true + } + + private companion object { + const val SURVEY_SPARROW_REQUEST_CODE = 1001 + } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt index e3318e0cab..59523e315d 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/menu/state/TesterMenuUM.kt @@ -27,5 +27,6 @@ data class TesterMenuUM( ACCOUNTS(R.string.accounts), ADDRESSES_INFO(R.string.addresses_info), STORY_BOOK(R.string.story_book), + SURVEY_SPARROW(R.string.survey_sparrow), } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt index ca2ae6a0e9..09864f11b0 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/navigation/TesterScreen.kt @@ -16,4 +16,5 @@ internal enum class TesterScreen { ACCOUNTS, ADDRESSES_INFO, STORY_BOOK, + SURVEY_SPARROW, } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt new file mode 100644 index 0000000000..5a91649f19 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/surveysparrow/SurveySparrowManager.kt @@ -0,0 +1,55 @@ +package com.tangem.feature.tester.presentation.surveysparrow + +import android.app.Activity +import com.surveysparrow.ss_android_sdk.SsSurvey +import com.surveysparrow.ss_android_sdk.SurveySparrow +import timber.log.Timber + +/** + * Manager for Survey Sparrow SDK. + * + * @param domain Survey Sparrow domain (e.g., "yourcompany") + * @param token Survey Sparrow SDK token + */ +class SurveySparrowManager( + private val domain: String, + private val token: String, +) { + + /** + * Create a SurveySparrow instance to start a survey. + * + * @param activity The activity context + * @param customVariables Optional custom variables to pass to the survey + * @return SurveySparrow instance ready to start + */ + fun createSurvey(activity: Activity, customVariables: Map? = null): SurveySparrow? { + return try { + val survey = SsSurvey(domain, token).apply { + customVariables?.forEach { (key, value) -> + addCustomParam(key, value) + } + } + + SurveySparrow(activity, survey) + } catch (e: Exception) { + Timber.e(e, "Failed to create SurveySparrow survey") + null + } + } + + /** + * Start a survey for result. + * + * @param activity The activity context + * @param requestCode The request code for onActivityResult + * @param customVariables Optional custom variables to pass to the survey + */ + fun startSurveyForResult(activity: Activity, requestCode: Int, customVariables: Map? = null) { + val surveySparrow = createSurvey(activity, customVariables) + if (surveySparrow != null) { + surveySparrow.startSurveyForResult(requestCode) + Timber.d("SurveySparrow survey started with requestCode: $requestCode") + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/res/values/strings.xml b/features/tester/impl/src/main/res/values/strings.xml index 19d8c518ee..f6d9e5cf50 100644 --- a/features/tester/impl/src/main/res/values/strings.xml +++ b/features/tester/impl/src/main/res/values/strings.xml @@ -23,4 +23,5 @@ News details (Bottom Sheet) Addresses info Story book + Survey Sparrow diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7169e359a2..759030eecc 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -104,6 +104,7 @@ sumsub = "1.38.0" haze = "1.7.1" kotlinpoet = "1.18.1" customerio = "4.6.3" +surveysparrow = "1.2.9" # endregion Other libraries # region Tools @@ -316,4 +317,5 @@ haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } customerio-analytics = { module = "io.customer.android:datapipelines", version.ref = "customerio" } customerio-messaging = { module = "io.customer.android:messaging-push-fcm", version.ref = "customerio" } +surveysparrow = { module = "com.github.surveysparrow:surveysparrow-android-sdk", version.ref = "surveysparrow" } # endregion Other From 4acd7ce3b238f7bfdb8844562b54879d5b1c8fba Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2026 07:32:06 +0100 Subject: [PATCH 78/97] Updated on 2026-08-14 --- .../tangem/core/ui/components/BottomFade.kt | 60 ------ .../com/tangem/core/ui/components/Fade.kt | 144 +++++++++++++ .../core/ui/components/UnableToLoadData.kt | 49 ++++- .../tangem/core/ui/components/chip/Chip.kt | 10 +- .../tangem/core/ui/ds/badge/TangemBadge.kt | 109 ++++++---- .../tangem/core/ui/ds/badge/TangemBadgeUM.kt | 6 +- .../tangem/core/ui/ds/image/TangemIconUM.kt | 31 +++ .../ui/ds/opportunities/OpportunitiesBG.kt | 30 +++ .../row/token/internal/TokenRowPromoBanner.kt | 3 +- .../com/tangem/core/ui/ds/tabs/TangemTab.kt | 2 +- .../main/res/drawable/ic_arrow_back_28.xml | 9 + .../src/main/res/drawable/ic_calendar_20.xml | 9 + .../src/main/res/drawable/ic_share_new_24.xml | 9 + .../feed/ui/earn/components/MostlyUsedCard.kt | 4 +- .../feed/ui/feed/components/NewsSlider.kt | 89 +++++++- .../feed/ui/feed/components/NewsSliderV1.kt | 67 ------ .../feed/ui/feed/components/NewsSliderV2.kt | 112 ---------- .../feed/components/articles/ArticleCard.kt | 4 +- .../feed/components/articles/ArticleCardV2.kt | 43 +++- .../components/articles/ArticleLoadingCard.kt | 178 +++++++++++++++- .../feed/ui/feed/components/articles/Tags.kt | 44 +++- .../components/NewsDetailsPlaceholder.kt | 191 +++++++++++++++++- .../ui/news/details/components/QuickRecap.kt | 126 ++++++++++++ .../feed/ui/news/list/NewsListContent.kt | 15 +- .../list/components/NewsListLazyColumn.kt | 4 +- 25 files changed, 1010 insertions(+), 338 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt create mode 100644 core/ui/src/main/res/drawable/ic_arrow_back_28.xml create mode 100644 core/ui/src/main/res/drawable/ic_calendar_20.xml create mode 100644 core/ui/src/main/res/drawable/ic_share_new_24.xml delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/QuickRecap.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt deleted file mode 100644 index 1bdae671ae..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/components/BottomFade.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.core.ui.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.tangem.core.ui.res.TangemTheme - -/** - * A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating - * elements and floating button at the bottom of the screen. - */ -@Composable -fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } - - Box( - modifier = modifier - .fillMaxWidth() - .height(TangemTheme.dimens.size100 + bottomBarHeight) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - backgroundColor, - ), - ), - ), - ) -} - -/** - * A composable that draws a fade effect. Used on screens with a list of repeating - * elements and floating button at the bottom of the screen. - */ -@Composable -fun Fade( - modifier: Modifier = Modifier, - backgroundColor: Color = TangemTheme.colors.background.secondary, - height: Dp = 32.dp, -) { - Box( - modifier = modifier - .fillMaxWidth() - .height(height) - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - backgroundColor, - ), - ), - ), - ) -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt new file mode 100644 index 0000000000..d2e0edcec9 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Fade.kt @@ -0,0 +1,144 @@ +package com.tangem.core.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.res.TangemTheme +import dev.chrisbanes.haze.HazeProgressive +import dev.chrisbanes.haze.HazeStyle +import dev.chrisbanes.haze.HazeTint + +/** + * A composable that draws a fade effect at the bottom of the screen. Used on screens with a list of repeating + * elements and floating button at the bottom of the screen. + */ +@Composable +fun BottomFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size100 + bottomBarHeight) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ) +} + +/** + * A composable that draws a fade effect at the right end of the screen. Same as [BottomFade] + * but with a horizontal gradient. + */ +@Composable +fun HorizontalFade(modifier: Modifier = Modifier, backgroundColor: Color = TangemTheme.colors.background.secondary) { + Box( + modifier = modifier + .fillMaxHeight() + .background( + brush = Brush.horizontalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ) +} + +/** + * A composable that draws a fade effect at the bottom of the screen. Same as [BottomFade] + * but with a vertical blur. + */ +@Composable +fun BottomFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) { + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens.size100 + bottomBarHeight) + .hazeEffectTangem( + style = HazeStyle( + blurRadius = 20.dp, + tint = HazeTint( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ), + ) { + progressive = + HazeProgressive.verticalGradient(startIntensity = 0f, endIntensity = 1f) + }, + ) +} + +/** + * A composable that draws a fade effect at the right end of the screen. Same as [HorizontalFade] + * but with blur. + */ +@Composable +fun HorizontalFadeWithBlur(backgroundColor: Color, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxHeight() + .hazeEffectTangem( + style = HazeStyle( + blurRadius = 20.dp, + tint = HazeTint( + brush = Brush.horizontalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + backgroundColor = Color.Transparent, + ), + ) { + progressive = + HazeProgressive.horizontalGradient(startIntensity = 0f, endIntensity = 1f) + }, + ) +} + +/** + * A composable that draws a fade effect. Used on screens with a list of repeating + * elements and floating button at the bottom of the screen. + */ +@Composable +fun Fade( + modifier: Modifier = Modifier, + backgroundColor: Color = TangemTheme.colors.background.secondary, + height: Dp = 32.dp, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(height) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + backgroundColor, + ), + ), + ), + ) +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt index 19f8e5fcbd..f3275d7024 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/UnableToLoadData.kt @@ -8,16 +8,29 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.components.buttons.SecondarySmallButton import com.tangem.core.ui.components.buttons.SmallButtonConfig +import com.tangem.core.ui.ds.button.* import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + UnableToLoadDataV2(onRetryClick, modifier) + } else { + UnableToLoadDataV1(onRetryClick, modifier) + } +} + +@Composable +private fun UnableToLoadDataV1(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), @@ -37,11 +50,43 @@ fun UnableToLoadData(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { } } +@Composable +private fun UnableToLoadDataV2(onRetryClick: () -> Unit, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResourceSafe(R.string.markets_loading_error_title), + style = TangemTheme.typography2.bodyRegular14, + color = TangemTheme.colors2.text.neutral.secondary, + ) + TangemButton( + buttonUM = TangemButtonUM( + text = resourceReference(R.string.try_to_load_data_again_button_title), + onClick = onRetryClick, + type = TangemButtonType.Secondary, + size = TangemButtonSize.X8, + shape = TangemButtonShape.Rounded, + ), + ) + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, showBackground = true) +@Composable +private fun PreviewV2() { + TangemThemePreviewRedesign { + UnableToLoadDataV2(onRetryClick = {}) + } +} + @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview() { +private fun PreviewV1() { TangemThemePreview { - UnableToLoadData(onRetryClick = {}) + UnableToLoadDataV1(onRetryClick = {}) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt b/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt index fb14a4a72e..b0c25bc839 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt @@ -5,13 +5,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.material3.ripple @@ -72,7 +66,7 @@ fun Chip(state: ChipUM, modifier: Modifier = Modifier) { @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun ChipPreview() { +private fun ChipPreviewV() { TangemThemePreview { Column( verticalArrangement = Arrangement.spacedBy(8.dp), diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt index aba734064b..b8939058d7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt @@ -1,13 +1,11 @@ package com.tangem.core.ui.ds.badge import android.content.res.Configuration -import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable @@ -15,18 +13,17 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.ds.badge.TangemBadgeSize.* -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.clickableSingle -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -43,7 +40,7 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { TangemBadge( text = badgeUM.text, - iconRes = badgeUM.iconRes, + tangemIconUM = badgeUM.tangemIconUM, size = badgeUM.size, shape = badgeUM.shape, color = badgeUM.color, @@ -60,7 +57,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { * * @param text TextReference for the badge label. * @param modifier Modifier to be applied to the badge. - * @param iconRes Drawable resource ID for the icon to be displayed in the badge. + * @param tangemIconUM Model of representation for the icon to be displayed in the badge. * @param size [TangemBadgeSize] defining the size of the badge. * @param shape [TangemBadgeShape] defining the shape of the badge. * @param color [TangemBadgeColor] defining the color scheme of the badge. @@ -74,7 +71,7 @@ fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) { fun TangemBadge( modifier: Modifier = Modifier, text: TextReference? = null, - @DrawableRes iconRes: Int? = null, + tangemIconUM: TangemIconUM? = null, size: TangemBadgeSize = X9, shape: TangemBadgeShape = TangemBadgeShape.Default, color: TangemBadgeColor = TangemBadgeColor.Gray, @@ -93,18 +90,12 @@ fun TangemBadge( .padding(size.toPaddingDp(position = iconPosition)) .clickableSingle(enabled = onClick != null, onClick = { onClick?.invoke() }), ) { - AnimatedVisibility( - visible = iconRes != null && iconPosition != TangemBadgeIconPosition.End, - modifier = Modifier.size(size = size.toContentSize()), - label = "Start Icon Visibility", - ) { - val wrappedIconRes = remember(this) { requireNotNull(iconRes) } - Icon( - painter = painterResource(id = wrappedIconRes), - contentDescription = null, - tint = iconColor, - ) - } + StartIcon( + tangemIconUM = tangemIconUM, + iconPosition = iconPosition, + size = size, + iconColor = iconColor, + ) AnimatedVisibility( visible = text != null, label = "Text Visibility", @@ -117,18 +108,66 @@ fun TangemBadge( color = getTextColor(type = type, color = color), ) } - AnimatedVisibility( - visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End, - modifier = Modifier.size(size = size.toContentSize()), - label = "End Icon Visibility", - ) { - val wrappedIconRes = remember(this) { requireNotNull(iconRes) } - Icon( - painter = painterResource(id = wrappedIconRes), - contentDescription = null, - tint = iconColor, - ) - } + EndIcon( + tangemIconUM = tangemIconUM, + iconPosition = iconPosition, + size = size, + iconColor = iconColor, + ) + } +} + +@Composable +private fun StartIcon( + iconPosition: TangemBadgeIconPosition, + size: TangemBadgeSize, + iconColor: Color, + tangemIconUM: TangemIconUM? = null, +) { + AnimatedVisibility( + visible = tangemIconUM != null && iconPosition != TangemBadgeIconPosition.End, + modifier = Modifier.size(size = size.toContentSize()), + label = "Start Icon Visibility", + ) { + val wrappedIconRes = remember(this) { requireNotNull(tangemIconUM) } + TangemIcon( + modifier = Modifier.fillMaxSize(), + tangemIconUM = when (wrappedIconRes) { + is TangemIconUM.Currency, + is TangemIconUM.Ident, + is TangemIconUM.Image, + is TangemIconUM.Url, + -> wrappedIconRes + is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor }) + }, + ) + } +} + +@Composable +private fun EndIcon( + iconPosition: TangemBadgeIconPosition, + size: TangemBadgeSize, + iconColor: Color, + tangemIconUM: TangemIconUM? = null, +) { + AnimatedVisibility( + visible = tangemIconUM != null && iconPosition == TangemBadgeIconPosition.End, + modifier = Modifier.size(size = size.toContentSize()), + label = "End Icon Visibility", + ) { + val wrappedIconRes = remember(this) { requireNotNull(tangemIconUM) } + TangemIcon( + modifier = Modifier.fillMaxSize(), + tangemIconUM = when (wrappedIconRes) { + is TangemIconUM.Currency, + is TangemIconUM.Ident, + is TangemIconUM.Image, + is TangemIconUM.Url, + -> wrappedIconRes + is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor }) + }, + ) } } @@ -351,7 +390,7 @@ private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::cl repeat(TangemBadgeType.entries.size) { index -> TangemBadge( text = stringReference("Title").takeIf { yIndex < 2 }, - iconRes = R.drawable.ic_information_24, + tangemIconUM = TangemIconUM.Icon(R.drawable.ic_information_24), type = TangemBadgeType.entries[index], color = params, shape = TangemBadgeShape.entries[yIndex % 2], diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt index 5f666898bb..83843fbc3d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadgeUM.kt @@ -1,14 +1,14 @@ package com.tangem.core.ui.ds.badge -import androidx.annotation.DrawableRes import com.tangem.core.ui.ds.badge.TangemBadgeSize.X9 +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.TextReference /** * UI model for [TangemBadge] component * * @param text TextReference for the badge label. - * @param iconRes Drawable resource ID for the icon to be displayed in the badge. + * @param tangemIconUM Model of representation for the icon to be displayed in the badge. * @param size [TangemBadgeSize] defining the size of the badge. * @param shape [TangemBadgeShape] defining the shape of the badge. * @param color [TangemBadgeColor] defining the color scheme of the badge. @@ -18,7 +18,7 @@ import com.tangem.core.ui.extensions.TextReference */ class TangemBadgeUM( val text: TextReference, - @DrawableRes val iconRes: Int? = null, + val tangemIconUM: TangemIconUM? = null, val size: TangemBadgeSize = X9, val shape: TangemBadgeShape = TangemBadgeShape.Default, val color: TangemBadgeColor = TangemBadgeColor.Gray, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt index ff7be95be2..2c6ecb3b07 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/TangemIconUM.kt @@ -2,12 +2,19 @@ package com.tangem.core.ui.ds.image import androidx.annotation.DrawableRes import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.vectorResource +import coil.compose.SubcomposeAsyncImage +import coil.request.ImageRequest +import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.icons.identicon.IdentIcon @@ -40,6 +47,11 @@ sealed interface TangemIconUM { data class Ident( val text: String, ) : TangemIconUM + + /** Image represented from network by url */ + data class Url( + val url: String, + ) : TangemIconUM } /** @@ -72,5 +84,24 @@ fun TangemIcon(tangemIconUM: TangemIconUM, modifier: Modifier = Modifier) { address = tangemIconUM.text, modifier = modifier, ) + is TangemIconUM.Url -> SubcomposeAsyncImage( + modifier = modifier, + model = ImageRequest.Builder(context = LocalContext.current) + .data(tangemIconUM.url) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { CircleShimmer() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors2.surface.level3, + shape = CircleShape, + ), + ) + }, + contentDescription = null, + ) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt index 368874114b..8fd7342071 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/opportunities/OpportunitiesBG.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -27,12 +28,15 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import coil.compose.AsyncImage +import coil.compose.SubcomposeAsyncImage import coil.request.ImageRequest import com.tangem.core.ui.R +import com.tangem.core.ui.components.CircleShimmer import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.haze.hazeForegroundEffectTangem import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import dev.chrisbanes.haze.HazeStyle @@ -117,6 +121,7 @@ private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp) is TangemIconUM.Icon -> SolidColorBackground(icon.tintReference(), blurRadius) is TangemIconUM.Ident -> Unit is TangemIconUM.Image -> ResBackground(icon.imageRes, blurRadius) + is TangemIconUM.Url -> UrlColorBackground(icon.url, blurRadius) } } @@ -213,6 +218,31 @@ private fun BoxScope.SolidColorBackground(color: Color, blurRadius: Dp) { ) } +@Composable +private fun BoxScope.UrlColorBackground(url: String, blurRadius: Dp) { + SubcomposeAsyncImage( + modifier = Modifier + .matchParentSize() + .hazeForegroundEffectTangem(style = HazeStyle(blurRadius = blurRadius, tint = null)), + model = ImageRequest.Builder(context = LocalContext.current) + .data(url) + .crossfade(enable = true) + .allowHardware(enable = false) + .build(), + loading = { CircleShimmer() }, + error = { + Box( + modifier = Modifier + .background( + color = TangemTheme.colors2.surface.level3, + shape = CircleShape, + ), + ) + }, + contentDescription = null, + ) +} + private const val SCALE_FACTOR = 1.5f private const val INNER_SHADOW_COLOR_START = 0x00000000 private const val INNER_SHADOW_COLOR_END = 0xFFFFFFFF diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt index 6ab252c5b8..013204a20f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPromoBanner.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.ds.row.token.TangemTokenRowUM import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -81,7 +82,7 @@ internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.C shape = TangemBadgeShape.Rounded, color = TangemBadgeColor.Green, type = TangemBadgeType.Tinted, - iconRes = R.drawable.ic_close_24, + tangemIconUM = TangemIconUM.Icon(R.drawable.ic_close_24), iconPosition = TangemBadgeIconPosition.None, onClick = promoBannerUM.onCloseClick, ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt index 67e1989fae..54d39c5104 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt @@ -45,7 +45,7 @@ fun TangemTab( val backgroundColor = if (isChecked) { TangemTheme.colors2.tabs.backgroundPrimary } else { - TangemTheme.colors2.tabs.textPrimary + TangemTheme.colors2.tabs.backgroundSecondary } val textColor = if (isChecked) { TangemTheme.colors2.tabs.textPrimary diff --git a/core/ui/src/main/res/drawable/ic_arrow_back_28.xml b/core/ui/src/main/res/drawable/ic_arrow_back_28.xml new file mode 100644 index 0000000000..3cfbdc0aa5 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_arrow_back_28.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_calendar_20.xml b/core/ui/src/main/res/drawable/ic_calendar_20.xml new file mode 100644 index 0000000000..46a4606330 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_calendar_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_share_new_24.xml b/core/ui/src/main/res/drawable/ic_share_new_24.xml new file mode 100644 index 0000000000..d24e87b696 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_share_new_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt index 21fdbcc0a4..5ce51b2458 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/earn/components/MostlyUsedCard.kt @@ -25,9 +25,7 @@ import com.tangem.features.feed.ui.earn.state.EarnListItemUM @Composable internal fun MostlyUsedCard(item: EarnListItemUM, onClick: () -> Unit, modifier: Modifier = Modifier) { - val isRedesignEnabled = LocalRedesignEnabled.current - - if (isRedesignEnabled) { + if (LocalRedesignEnabled.current) { MostlyUsedCardV2( modifier = modifier, item = item, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt index 74d1b42fd9..4e20ea9c2f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt @@ -1,15 +1,96 @@ package com.tangem.features.feed.ui.feed.components +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.onFirstVisible +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.HorizontalFadeWithBlur +import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.extensions.conditionalCompose +import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.feed.components.articles.ArticleCard +import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard import com.tangem.features.feed.ui.feed.state.NewsSliderConfig +@Suppress("LongMethod") @Composable internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) { + val background = LocalMainBottomSheetColor.current.value val isRedesignEnabled = LocalRedesignEnabled.current - if (isRedesignEnabled) { - NewsSliderV2(newsSliderConfig) - } else { - NewsSliderV1(newsSliderConfig) + Box( + modifier = Modifier.fillMaxWidth(), + ) { + LazyRow( + modifier = Modifier + .conditionalCompose( + condition = isRedesignEnabled, + modifier = { + hazeSourceTangem(-1f) + }, + ) + .background(color = background), + verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + state = rememberLazyListState(), + ) { + itemsIndexed( + items = newsSliderConfig.content, + key = { index, _ -> index }, + contentType = { _, _ -> "article" }, + ) { index, article -> + val articleModifier = if (index == FOURTH_ITEM_INDEX) { + Modifier.onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderScroll, + ) + } else { + Modifier + } + ArticleCard( + articleConfigUM = article, + onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) }, + modifier = articleModifier + .width(228.dp) + .heightIn(min = 172.dp) + .fillMaxHeight(), + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), + ) + } + + if (newsSliderConfig.shouldShowSeeAllNewsItem) { + item(contentType = "show_more") { + ShowMoreArticlesCard( + modifier = Modifier + .width(228.dp) + .heightIn(min = 172.dp) + .onFirstVisible( + minFractionVisible = 0.5f, + callback = newsSliderConfig.callbacks.onSliderEndReached, + ), + onClick = newsSliderConfig.callbacks.onOpenAllNews, + ) + } + } + } + if (isRedesignEnabled) { + HorizontalFadeWithBlur( + modifier = Modifier + .align(Alignment.TopEnd) + .heightIn(min = 172.dp) + .fillMaxHeight() + .width(100.dp), + backgroundColor = background, + ) + } } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt deleted file mode 100644 index e035dcc7a4..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV1.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.tangem.features.feed.ui.feed.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.onFirstVisible -import androidx.compose.ui.unit.dp -import com.tangem.features.feed.ui.feed.components.articles.ArticleCard -import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.feed.ui.feed.state.NewsSliderConfig - -@Composable -internal fun NewsSliderV1(newsSliderConfig: NewsSliderConfig) { - LazyRow( - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(horizontal = 16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - state = rememberLazyListState(), - ) { - itemsIndexed( - items = newsSliderConfig.content, - key = { index, _ -> index }, - contentType = { _, _ -> "article" }, - ) { index, article -> - val articleModifier = if (index == FOURTH_ITEM_INDEX) { - Modifier.onFirstVisible( - minFractionVisible = 0.5f, - callback = newsSliderConfig.callbacks.onSliderScroll, - ) - } else { - Modifier - } - ArticleCard( - articleConfigUM = article, - onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) }, - modifier = articleModifier - .heightIn(min = 164.dp) - .width(216.dp), - colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), - ) - } - - if (newsSliderConfig.shouldShowSeeAllNewsItem) { - item(contentType = "show_more") { - ShowMoreArticlesCard( - modifier = Modifier - .width(216.dp) - .heightIn(min = 164.dp) - .onFirstVisible( - minFractionVisible = 0.5f, - callback = newsSliderConfig.callbacks.onSliderEndReached, - ), - onClick = newsSliderConfig.callbacks.onOpenAllNews, - ) - } - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt deleted file mode 100644 index f27b0a8b1c..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSliderV2.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.tangem.features.feed.ui.feed.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.onFirstVisible -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Constraints -import androidx.compose.ui.unit.dp -import com.tangem.features.feed.ui.feed.components.articles.ArticleCard -import com.tangem.features.feed.ui.feed.components.articles.ShowMoreArticlesCard -import com.tangem.core.ui.extensions.conditional -import com.tangem.core.ui.res.TangemTheme -import com.tangem.features.feed.ui.feed.state.NewsSliderConfig - -private val dividerSpacerWidth = 20.dp - -@Suppress("MagicNumber", "LongMethod") -@Composable -internal fun NewsSliderV2(newsSliderConfig: NewsSliderConfig) { - val density = LocalDensity.current - val dividerWidthPx = with(density) { 1.dp.roundToPx() } - val spacerWidthPx = with(density) { dividerSpacerWidth.roundToPx() } - - LazyRow( - verticalAlignment = Alignment.CenterVertically, - contentPadding = PaddingValues(horizontal = 16.dp), - state = rememberLazyListState(), - ) { - itemsIndexed( - items = newsSliderConfig.content, - key = { index, _ -> index }, - contentType = { _, _ -> "article" }, - ) { index, article -> - val articleModifier = Modifier.conditional( - condition = index == FOURTH_ITEM_INDEX, - modifier = { - onFirstVisible( - minFractionVisible = 0.5f, - callback = newsSliderConfig.callbacks.onSliderScroll, - ) - }, - ) - - val shouldShowDivider = newsSliderConfig.shouldShowSeeAllNewsItem || - index < newsSliderConfig.content.size - 1 - - // have to use layout cause LazyRow has not fixed height and divider can not be measured - Layout( - modifier = Modifier, - content = { - ArticleCard( - articleConfigUM = article, - onArticleClick = { newsSliderConfig.callbacks.onArticleClick(article.id) }, - modifier = articleModifier - .fillMaxHeight() - .width(220.dp), - ) - Spacer(modifier = Modifier.width(dividerSpacerWidth)) - Box( - modifier = Modifier - .width(1.dp) - .background(TangemTheme.colors2.border.neutral.secondary), - ) - Spacer(modifier = Modifier.width(dividerSpacerWidth)) - }, - ) { measurables, constraints -> - val cardPlaceable = measurables[0].measure(constraints) - val height = cardPlaceable.height - - if (shouldShowDivider) { - val leftSpacer = measurables[1].measure(Constraints.fixed(spacerWidthPx, height)) - val divider = measurables[2].measure(Constraints.fixed(dividerWidthPx, height)) - val rightSpacer = measurables[3].measure(Constraints.fixed(spacerWidthPx, height)) - val totalWidth = cardPlaceable.width + leftSpacer.width + divider.width + rightSpacer.width - - layout(totalWidth, height) { - cardPlaceable.place(0, 0) - leftSpacer.place(cardPlaceable.width, 0) - divider.place(cardPlaceable.width + leftSpacer.width, 0) - rightSpacer.place(cardPlaceable.width + leftSpacer.width + divider.width, 0) - } - } else { - layout(cardPlaceable.width, height) { - cardPlaceable.place(0, 0) - } - } - } - } - - if (newsSliderConfig.shouldShowSeeAllNewsItem) { - item(contentType = "show_more") { - ShowMoreArticlesCard( - modifier = Modifier - .fillMaxHeight() - .width(216.dp) - .onFirstVisible( - minFractionVisible = 0.5f, - callback = newsSliderConfig.callbacks.onSliderEndReached, - ), - onClick = newsSliderConfig.callbacks.onOpenAllNews, - ) - } - } - } -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt index 53e0457e5a..48d21b699b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCard.kt @@ -13,9 +13,7 @@ fun ArticleCard( modifier: Modifier = Modifier, colors: CardColors = TangemBlockCardColors, ) { - val isRedesignEnabled = LocalRedesignEnabled.current - - if (isRedesignEnabled) { + if (LocalRedesignEnabled.current) { ArticleCardV2( articleConfigUM = articleConfigUM, onArticleClick = onArticleClick, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt index a47843225f..d55d4773c2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleCardV2.kt @@ -122,6 +122,7 @@ internal fun ShowMoreArticlesCardV2(modifier: Modifier = Modifier, onClick: () - .padding(vertical = 41.dp, horizontal = 16.dp), ) { Image( + modifier = Modifier.size(40.dp), imageVector = ImageVector.vectorResource(R.drawable.ic_show_more_news_48), contentDescription = stringResourceSafe(R.string.common_show_more), ) @@ -134,6 +135,8 @@ internal fun ShowMoreArticlesCardV2(modifier: Modifier = Modifier, onClick: () - color = TangemTheme.colors2.text.neutral.primary, ) + SpacerH(4.dp) + Text( text = stringResourceSafe(R.string.news_stay_in_the_loop), style = TangemTheme.typography2.captionSemibold12, @@ -150,17 +153,27 @@ private fun DefaultArticle( ) { Column( modifier = modifier - .background(TangemTheme.colors2.surface.level2) - .clickable { onArticleClick() } - .padding(vertical = 16.dp, horizontal = 10.dp), + .clip(RoundedCornerShape(20.dp)) + .background(color = TangemTheme.colors2.surface.level3) + .clickable(onClick = onArticleClick) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.primary, + shape = RoundedCornerShape(20.dp), + ) + .padding(16.dp), ) { Row(verticalAlignment = Alignment.CenterVertically) { - RatingInfo(rating = stringReference("${articleConfigUM.score}")) + RatingInfo( + rating = stringReference("${articleConfigUM.score}"), + isTrending = false, + ) } SpacerH(8.dp) Text( + modifier = Modifier.weight(1f), text = articleConfigUM.title, color = if (articleConfigUM.isViewed) { TangemTheme.colors2.text.neutral.tertiary @@ -168,13 +181,13 @@ private fun DefaultArticle( TangemTheme.colors2.text.neutral.primary }, style = TangemTheme.typography2.bodyRegular16, - minLines = 3, maxLines = 3, overflow = TextOverflow.Ellipsis, ) + SpacerH(8.dp) + Text( - modifier = Modifier.padding(vertical = 20.dp), text = articleConfigUM.createdAt.resolveReference(), style = TangemTheme.typography2.captionSemibold12, color = TangemTheme.colors2.text.neutral.secondary, @@ -182,6 +195,8 @@ private fun DefaultArticle( maxLines = 1, ) + SpacerH(8.dp) + Tags(tags = articleConfigUM.tags.toImmutableList()) } } @@ -269,7 +284,7 @@ private fun DayAndRatingInfo(rating: TextReference, modifier: Modifier = Modifie modifier = modifier, verticalAlignment = Alignment.CenterVertically, ) { - RatingInfo(rating) + RatingInfo(rating = rating, isTrending = true) SpacerW(8.dp) @@ -282,10 +297,14 @@ private fun DayAndRatingInfo(rating: TextReference, modifier: Modifier = Modifie } @Composable -private fun RatingInfo(rating: TextReference) { +private fun RatingInfo(rating: TextReference, isTrending: Boolean) { Icon( imageVector = ImageVector.vectorResource(R.drawable.ic_wrapped_circle_star_16), - tint = TangemTheme.colors2.fill.status.attention, + tint = if (isTrending) { + TangemTheme.colors2.fill.status.attention + } else { + TangemTheme.colors2.markers.iconGray + }, contentDescription = null, ) @@ -293,7 +312,11 @@ private fun RatingInfo(rating: TextReference) { Text( text = rating.resolveReference(), - color = TangemTheme.colors2.text.status.attention, + color = if (isTrending) { + TangemTheme.colors2.text.status.attention + } else { + TangemTheme.colors2.text.neutral.secondary + }, style = TangemTheme.typography2.captionSemibold12, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt index 1d5a646dda..74fbda7a34 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/ArticleLoadingCard.kt @@ -1,18 +1,32 @@ package com.tangem.features.feed.ui.feed.components.articles +import android.content.res.Configuration import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.block.BlockCard import com.tangem.core.ui.components.block.TangemBlockCardColors +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable fun TrendingLoadingArticle(modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + TrendingLoadingArticleV2(modifier) + } else { + TrendingLoadingArticleV1(modifier) + } +} + +@Composable +private fun TrendingLoadingArticleV1(modifier: Modifier = Modifier) { BlockCard( modifier = modifier, colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), @@ -40,25 +54,167 @@ fun TrendingLoadingArticle(modifier: Modifier = Modifier) { } } +@Composable +private fun TrendingLoadingArticleV2(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors2.surface.level3), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(8.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(24.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(48.dp) + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(12.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer( + modifier = Modifier.size(width = 58.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 58.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 58.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + } + } + } +} + @Composable fun DefaultLoadingArticle(modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + DefaultLoadingArticleV2(modifier) + } else { + DefaultLoadingArticleV1(modifier) + } +} + +@Composable +private fun DefaultLoadingArticleV1(modifier: Modifier = Modifier) { BlockCard( modifier = modifier, colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), ) { - Column(modifier = Modifier.padding(12.dp)) { - RectangleShimmer(modifier = Modifier.size(width = 110.dp, height = 16.dp), radius = 4.dp) - SpacerH(12.dp) - RectangleShimmer(modifier = Modifier.size(width = 142.dp, height = 18.dp), radius = 4.dp) - SpacerH(6.dp) - RectangleShimmer(modifier = Modifier.size(width = 176.dp, height = 18.dp), radius = 4.dp) - SpacerH(6.dp) - RectangleShimmer(modifier = Modifier.size(width = 120.dp, height = 18.dp), radius = 4.dp) - SpacerH(16.dp) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(8.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(28.dp) + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(8.dp) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { - RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp) - RectangleShimmer(modifier = Modifier.size(width = 72.dp, height = 24.dp), radius = 8.dp) + RectangleShimmer( + modifier = Modifier.size(width = 72.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 62.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 32.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) } } } +} + +@Composable +private fun DefaultLoadingArticleV2(modifier: Modifier = Modifier) { + BlockCard( + modifier = modifier, + colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors2.surface.level3), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + ) { + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(8.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(44.dp) + RectangleShimmer(modifier = Modifier.size(width = 46.dp, height = 16.dp), radius = TangemTheme.dimens2.x25) + SpacerH(12.dp) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer( + modifier = Modifier.size(width = 72.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 62.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 32.dp, height = 24.dp), + radius = TangemTheme.dimens2.x25, + ) + } + } + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TrendingLoadingArticlePreviewV1() { + TangemThemePreview { + TrendingLoadingArticle() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun TrendingLoadingArticlePreviewV2() { + TangemThemePreviewRedesign { + TrendingLoadingArticle() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DefaultLoadingArticlePreviewV1() { + TangemThemePreview { + DefaultLoadingArticle() + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun DefaultLoadingArticlePreviewV2() { + TangemThemePreviewRedesign { + DefaultLoadingArticle() + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt index 8508ef89a9..f5ec98879e 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/articles/Tags.kt @@ -8,8 +8,12 @@ import androidx.compose.ui.layout.SubcomposeMeasureScope import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.label.Label +import com.tangem.core.ui.components.label.entity.LabelLeadingContentUM import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList @@ -31,7 +35,25 @@ internal fun Tags(tags: ImmutableList, modifier: Modifier = Modifier) { val tagPlaceables = subcompose(ContentSlot.Tags) { tags.forEach { tag -> - Label(state = tag) + if (LocalRedesignEnabled.current) { + TangemBadge( + text = tag.text, + tangemIconUM = when (val content = tag.leadingContent) { + LabelLeadingContentUM.None -> null + is LabelLeadingContentUM.Token -> TangemIconUM.Url(content.iconUrl) + }, + shape = TangemBadgeShape.Rounded, + size = TangemBadgeSize.X6, + type = TangemBadgeType.Tinted, + color = TangemBadgeColor.Gray, + iconPosition = when (tag.leadingContent) { + LabelLeadingContentUM.None -> TangemBadgeIconPosition.None + is LabelLeadingContentUM.Token -> TangemBadgeIconPosition.Start + }, + ) + } else { + Label(state = tag) + } } }.map { it.measure(constraints) } @@ -108,12 +130,22 @@ private fun SubcomposeMeasureScope.calculateLayoutInfo( @Composable private fun OverflowLabel(count: Int) { - Label( - state = LabelUM( + if (LocalRedesignEnabled.current) { + TangemBadge( text = TextReference.Str("${StringsSigns.PLUS}$count"), - maxLines = 1, - ), - ) + shape = TangemBadgeShape.Rounded, + size = TangemBadgeSize.X6, + type = TangemBadgeType.Tinted, + color = TangemBadgeColor.Gray, + ) + } else { + Label( + state = LabelUM( + text = TextReference.Str("${StringsSigns.PLUS}$count"), + maxLines = 1, + ), + ) + } } private fun calculateRowWidth(placeables: List, spacingPx: Int): Int { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt index e985004e1b..a814d22eed 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/NewsDetailsPlaceholder.kt @@ -1,23 +1,45 @@ package com.tangem.features.feed.ui.news.details.components +import android.content.res.Configuration import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.VerticalDivider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + NewsDetailsPlaceholderV2(background, modifier) + } else { + NewsDetailsPlaceholderV1(background, modifier) + } +} + +@Suppress("LongMethod") +@Composable +private fun NewsDetailsPlaceholderV1(background: Color, modifier: Modifier = Modifier) { Column( - modifier = modifier.fillMaxSize().background(background).padding(16.dp), + modifier = modifier + .fillMaxSize() + .background(background) + .padding(16.dp), ) { RectangleShimmer(modifier = Modifier.size(width = 112.dp, height = 20.dp)) SpacerH(8.dp) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(28.dp), + modifier = Modifier + .fillMaxWidth() + .height(28.dp), ) SpacerH(4.dp) RectangleShimmer(modifier = Modifier.size(height = 28.dp, width = 208.dp)) @@ -29,29 +51,178 @@ fun NewsDetailsPlaceholder(background: Color, modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(8.dp), ) { RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 30.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 30.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 24.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 24.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 70.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 70.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 30.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 30.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 96.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 96.dp), ) RectangleShimmer( - modifier = Modifier.fillMaxWidth().height(20.dp).padding(end = 100.dp), + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 100.dp), ) } } +} + +@Suppress("LongMethod") +@Composable +private fun NewsDetailsPlaceholderV2(background: Color, modifier: Modifier = Modifier) { + Column( + modifier = modifier + .fillMaxSize() + .background(background) + .padding(16.dp), + ) { + Row( + modifier = Modifier.height(50.dp), + horizontalArrangement = Arrangement.spacedBy(30.dp), + ) { + Column { + RectangleShimmer( + modifier = Modifier.size(width = 50.dp, height = 20.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(10.dp) + RectangleShimmer( + modifier = Modifier.size(width = 90.dp, height = 18.dp), + radius = TangemTheme.dimens2.x25, + ) + } + + VerticalDivider(color = TangemTheme.colors2.border.neutral.primary) + + Column { + RectangleShimmer( + modifier = Modifier.size(width = 50.dp, height = 20.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(10.dp) + RectangleShimmer( + modifier = Modifier.size(width = 90.dp, height = 18.dp), + radius = TangemTheme.dimens2.x25, + ) + } + } + + SpacerH(36.dp) + + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(40.dp), + radius = TangemTheme.dimens2.x25, + ) + + SpacerH(12.dp) + + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + .padding(end = 106.dp), + radius = TangemTheme.dimens2.x25, + ) + + SpacerH(36.dp) + + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + RectangleShimmer( + modifier = Modifier.size(width = 98.dp, height = 36.dp), + radius = TangemTheme.dimens2.x25, + ) + RectangleShimmer( + modifier = Modifier.size(width = 98.dp, height = 36.dp), + radius = TangemTheme.dimens2.x25, + ) + } + + SpacerH(20.dp) + + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 22.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(12.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 66.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(12.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 18.dp), + radius = TangemTheme.dimens2.x25, + ) + SpacerH(12.dp) + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + .padding(end = 46.dp), + radius = TangemTheme.dimens2.x25, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NewsDetailsPlaceholderPreviewV1() { + TangemThemePreview { + NewsDetailsPlaceholder(background = TangemTheme.colors.background.tertiary) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun NewsDetailsPlaceholderPreviewV2() { + TangemThemePreviewRedesign { + NewsDetailsPlaceholder(background = TangemTheme.colors2.surface.level3) + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/QuickRecap.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/QuickRecap.kt new file mode 100644 index 0000000000..725bb4866b --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/details/components/QuickRecap.kt @@ -0,0 +1,126 @@ +package com.tangem.features.feed.ui.news.details.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.VerticalDivider +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SpacerH +import com.tangem.core.ui.components.SpacerW +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.LocalRedesignEnabled +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun QuickRecap(content: String, modifier: Modifier = Modifier) { + if (LocalRedesignEnabled.current) { + QuickRecapV2(content, modifier) + } else { + QuickRecapV1(content, modifier) + } +} + +@Composable +private fun QuickRecapV1(content: String, modifier: Modifier = Modifier) { + Box( + modifier = modifier.height(IntrinsicSize.Min), + ) { + VerticalDivider( + modifier = Modifier + .fillMaxHeight() + .padding(start = 8.dp), + thickness = 2.dp, + color = TangemTheme.colors.stroke.primary, + ) + Column(modifier = Modifier.padding(start = 20.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = R.drawable.ic_quick_recap_16), + contentDescription = null, + tint = TangemTheme.colors.icon.accent, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResourceSafe(R.string.news_quick_recap), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.accent, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +@Composable +private fun QuickRecapV2(content: String, modifier: Modifier = Modifier) { + Column(modifier = modifier.height(IntrinsicSize.Min)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + imageVector = ImageVector.vectorResource(R.drawable.ic_stars_20), + contentDescription = null, + ) + + SpacerW(2.dp) + + Text( + text = buildAnnotatedString { + withStyle( + SpanStyle().copy( + brush = Brush.linearGradient( + GRADIENT_START to Color(LINEAR_GRADIENT_FIRST_PART), + GRADIENT_END to Color(LINEAR_GRADIENT_SECOND_PART), + ), + ), + ) { + append(stringResourceSafe(R.string.news_quick_recap)) + } + }, + style = TangemTheme.typography2.bodyRegular14, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + + SpacerH(10.dp) + + Box { + VerticalDivider( + modifier = Modifier.fillMaxHeight(), + thickness = 2.dp, + color = Color(QUICK_RECAP_DIVIDER_COLOR), + ) + Text( + modifier = Modifier.padding(start = 16.dp), + text = content, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } +} + +private const val QUICK_RECAP_DIVIDER_COLOR = 0xFFA99FFF +private const val LINEAR_GRADIENT_FIRST_PART = 0xFFA3A0FF +private const val LINEAR_GRADIENT_SECOND_PART = 0xFFF79DFF +private const val GRADIENT_START = 0f +private const val GRADIENT_END = 0.5f \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt index 61ae51398f..ced6feef80 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/NewsListContent.kt @@ -17,8 +17,10 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.chip.Chip import com.tangem.core.ui.components.chip.entity.ChipUM import com.tangem.core.ui.components.label.entity.LabelUM +import com.tangem.core.ui.ds.tabs.TangemTab import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.res.LocalMainBottomSheetColor +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemThemePreview import com.tangem.features.feed.ui.news.list.components.NewsListLazyColumn import com.tangem.features.feed.ui.news.list.state.NewsListState @@ -29,6 +31,7 @@ import kotlinx.collections.immutable.toImmutableSet @Composable internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) { val background = LocalMainBottomSheetColor.current.value + val isRedesignEnabled = LocalRedesignEnabled.current val lazyListState = rememberLazyListState() Column( @@ -44,7 +47,17 @@ internal fun NewsListContent(state: NewsListUM, modifier: Modifier = Modifier) { items = state.filters, key = { it.id }, ) { filter -> - Chip(state = filter) + if (isRedesignEnabled) { + TangemTab( + text = filter.text, + isChecked = filter.isSelected, + onCheckedChange = { + filter.onClick() + }, + ) + } else { + Chip(state = filter) + } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt index 8d41f60ea3..177f191773 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/news/list/components/NewsListLazyColumn.kt @@ -112,7 +112,9 @@ private fun Content( key = ArticleConfigUM::id, ) { article -> ArticleCard( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .heightIn(min = 152.dp) + .fillMaxWidth(), colors = TangemBlockCardColors.copy(containerColor = TangemTheme.colors.background.action), articleConfigUM = article, onArticleClick = { From 04a21de3b690c42a1dd282ef1ca9027a3704c1f9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 26 Feb 2026 19:50:01 +0400 Subject: [PATCH 79/97] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 2 - .../configs/feature_toggles_config.json | 4 -- .../tangem/data/tokens/di/TokensDataModule.kt | 3 - .../DefaultCurrencyChecksRepository.kt | 3 - .../DefaultGaslessTransactionRepository.kt | 6 -- .../transaction/di/TransactionDataModule.kt | 3 - .../send/v2/api/SendFeatureToggles.kt | 4 +- .../send/v2/DefaultSendFeatureToggles.kt | 8 +-- .../DefaultFeeSelectorBlockComponent.kt | 3 - .../v2/feeselector/model/FeeSelectorLogic.kt | 7 +-- .../feeselector/ui/FeeSelectorBlockContent.kt | 35 ++++------- .../feature/swap/DefaultSwapComponent.kt | 62 +++++++++---------- .../tangem/feature/swap/model/SwapModel.kt | 26 ++------ 13 files changed, 48 insertions(+), 118 deletions(-) diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 08985168ae..8532e23891 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -146,9 +146,7 @@ abstract class BaseTestCase : TestCase( return ApplicationInjectionExecutionRule( toggleStates = mapOf( "SWAP_REDESIGN_ENABLED" to false, - "HOT_WALLET_ENABLED" to true, "ACCOUNTS_FEATURE_ENABLED" to true, - "GASLESS_TRANSACTIONS_ENABLED" to true, ) ) } diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index f79453a6f5..dc14b38ca6 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -32,10 +32,6 @@ "name": "APP_REDESIGN_ENABLED", "version": "undefined" }, - { - "name": "GASLESS_TRANSACTIONS_ENABLED", - "version": "5.33.0" - }, { "name": "SWAP_MARKET_LIST_ENABLED", "version": "5.34" diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index f30629e0d4..652ffeaa92 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -22,7 +22,6 @@ import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository import com.tangem.domain.tokens.repository.YieldSupplyWarningsViewedRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -73,12 +72,10 @@ internal object TokensDataModule { fun provideCurrencyChecksRepository( walletManagersFacade: WalletManagersFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, - sendFeatureToggles: SendFeatureToggles, ): CurrencyChecksRepository { return DefaultCurrencyChecksRepository( walletManagersFacade = walletManagersFacade, coroutineDispatchers = coroutineDispatcherProvider, - sendFeatureToggles = sendFeatureToggles, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index b1ecc1457c..b7d34b0d39 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -15,7 +15,6 @@ import com.tangem.domain.tokens.model.blockchains.UtxoAmountLimit import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.isZero import com.tangem.utils.extensions.orZero @@ -25,7 +24,6 @@ import java.math.BigDecimal internal class DefaultCurrencyChecksRepository( private val walletManagersFacade: WalletManagersFacade, private val coroutineDispatchers: CoroutineDispatcherProvider, - private val sendFeatureToggles: SendFeatureToggles, ) : CurrencyChecksRepository { override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? { @@ -67,7 +65,6 @@ internal class DefaultCurrencyChecksRepository( } override fun isNetworkSupportedForGaslessTx(network: Network): Boolean { - if (!sendFeatureToggles.isGaslessTransactionsEnabled) return false val blockchain = Blockchain.fromId(network.rawId) return blockchain.isGaslessTxSupported } diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt index 85eb924072..849883a543 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt @@ -13,7 +13,6 @@ import com.tangem.domain.transaction.GaslessTransactionRepository import com.tangem.domain.transaction.models.Eip7702Authorization import com.tangem.domain.transaction.models.GaslessSignedTransactionResult import com.tangem.domain.transaction.models.GaslessTransactionData -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update @@ -26,7 +25,6 @@ class DefaultGaslessTransactionRepository( private val gaslessTxServiceApi: GaslessTxServiceApi, private val coroutineDispatcherProvider: CoroutineDispatcherProvider, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, - private val sendFeatureToggles: SendFeatureToggles, ) : GaslessTransactionRepository { private val supportedTokensState = MutableStateFlow>>(hashMapOf()) @@ -131,9 +129,6 @@ class DefaultGaslessTransactionRepository( } override suspend fun getGaslessFeeAddresses(): Set { - if (!sendFeatureToggles.isGaslessTransactionsEnabled) { - return EMPTY_ADDRESSES - } return allAddressesMutex.withLock { allFeeRecipientAddress.ifEmpty { val allFeeAddresses = getAllFeeRecipientAddresses() @@ -150,6 +145,5 @@ class DefaultGaslessTransactionRepository( private companion object { val BASE_GAS_FOR_TRANSACTION: BigInteger = BigInteger("60000") - val EMPTY_ADDRESSES = emptySet() } } \ No newline at end of file diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt index 8083f49496..2a38ea32b4 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/di/TransactionDataModule.kt @@ -16,7 +16,6 @@ import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.WalletAddressServiceRepository import com.tangem.domain.transaction.error.FeeErrorResolver import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -77,13 +76,11 @@ internal object TransactionDataModule { responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, gaslessTxServiceApi: GaslessTxServiceApi, coroutineDispatcherProvider: CoroutineDispatcherProvider, - sendFeatureToggles: SendFeatureToggles, ): GaslessTransactionRepository { return DefaultGaslessTransactionRepository( gaslessTxServiceApi = gaslessTxServiceApi, coroutineDispatcherProvider = coroutineDispatcherProvider, responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, - sendFeatureToggles = sendFeatureToggles, ) } } \ No newline at end of file diff --git a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt index c8e3780fe2..4dbf452662 100644 --- a/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt +++ b/features/send-v2/api/src/main/java/com/tangem/features/send/v2/api/SendFeatureToggles.kt @@ -1,5 +1,3 @@ package com.tangem.features.send.v2.api -interface SendFeatureToggles { - val isGaslessTransactionsEnabled: Boolean -} \ No newline at end of file +interface SendFeatureToggles \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt index 8b63d8547e..08569966e9 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/DefaultSendFeatureToggles.kt @@ -1,12 +1,6 @@ package com.tangem.features.send.v2 -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.features.send.v2.api.SendFeatureToggles import javax.inject.Inject -internal class DefaultSendFeatureToggles @Inject constructor( - private val featureTogglesManager: FeatureTogglesManager, -) : SendFeatureToggles { - override val isGaslessTransactionsEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled("GASLESS_TRANSACTIONS_ENABLED") -} \ No newline at end of file +internal class DefaultSendFeatureToggles @Inject constructor() : SendFeatureToggles \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt index ca35f59a98..cd1e7a333d 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/DefaultFeeSelectorBlockComponent.kt @@ -14,7 +14,6 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.extensions.conditional import com.tangem.features.send.v2.api.FeeSelectorBlockComponent import com.tangem.features.send.v2.api.FeeSelectorComponent -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.params.FeeSelectorParams import com.tangem.features.send.v2.feeselector.model.FeeSelectorBlockModel @@ -32,7 +31,6 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( @Assisted private val params: FeeSelectorParams.FeeSelectorBlockParams, @Assisted onResult: (feeSelectorUM: FeeSelectorUM) -> Unit, private val feeSelectorComponentFactory: FeeSelectorComponent.Factory, - private val sendFeatureToggles: SendFeatureToggles, ) : FeeSelectorBlockComponent, AppComponentContext by appComponentContext { private val model: FeeSelectorBlockModel = getOrCreateModel(params = params) @@ -91,7 +89,6 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor( FeeSelectorBlockContent( state = state, onReadMoreClick = model::onReadMoreClicked, - isGaslessFeatureEnabled = sendFeatureToggles.isGaslessTransactionsEnabled, modifier = modifier .conditional(isScreenSource && (isNotSingleFee || isGaslessAvailable)) { Modifier.clickable { diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt index a3cbaac500..ac73f11c28 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/model/FeeSelectorLogic.kt @@ -19,7 +19,6 @@ import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase import com.tangem.domain.transaction.usecase.gasless.GetAvailableFeeTokensUseCase import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents.NonceInserted import com.tangem.features.send.v2.api.entity.FeeItem import com.tangem.features.send.v2.api.entity.FeeNonce @@ -59,7 +58,6 @@ internal class FeeSelectorLogic @AssistedInject constructor( private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getAvailableFeeTokensUseCase: GetAvailableFeeTokensUseCase, - sendFeatureToggles: SendFeatureToggles, isGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, ) : FeeSelectorIntents { @@ -67,8 +65,7 @@ internal class FeeSelectorLogic @AssistedInject constructor( private val loadFeeJobHolder = JobHolder() val uiState = MutableStateFlow(params.state) - val isGaslessEnabled = sendFeatureToggles.isGaslessTransactionsEnabled && - params.onLoadFeeExtended != null && + val isGaslessEnabled = params.onLoadFeeExtended != null && isGaslessFeeSupportedForNetwork(params.feeCryptoCurrencyStatus.currency.network) && params.cryptoCurrencyStatus.currency is CryptoCurrency.Token @@ -227,7 +224,7 @@ internal class FeeSelectorLogic @AssistedInject constructor( }, ) }, - ifLeft = { feeError -> + ifLeft = { _ -> feeSelectorCheckReloadTrigger.callbackCheckResult(false) feeSelectorAlertFactory.getFeeUnreachableErrorState { loadFee(isReload = true) } }, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index c635854c69..a74fac4f79 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -57,7 +57,6 @@ private const val READ_MORE_TAG = "READ_MORE" @Composable internal fun FeeSelectorBlockContent( state: FeeSelectorUM, - isGaslessFeatureEnabled: Boolean, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -80,25 +79,16 @@ internal fun FeeSelectorBlockContent( contentDescription = null, tint = TangemTheme.colors.icon.accent, ) - FeeSelectorDescription( - state = state, - isGaslessFeatureEnabled = isGaslessFeatureEnabled, - onReadMoreClick = onReadMoreClick, - ) + FeeSelectorDescription(state = state, onReadMoreClick = onReadMoreClick) } } @Composable -private fun FeeSelectorDescription( - state: FeeSelectorUM, - isGaslessFeatureEnabled: Boolean, - onReadMoreClick: () -> Unit, - modifier: Modifier = Modifier, -) { +private fun FeeSelectorDescription(state: FeeSelectorUM, onReadMoreClick: () -> Unit, modifier: Modifier = Modifier) { Row(modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween) { FeeSelectorStaticPart(modifier = Modifier.weight(1f), onReadMoreClick = onReadMoreClick) when (state) { - is FeeSelectorUM.Content -> FeeContent(state, isGaslessFeatureEnabled) + is FeeSelectorUM.Content -> FeeContent(state) is FeeSelectorUM.Loading -> FeeLoading() is FeeSelectorUM.Error -> FeeError() } @@ -181,17 +171,15 @@ private fun FeeLoading() { } @Composable -private fun FeeContent(state: FeeSelectorUM.Content, isGaslessFeatureEnabled: Boolean, modifier: Modifier = Modifier) { +private fun FeeContent(state: FeeSelectorUM.Content, modifier: Modifier = Modifier) { val fiatRate = state.feeFiatRateUM Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - if (isGaslessFeatureEnabled) { - AuditLabel( - state = AuditLabelUM( - text = stringReference(state.selectedFeeItem.fee.amount.currencySymbol), - type = AuditLabelUM.Type.General, - ), - ) - } + AuditLabel( + state = AuditLabelUM( + text = stringReference(state.selectedFeeItem.fee.amount.currencySymbol), + type = AuditLabelUM.Type.General, + ), + ) EllipsisText( text = if (state.feeExtraInfo.isFeeConvertibleToFiat && fiatRate != null) { @@ -218,7 +206,7 @@ private fun FeeContent(state: FeeSelectorUM.Content, isGaslessFeatureEnabled: Bo .testTag(FeeSelectorBlockTestTags.FEE_AMOUNT), ) - val isGaslessAvailable = isGaslessFeatureEnabled && state.feeExtraInfo.transactionFeeExtended != null + val isGaslessAvailable = state.feeExtraInfo.transactionFeeExtended != null if (!state.feeItems.isSingleItem() || isGaslessAvailable) { Icon( @@ -240,7 +228,6 @@ private fun FeeSelectorBlockContent_Preview(@PreviewParameter(FeeSelectorUMProvi TangemThemePreview { FeeSelectorBlockContent( modifier = Modifier.fillMaxWidth(), - isGaslessFeatureEnabled = true, state = state, onReadMoreClick = {}, ) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt index b3b291e98f..033d257c1d 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/DefaultSwapComponent.kt @@ -32,7 +32,6 @@ import com.tangem.feature.swap.ui.SwapSelectTokenScreen import com.tangem.feature.swap.ui.SwapSuccessScreen import com.tangem.features.approval.api.GiveApprovalComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.swap.SwapComponent import com.tangem.utils.extensions.isZero @@ -46,7 +45,6 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, @Assisted private val params: SwapComponent.Params, private val swapFeeSelectorBlockComponentFactory: SwapFeeSelectorBlockComponent.Factory, - private val sendFeatureToggles: SendFeatureToggles, private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, private val giveApprovalComponentFactory: GiveApprovalComponent.Factory, ) : SwapComponent, AppComponentContext by appComponentContext { @@ -58,7 +56,7 @@ internal class DefaultSwapComponent @AssistedInject constructor( serializer = AddToPortfolioRoute.serializer(), key = BOTTOM_SHEET_SLOT_KEY, handleBackButton = false, - childFactory = { configuration, context -> bottomSheetChild(context) }, + childFactory = { _, context -> bottomSheetChild(context) }, ) private val approvalSlot = childSlot( @@ -83,8 +81,8 @@ internal class DefaultSwapComponent @AssistedInject constructor( ) } - val slotNavigation = SlotNavigation() - val childSlot = childSlot( + private val slotNavigation = SlotNavigation() + private val childSlot = childSlot( source = slotNavigation, serializer = null, key = FEE_SELECTOR_SLOT_KEY, @@ -123,37 +121,35 @@ internal class DefaultSwapComponent @AssistedInject constructor( @Suppress("LongMethod", "CyclomaticComplexMethod") @Composable override fun Content(modifier: Modifier) { - if (sendFeatureToggles.isGaslessTransactionsEnabled) { - val dataState by model.dataStateStateFlow.collectAsStateWithLifecycle() - val fromCryptoCurrency by remember { derivedStateOf { dataState.fromCryptoCurrency } } - val feePaidCryptoCurrency by remember { derivedStateOf { dataState.feePaidCryptoCurrency } } - val shouldHideBlock by remember { - derivedStateOf { toBigDecimalOrZero(dataState.amount).isZero() || model.uiState.isInsufficientFunds } + val dataState by model.dataStateStateFlow.collectAsStateWithLifecycle() + val fromCryptoCurrency by remember { derivedStateOf { dataState.fromCryptoCurrency } } + val feePaidCryptoCurrency by remember { derivedStateOf { dataState.feePaidCryptoCurrency } } + val shouldHideBlock by remember { + derivedStateOf { toBigDecimalOrZero(dataState.amount).isZero() || model.uiState.isInsufficientFunds } + } + + LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) { + if (shouldHideBlock) { + slotNavigation.dismiss() + return@LaunchedEffect } - LaunchedEffect(fromCryptoCurrency, feePaidCryptoCurrency, shouldHideBlock) { - if (shouldHideBlock) { - slotNavigation.dismiss() - return@LaunchedEffect - } - - val sendingCryptoCurrencyStatus = fromCryptoCurrency ?: run { - slotNavigation.dismiss() - return@LaunchedEffect - } - - val feeCurrencyStatus = feePaidCryptoCurrency ?: run { - slotNavigation.dismiss() - return@LaunchedEffect - } - - slotNavigation.activate( - FeeSelectorConfig( - sendingCurrencyStatus = sendingCryptoCurrencyStatus, - feeCurrencyStatus = feeCurrencyStatus, - ), - ) + val sendingCryptoCurrencyStatus = fromCryptoCurrency ?: run { + slotNavigation.dismiss() + return@LaunchedEffect } + + val feeCurrencyStatus = feePaidCryptoCurrency ?: run { + slotNavigation.dismiss() + return@LaunchedEffect + } + + slotNavigation.activate( + FeeSelectorConfig( + sendingCurrencyStatus = sendingCryptoCurrencyStatus, + feeCurrencyStatus = feeCurrencyStatus, + ), + ) } val feeSelectorChildStackState by childSlot.subscribeAsState() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index f04f4d1bcb..b2ce189459 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -30,18 +30,11 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.R -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.combinedReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.core.ui.extensions.wrappedList -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.toWrappedList +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.core.ui.utils.parseBigDecimal -import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter -import com.tangem.feature.swap.models.SwapAlertUM import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus @@ -90,6 +83,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.analytics.StoriesEvents import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.component.SwapFeeSelectorBlockComponent +import com.tangem.feature.swap.converters.SwapTransactionErrorStateConverter import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.TransactionFeeResult import com.tangem.feature.swap.domain.TxFeeSealedState @@ -99,6 +93,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.AddToPortfolioRoute +import com.tangem.feature.swap.models.SwapAlertUM import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.UiActions import com.tangem.feature.swap.models.market.SwapMarketsListBatchFlowManager @@ -110,7 +105,6 @@ import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager -import com.tangem.features.send.v2.api.SendFeatureToggles import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.subcomponents.feeSelector.FeeSelectorReloadTrigger import com.tangem.features.swap.SwapComponent @@ -167,7 +161,6 @@ internal class SwapModel @Inject constructor( private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, private val feeSelectorReloadTrigger: FeeSelectorReloadTrigger, - private val sendFeatureToggles: SendFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val swapFeatureToggles: SwapFeatureToggles, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, @@ -241,7 +234,7 @@ internal class SwapModel @Inject constructor( val feeSelectorRepository = FeeSelectorRepository() // shows currency order (direct - swap initial to selected, reversed = selected to initial) - var isOrderReversed by mutableStateOf(false) + private var isOrderReversed by mutableStateOf(false) private val lastAmount = mutableStateOf(INITIAL_AMOUNT) private val lastReducedBalanceBy = mutableStateOf(BigDecimal.ZERO) private val swapRouter: SwapRouter = SwapRouter(router = router) @@ -2399,13 +2392,6 @@ internal class SwapModel @Inject constructor( } private fun getSelectedFeeState(): TxFeeSealedState { - if (!sendFeatureToggles.isGaslessTransactionsEnabled) { - return TxFeeSealedState.Legacy( - txFeeState = TxFeeState.Empty, - selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - ) - } - val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content ?: return TxFeeSealedState.Legacy( txFeeState = TxFeeState.Empty, @@ -2424,10 +2410,6 @@ internal class SwapModel @Inject constructor( } private fun getSelectedFee(): TxFee? { - if (!sendFeatureToggles.isGaslessTransactionsEnabled) { - return dataState.selectedFee - } - val feeStateUM = feeSelectorRepository.state.value as? FeeSelectorUM.Content ?: return null val transactionFeeExtended = feeStateUM.feeExtraInfo.transactionFeeExtended From 1e659525790b0fef94223824c7f9ee54549df470 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2026 14:08:19 +0300 Subject: [PATCH 80/97] Updated on 2026-08-14 --- .../presentation/storybook/page/badge/TangemBadgeStory.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt index 2caf62dae0..f69f3db054 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.tangem.core.ui.R import com.tangem.core.ui.ds.badge.* +import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory @@ -185,7 +186,7 @@ private fun BadgeTypeRow( ) { TangemBadge( text = stringReference("New"), - iconRes = R.drawable.ic_information_24, + tangemIconUM = TangemIconUM.Icon(R.drawable.ic_information_24), size = size, shape = shape, color = color, @@ -210,7 +211,7 @@ private fun BadgeTypeRow( modifier = Modifier.weight(1f), ) { TangemBadge( - iconRes = R.drawable.ic_information_24, + tangemIconUM = TangemIconUM.Icon(R.drawable.ic_information_24), size = size, shape = shape, color = color, From 09f1669029c4eb66266180068504097cbad2a300 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2026 14:18:42 +0200 Subject: [PATCH 81/97] Updated on 2026-08-14 --- .../res/drawable/ic_launcher_foreground.xml | 40 +++++++++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 40 +++++++++++++++++++ .../res/drawable/ic_launcher_foreground.xml | 25 ++---------- .../drawable/ic_launcher_foreground_base.xml | 21 ++++++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 2 +- .../res/drawable/ic_launcher_foreground.xml | 40 +++++++++++++++++++ 6 files changed, 146 insertions(+), 22 deletions(-) create mode 100644 app/src/debug/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/internal/res/drawable/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_foreground_base.xml create mode 100644 app/src/mocked/res/drawable/ic_launcher_foreground.xml diff --git a/app/src/debug/res/drawable/ic_launcher_foreground.xml b/app/src/debug/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..9878c6fe3f --- /dev/null +++ b/app/src/debug/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/internal/res/drawable/ic_launcher_foreground.xml b/app/src/internal/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..4a9d1d1208 --- /dev/null +++ b/app/src/internal/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 3b1aea3367..ad92c990d1 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,21 +1,4 @@ - - - - - - - + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground_base.xml b/app/src/main/res/drawable/ic_launcher_foreground_base.xml new file mode 100644 index 0000000000..3b1aea3367 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground_base.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 8b20aae928..8cc7aedc0a 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,5 +2,5 @@ - + \ No newline at end of file diff --git a/app/src/mocked/res/drawable/ic_launcher_foreground.xml b/app/src/mocked/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..ba2ae549e5 --- /dev/null +++ b/app/src/mocked/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + From 81ef606a43945422c142f8efd4f86cb09dbede46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2026 16:53:46 +0300 Subject: [PATCH 82/97] Updated on 2026-08-14 --- .../TokensListPortfolioItemConverter.kt | 31 ++++++++ .../tokenlist/state/TokensListItemUM.kt | 22 +++++- .../LoadingAccountTokenItemConverter.kt | 5 +- .../SetNoAvailablePairsTransformerV2.kt | 5 +- .../UpdateAccountTokenItemConverter.kt | 5 +- .../converters/AccountTokenItemConverter.kt | 5 +- .../intents/WalletContentClickIntents.kt | 50 +++++++----- .../WalletCurrencyActionsClickIntents.kt | 76 +++++++------------ .../preview/WalletScreenPreviewDataLegacy.kt | 23 +++++- .../router/DefaultWalletRouter.kt | 10 +++ .../presentation/router/InnerWalletRouter.kt | 4 + .../SetCryptoCurrencyActionsTransformer.kt | 10 +-- .../transformers/TokenConverterParams.kt | 5 +- .../MultiWalletCurrencyActionsConverter.kt | 19 +++-- .../converter/TokenListStateConverter.kt | 38 ++++++---- .../WalletTokenCurrencyItemConverter.kt | 20 ++--- .../converter/WalletTokensListUMConverter.kt | 43 +++++++---- .../subscribers/BasicAccountListSubscriber.kt | 7 +- .../SingleWalletButtonsSubscriber.kt | 7 +- .../MultiCurrencyAccountContent.kt | 42 +++++++--- .../YieldSupplyPromoBannerConverterTest.kt | 13 ++-- 21 files changed, 283 insertions(+), 157 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt diff --git a/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt new file mode 100644 index 0000000000..f4d414cae3 --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/account/TokensListPortfolioItemConverter.kt @@ -0,0 +1,31 @@ +package com.tangem.common.ui.account + +import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM +import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList + +class TokensListPortfolioItemConverter( + val tokenItemUM: TokenItemState, + val isExpanded: Boolean, + val isCollapsable: Boolean, + val tokens: ImmutableList, + val onEmptyAction: PortfolioItemContentUM.Empty.Action? = null, +) : Converter { + + override fun convert(value: Unit): TokensListItemUM.Portfolio { + val content = if (tokens.isEmpty()) { + PortfolioItemContentUM.Empty(onEmptyAction) + } else { + PortfolioItemContentUM.Tokens(tokens) + } + return TokensListItemUM.Portfolio( + tokenItemUM = tokenItemUM, + isExpanded = isExpanded, + isCollapsable = isCollapsable, + content = content, + ) + } +} \ 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 7797159dc1..c09e64697b 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 @@ -5,6 +5,7 @@ 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 +import kotlinx.collections.immutable.persistentListOf /** Tokens list item state */ @Immutable @@ -45,15 +46,34 @@ sealed interface TokensListItemUM { val tokenItemUM: TokenItemState, val isExpanded: Boolean, val isCollapsable: Boolean, - val tokens: ImmutableList, + val content: PortfolioItemContentUM, ) : TokensListItemUM { override val id: String = tokenItemUM.id + + val tokens: ImmutableList + get() = when (content) { + is PortfolioItemContentUM.Tokens -> content.tokens + is PortfolioItemContentUM.Empty -> persistentListOf() + } } data class Text(override val id: Any, val text: TextReference) : TokensListItemUM } +@Immutable sealed interface PortfolioTokensListItemUM { /** Unique ID */ val id: Any +} + +@Immutable +sealed interface PortfolioItemContentUM { + data class Tokens(val tokens: ImmutableList) : PortfolioItemContentUM + data class Empty(val action: Action? = null) : PortfolioItemContentUM { + + data class Action( + val text: TextReference, + val onClick: () -> Unit, + ) + } } \ No newline at end of file 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 8040f59244..61552e4822 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 @@ -1,6 +1,7 @@ package com.tangem.features.onramp.swap.availablepairs.entity.converters import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.TotalFiatBalance @@ -15,7 +16,7 @@ internal class LoadingAccountTokenItemConverter( override fun convert(value: AccountStatus.CryptoPortfolio): TokensListItemUM.Portfolio { val (account, currencies) = value - return TokensListItemUM.Portfolio( + return TokensListPortfolioItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = account, @@ -26,6 +27,6 @@ internal class LoadingAccountTokenItemConverter( tokens = currencies.flattenCurrencies() .map { LoadingTokenListItemConverter.convert(it.currency) } .toPersistentList(), - ) + ).convert(Unit) } } \ 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 index ac87e348b8..eba2f260ee 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 @@ -1,6 +1,7 @@ package com.tangem.features.onramp.swap.availablepairs.entity.transformers import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference @@ -34,7 +35,7 @@ internal class SetNoAvailablePairsTransformerV2( tokensListData = if (isAccountsMode) { TokenListUMData.AccountList( tokensList = accountList.map { (account, cryptoCurrencies) -> - TokensListItemUM.Portfolio( + TokensListPortfolioItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = account, @@ -45,7 +46,7 @@ internal class SetNoAvailablePairsTransformerV2( tokens = unavailableConverter.convertList(cryptoCurrencies) .map(TokensListItemUM::Token) .toPersistentList(), - ) + ).convert(Unit) }.toPersistentList(), totalTokensCount = totalTokensCount, ) 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 f90f1e9794..791645af20 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 @@ -1,6 +1,7 @@ package com.tangem.features.onramp.tokenlist.entity.transformer import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter 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 @@ -25,7 +26,7 @@ internal class UpdateAccountTokenItemConverter( .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) override fun convert(value: AccountAvailabilityUM): TokensListItemUM.Portfolio { - return TokensListItemUM.Portfolio( + return TokensListPortfolioItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = value.account, @@ -40,6 +41,6 @@ internal class UpdateAccountTokenItemConverter( unavailableConverter.convert(status) } }.map(TokensListItemUM::Token).toPersistentList(), - ) + ).convert(Unit) } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt index 919e908498..915211edc6 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.converters import com.tangem.common.getTotalCryptoAmount import com.tangem.common.getTotalFiatAmount import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter @@ -27,7 +28,7 @@ internal class AccountTokenItemConverter( ) : Converter { override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio { - return TokensListItemUM.Portfolio( + return TokensListPortfolioItemConverter( tokenItemUM = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = value.account, @@ -39,7 +40,7 @@ internal class AccountTokenItemConverter( createAvailableItemConverter() .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList(), - ) + ).convert(Unit) } fun createAvailableItemConverter(): TokenItemStateConverter { 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 4e6ddfac2c..9ccb19d1f6 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 @@ -9,6 +9,7 @@ import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.staking.StakingBalance @@ -28,7 +29,11 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAn import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter @@ -48,16 +53,11 @@ internal interface WalletContentClickIntents { fun onDismissMarketsTooltip() - fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) + fun onTokenItemClick(accountId: AccountId, currencyStatus: CryptoCurrencyStatus) - fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onTokenItemLongClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onApyLabelClick( - userWalletId: UserWalletId, - currencyStatus: CryptoCurrencyStatus, - apySource: ApySource, - apy: String, - ) + fun onApyLabelClick(accountId: AccountId, currencyStatus: CryptoCurrencyStatus, apySource: ApySource, apy: String) fun onYieldPromoCloseClick() @@ -69,6 +69,8 @@ internal interface WalletContentClickIntents { fun onAccountCollapseClick(account: Account) + fun onManageTokensClick(accountId: AccountId) + fun onTransactionClick(txHash: String) fun onDissmissBottomSheet() @@ -119,12 +121,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } } - override fun onTokenItemClick(userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus) { - router.openTokenDetails(userWalletId, currencyStatus) + override fun onTokenItemClick(accountId: AccountId, currencyStatus: CryptoCurrencyStatus) { + router.openTokenDetails(accountId.userWalletId, currencyStatus) } - override fun onTokenItemLongClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onTokenItemLongClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.main) { + val userWalletId = accountId.userWalletId val userWallet = getUserWalletUseCase(userWalletId).getOrElse { Timber.e( """ @@ -140,13 +143,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) .take(count = 1) .collectLatest { - showActionsBottomSheet(it, userWallet) + showActionsBottomSheet(it, userWallet, accountId) } } } override fun onApyLabelClick( - userWalletId: UserWalletId, + accountId: AccountId, currencyStatus: CryptoCurrencyStatus, apySource: ApySource, apy: String, @@ -161,9 +164,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( sendApyLabelClickAnalytics(navigationAction, currencyStatus) when (navigationAction) { - is NavigationAction.Staking -> router.openTokenDetails(userWalletId, currencyStatus, navigationAction) + is NavigationAction.Staking -> router.openTokenDetails( + accountId.userWalletId, + currencyStatus, + navigationAction, + ) is NavigationAction.YieldSupply -> openYieldSupply( - userWalletId = userWalletId, + userWalletId = accountId.userWalletId, cryptoCurrencyStatus = currencyStatus, apy = apy, ) @@ -209,6 +216,10 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( accountDependencies.expandedAccountsHolder.collapseAccount(account.accountId) } + override fun onManageTokensClick(accountId: AccountId) { + router.openManageTokensScreen(accountId) + } + private fun openYieldSupply(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, apy: String) { router.openYieldSupplyEntryScreen( userWalletId = userWalletId, @@ -248,11 +259,16 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(event) } - private fun showActionsBottomSheet(tokenActionsState: TokenActionsState, userWallet: UserWallet) { + private fun showActionsBottomSheet( + tokenActionsState: TokenActionsState, + userWallet: UserWallet, + accountId: AccountId, + ) { stateHolder.showBottomSheet( ActionsBottomSheetConfig( actions = MultiWalletCurrencyActionsConverter( userWallet = userWallet, + accountId = accountId, clickIntents = currencyActionsClickIntents, ).convert(tokenActionsState), ), 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 25d595780c..b57effd249 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 @@ -22,7 +22,6 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.DialogMessage -import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.extenstions.unwrap @@ -32,6 +31,7 @@ import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.NetworkAddress @@ -68,14 +68,13 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.launch -import timber.log.Timber import java.math.BigDecimal import javax.inject.Inject interface WalletCurrencyActionsClickIntents { fun onSendClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) @@ -83,32 +82,28 @@ interface WalletCurrencyActionsClickIntents { fun onSellClick(cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason) fun onBuyClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, - userWalletId: UserWalletId, + accountId: AccountId, unavailabilityReason: ScenarioUnavailabilityReason, ) - fun onReceiveClick( - userWalletId: UserWalletId, - cryptoCurrencyStatus: CryptoCurrencyStatus, - event: AnalyticsEvent? = null, - ) + fun onReceiveClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent? = null) - fun onStakeClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, option: StakingOption?) + fun onStakeClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, option: StakingOption?) fun onCopyAddressLongClick(cryptoCurrencyStatus: CryptoCurrencyStatus): TextReference? - fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onCopyAddressClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onHideTokensClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) - fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) + fun onPerformHideToken(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) fun onExploreClick() @@ -146,13 +141,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val isCryptoCurrencyCoinCouldHide: IsCryptoCurrencyCoinCouldHideUseCase, - private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val uiMessageSender: UiMessageSender, ) : BaseWalletClickIntents(), WalletCurrencyActionsClickIntents { override fun onSendClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) { @@ -176,18 +170,18 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name) stateHolder.hideBottomSheet() - navigateToSend(cryptoCurrencyStatus, userWalletId) + navigateToSend(cryptoCurrencyStatus, accountId.userWalletId) } }, ) } else { - navigateToSend(cryptoCurrencyStatus, userWalletId) + navigateToSend(cryptoCurrencyStatus, accountId.userWalletId) } } } override fun onReceiveClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, event: AnalyticsEvent?, ) { @@ -240,7 +234,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( return resourceReference(R.string.wallet_notification_address_copied) } - override fun onCopyAddressClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onCopyAddressClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send( event = TokenReceiveNewAnalyticsEvent.ButtonCopyAddress( token = cryptoCurrencyStatus.currency.symbol, @@ -251,10 +245,10 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { walletManagersFacade.getDefaultAddress( - userWalletId = userWalletId, + userWalletId = accountId.userWalletId, network = cryptoCurrencyStatus.currency.network, )?.let { address -> - stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + stateHolder.update(CloseBottomSheetTransformer(userWalletId = accountId.userWalletId)) clipboardManager.setText(text = address, isSensitive = true) walletEventSender.send(event = WalletEvent.CopyAddress) @@ -262,7 +256,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } } - override fun onHideTokensClick(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onHideTokensClick(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { analyticsEventHandler.send( event = TokenScreenAnalyticsEvent.ButtonRemoveToken(cryptoCurrencyStatus.currency.symbol), ) @@ -270,34 +264,22 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch(dispatchers.main) { val currency = cryptoCurrencyStatus.currency val isCryptoCurrencyCoinCouldHide = currency is CryptoCurrency.Coin && - !isCryptoCurrencyCoinCouldHide(userWalletId = userWalletId, cryptoCurrencyCoin = currency) + !isCryptoCurrencyCoinCouldHide(userWalletId = accountId.userWalletId, cryptoCurrencyCoin = currency) if (isCryptoCurrencyCoinCouldHide) { uiMessageSender.send(WalletAlertUM.unableHideToken(cryptoCurrency = cryptoCurrencyStatus.currency)) } else { uiMessageSender.send( WalletAlertUM.hideTokenConfirm(cryptoCurrency = cryptoCurrencyStatus.currency) { - onPerformHideToken(userWalletId, cryptoCurrencyStatus) + onPerformHideToken(accountId, cryptoCurrencyStatus) }, ) } } } - override fun onPerformHideToken(userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus) { + override fun onPerformHideToken(accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus) { modelScope.launch(dispatchers.io) { - val accountId = getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = userWalletId, - currency = cryptoCurrencyStatus.currency, - ) - .map { it.account.accountId } - .getOrNull() - - if (accountId == null) { - Timber.e("Account ID is null, cannot hide currency ${cryptoCurrencyStatus.currency.id}") - return@launch - } - manageCryptoCurrenciesUseCase(accountId = accountId, remove = cryptoCurrencyStatus.currency) .fold( ifLeft = { @@ -306,7 +288,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( ) }, ifRight = { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + stateHolder.update(CloseBottomSheetTransformer(userWalletId = accountId.userWalletId)) }, ) } @@ -340,7 +322,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onBuyClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, unavailabilityReason: ScenarioUnavailabilityReason, ) { @@ -356,7 +338,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push( AppRoute.Onramp( - userWalletId = userWalletId, + userWalletId = accountId.userWalletId, currency = cryptoCurrencyStatus.currency, source = OnrampSource.TOKEN_LONG_TAP, ), @@ -365,7 +347,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( override fun onSwapClick( cryptoCurrencyStatus: CryptoCurrencyStatus, - userWalletId: UserWalletId, + accountId: AccountId, unavailabilityReason: ScenarioUnavailabilityReason, ) { analyticsEventHandler.send( @@ -388,12 +370,12 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( modelScope.launch { saveViewedYieldSupplyWarningUseCase(cryptoCurrencyStatus.currency.name) stateHolder.hideBottomSheet() - navigateToSwap(cryptoCurrencyStatus, userWalletId) + navigateToSwap(cryptoCurrencyStatus, accountId.userWalletId) } }, ) } else { - navigateToSwap(cryptoCurrencyStatus, userWalletId) + navigateToSwap(cryptoCurrencyStatus, accountId.userWalletId) } } } @@ -435,11 +417,11 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( } override fun onStakeClick( - userWalletId: UserWalletId, + accountId: AccountId, cryptoCurrencyStatus: CryptoCurrencyStatus, option: StakingOption?, ) { - stateHolder.update(CloseBottomSheetTransformer(userWalletId = userWalletId)) + stateHolder.update(CloseBottomSheetTransformer(userWalletId = accountId.userWalletId)) val integrationId = option?.integrationId ?: return @@ -448,7 +430,7 @@ internal class WalletCurrencyActionsClickIntentsImplementor @Inject constructor( appRouter.push( AppRoute.Staking( - userWalletId = userWalletId, + userWalletId = accountId.userWalletId, cryptoCurrency = cryptoCurrency, integrationId = integrationId, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index ab76474be4..be54495064 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState import com.tangem.core.ui.components.token.AccountItemPreviewData import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.event.consumedEvent @@ -97,14 +98,20 @@ internal object WalletScreenPreviewDataLegacy { private val portfolioContentState = WalletTokensListState.ContentState.PortfolioContent( items = persistentListOf( TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), + content = PortfolioItemContentUM.Tokens( + tokens = textContentTokensState.items.filterIsInstance() + .toPersistentList(), + ), isExpanded = false, isCollapsable = true, tokenItemUM = AccountItemPreviewData.accountItem .copy(iconState = AccountItemPreviewData.accountLetterIcon), ), TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), + content = PortfolioItemContentUM.Tokens( + tokens = textContentTokensState.items.filterIsInstance() + .toPersistentList(), + ), isExpanded = true, isCollapsable = true, tokenItemUM = AccountItemPreviewData.accountItem, @@ -119,14 +126,22 @@ internal object WalletScreenPreviewDataLegacy { private val emptyPortfolioContentState = WalletTokensListState.ContentState.PortfolioContent( items = persistentListOf( TokensListItemUM.Portfolio( - tokens = textContentTokensState.items.filterIsInstance().toPersistentList(), + content = PortfolioItemContentUM.Tokens( + tokens = textContentTokensState.items.filterIsInstance() + .toPersistentList(), + ), isExpanded = false, isCollapsable = true, tokenItemUM = AccountItemPreviewData.accountItem .copy(iconState = AccountItemPreviewData.accountLetterIcon), ), TokensListItemUM.Portfolio( - tokens = persistentListOf(), + content = PortfolioItemContentUM.Empty( + action = PortfolioItemContentUM.Empty.Action( + text = stringReference("Manage tokens"), + onClick = {}, + ), + ), isExpanded = true, isCollapsable = true, tokenItemUM = AccountItemPreviewData.accountItem, 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 49f35c817d..625fe4fa31 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 @@ -6,7 +6,9 @@ import com.tangem.common.routing.AppRoute 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.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse @@ -50,6 +52,14 @@ internal class DefaultWalletRouter @Inject constructor( ) } + override fun openManageTokensScreen(accountId: AccountId) { + val route = AppRoute.ManageTokens( + source = AppRoute.ManageTokens.Source.ACCOUNT, + portfolioId = PortfolioId(accountId), + ) + router.push(route) + } + override fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean) { router.push( AppRoute.Onboarding( 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 afad2d5fe7..3e9f99185e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable import com.arkivanov.decompose.router.slot.SlotNavigation import com.tangem.common.routing.AppRoute import com.tangem.domain.models.TokenReceiveConfig +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.scan.ScanResponse @@ -37,6 +38,9 @@ internal interface InnerWalletRouter { /** Open details screen */ fun openDetailsScreen(selectedWalletId: UserWalletId) + /** Open manage tokens screen */ + fun openManageTokensScreen(accountId: AccountId) + /** Open onboarding screen */ fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean = false) 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 df4942ff07..f3dbd5dd9e 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 @@ -1,7 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.PortfolioId +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState @@ -16,7 +16,7 @@ import timber.log.Timber internal class SetCryptoCurrencyActionsTransformer( private val tokenActionsState: TokenActionsState, private val userWallet: UserWallet, - private val portfolioId: PortfolioId, + private val accountId: AccountId, private val clickIntents: WalletClickIntents, ) : WalletStateTransformer(userWallet.walletId) { @@ -51,7 +51,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onBuyClick( - userWalletId = portfolioId.userWalletId, + accountId = accountId, cryptoCurrencyStatus = cryptoCurrencyStatus, unavailabilityReason = action.unavailabilityReason, ) @@ -64,7 +64,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onReceiveClick( - portfolioId.userWalletId, + accountId, cryptoCurrencyStatus = cryptoCurrencyStatus, ) }, @@ -91,7 +91,7 @@ internal class SetCryptoCurrencyActionsTransformer( dimContent = action.unavailabilityReason != ScenarioUnavailabilityReason.None, onClick = { clickIntents.onSendClick( - userWalletId = portfolioId.userWalletId, + accountId = accountId, cryptoCurrencyStatus = cryptoCurrencyStatus, unavailabilityReason = action.unavailabilityReason, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt index fd8229e5e8..03ffab9a92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/TokenConverterParams.kt @@ -1,16 +1,17 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.tokenlist.TokenList sealed interface TokenConverterParams { + /** Wallet mode; list of tokens for main account */ data class Wallet( - val portfolioId: PortfolioId, + val accountId: AccountId, val tokenList: TokenList, ) : TokenConverterParams + /** Account mode; list of accounts */ data class Account( val accountList: AccountStatusList, val expandedAccounts: Set, 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 ad35b058ea..0e3d985f61 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.account.AccountId 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,11 +18,10 @@ import kotlinx.collections.immutable.toImmutableList internal class MultiWalletCurrencyActionsConverter( private val userWallet: UserWallet, + private val accountId: AccountId, private val clickIntents: WalletCurrencyActionsClickIntents, ) : Converter> { - private val userWalletId: UserWalletId = userWallet.walletId - override fun convert(value: TokenActionsState): ImmutableList { return value.states .filterIfSingleWithToken() @@ -58,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(userWalletId, cryptoCurrencyStatus, noneReason) } + action = { clickIntents.onBuyClick(accountId, cryptoCurrencyStatus, noneReason) } } is TokenActionsState.ActionState.Receive -> { title = resourceReference(R.string.common_receive) icon = R.drawable.ic_arrow_down_24 - action = { clickIntents.onReceiveClick(userWalletId, cryptoCurrencyStatus) } + action = { clickIntents.onReceiveClick(accountId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Stake -> { title = resourceReference(R.string.common_stake) icon = R.drawable.ic_staking_24 - action = { clickIntents.onStakeClick(userWalletId, cryptoCurrencyStatus, actionsState.option) } + action = { clickIntents.onStakeClick(accountId, cryptoCurrencyStatus, actionsState.option) } } is TokenActionsState.ActionState.Sell -> { title = resourceReference(R.string.common_sell) @@ -78,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(userWalletId, cryptoCurrencyStatus, noneReason) } + action = { clickIntents.onSendClick(accountId, cryptoCurrencyStatus, noneReason) } } is TokenActionsState.ActionState.Swap -> { title = resourceReference(R.string.swapping_swap_action) @@ -86,7 +85,7 @@ internal class MultiWalletCurrencyActionsConverter( action = { clickIntents.onSwapClick( cryptoCurrencyStatus = cryptoCurrencyStatus, - userWalletId = userWalletId, + accountId = accountId, unavailabilityReason = noneReason, ) } @@ -94,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(userWalletId, cryptoCurrencyStatus) } + action = { clickIntents.onCopyAddressClick(accountId, cryptoCurrencyStatus) } } is TokenActionsState.ActionState.HideToken -> { title = resourceReference(R.string.token_details_hide_token) icon = R.drawable.ic_hide_24 - action = { clickIntents.onHideTokensClick(userWalletId, cryptoCurrencyStatus) } + action = { clickIntents.onHideTokensClick(accountId, 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 1847ff842f..c33c75e00e 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 @@ -1,7 +1,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference @@ -9,7 +11,6 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.common.util.cardTypesResolver -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId @@ -49,35 +50,39 @@ internal class TokenListStateConverter( shouldShowMainPromo, ) - private val onTokenClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = + private val onTokenClick: (accountId: AccountId, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> - clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) + clickIntents.onTokenItemClick(accountId, currencyStatus) } - private val onTokenLongClick: (accountId: AccountId?, currencyStatus: CryptoCurrencyStatus) -> Unit = + private val onTokenLongClick: (accountId: AccountId, currencyStatus: CryptoCurrencyStatus) -> Unit = { accountId, currencyStatus -> - clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) + clickIntents.onTokenItemLongClick(accountId, currencyStatus) } - private val onApyLabelClick: - (currencyStatus: CryptoCurrencyStatus, apySource: TokenItemStateConverter.ApySource, apy: String) -> Unit = - { currencyStatus, apySource, apy -> + private val onApyLabelClick: ( + currencyStatus: CryptoCurrencyStatus, + accountId: AccountId, + apySource: TokenItemStateConverter.ApySource, + apy: String, + ) -> Unit = + { currencyStatus, accountId, apySource, apy -> clickIntents.onApyLabelClick( - userWalletId = selectedWallet.walletId, + accountId = accountId, currencyStatus = currencyStatus, apySource = apySource, apy = apy, ) } - private fun tokenStatusConverter(accountId: AccountId? = null) = TokenItemStateConverter( + private fun tokenStatusConverter(accountId: AccountId) = TokenItemStateConverter( appCurrency = appCurrency, yieldModuleApyMap = yieldModuleApyMap, promoCryptoCurrencyStatus = yieldSupplyPromoBannerConverter.convert(params), stakingApyMap = stakingAvailabilityMap, onItemClick = { _, status -> onTokenClick(accountId, status) }, onItemLongClick = { _, status -> onTokenLongClick(accountId, status) }, - onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, apySource, apy) }, + onApyLabelClick = { status, apySource, apy -> onApyLabelClick(status, accountId, apySource, apy) }, onYieldPromoCloseClick = clickIntents::onYieldPromoCloseClick, onYieldPromoShown = clickIntents::onYieldPromoShown, onYieldPromoClicked = clickIntents::onYieldPromoClicked, @@ -87,7 +92,7 @@ internal class TokenListStateConverter( return when (params) { is TokenConverterParams.Account -> convertAccountList(params) is TokenConverterParams.Wallet -> convertTokenList( - tokenConverter = tokenStatusConverter((params.portfolioId as? PortfolioId.Account)?.accountId), + tokenConverter = tokenStatusConverter(params.accountId), tokenList = params.tokenList, ) } @@ -136,12 +141,17 @@ internal class TokenListStateConverter( is WalletTokensListState.ContentState.Locked -> tokensListState.items is WalletTokensListState.Empty -> listOf() } - return TokensListItemUM.Portfolio( + val onEmptyAction = PortfolioItemContentUM.Empty.Action( + text = resourceReference(id = R.string.main_manage_tokens), + onClick = { clickIntents.onManageTokensClick(account.accountId) }, + ) + return TokensListPortfolioItemConverter( tokenItemUM = accountItem, isExpanded = isExtend, isCollapsable = true, tokens = items.filterIsInstance().toPersistentList(), - ) + onEmptyAction = onEmptyAction, + ).convert(Unit) } val accountItems = accountList.accountStatuses diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt index 0828c8e871..ed2a68e455 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokenCurrencyItemConverter.kt @@ -17,9 +17,9 @@ import com.tangem.core.ui.format.bigdecimal.* import com.tangem.core.ui.res.TangemTheme import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.AccountId 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.staking.model.StakingAvailability import com.tangem.feature.wallet.child.wallet.model.intents.WalletContentClickIntents import com.tangem.feature.wallet.impl.R @@ -33,11 +33,12 @@ import java.math.BigDecimal internal class WalletTokenCurrencyItemConverter( private val appCurrency: AppCurrency, - private val selectedWallet: UserWallet, + private val accountId: AccountId, + private val shouldShowPromo: Boolean, private val yieldModuleApyMap: Map, private val clickIntents: WalletContentClickIntents, stakingAvailabilityMap: Map, -) : Converter, TangemTokenRowUM> { +) : Converter { private val currencyToIconStateConverter = CryptoCurrencyToIconStateConverter() private val earnApyConverter = EarnApyConverter( @@ -45,8 +46,7 @@ internal class WalletTokenCurrencyItemConverter( stakingApyMap = stakingAvailabilityMap, ) - override fun convert(value: Pair): TangemTokenRowUM { - val (currencyStatus, shouldShowPromo) = value + override fun convert(currencyStatus: CryptoCurrencyStatus): TangemTokenRowUM { val earnApyInfo = earnApyConverter.convert(currencyStatus) return TangemTokenRowUM.Content( @@ -59,6 +59,7 @@ internal class WalletTokenCurrencyItemConverter( topEndContentUM = toCurrencyRowTopEnd(currencyStatus), bottomEndContentUM = toCurrencyRowBottomEnd(currencyStatus), promoBannerUM = toPromoBannerUM( + accountId, currencyStatus, earnApyInfo.takeIf { shouldShowPromo }, ), @@ -68,7 +69,7 @@ internal class WalletTokenCurrencyItemConverter( -> null else -> { { - clickIntents.onTokenItemClick(selectedWallet.walletId, currencyStatus) + clickIntents.onTokenItemClick(accountId, currencyStatus) } } }, @@ -76,7 +77,7 @@ internal class WalletTokenCurrencyItemConverter( CryptoCurrencyStatus.Loading -> null else -> { { - clickIntents.onTokenItemLongClick(selectedWallet.walletId, currencyStatus) + clickIntents.onTokenItemLongClick(accountId, currencyStatus) } } }, @@ -117,7 +118,7 @@ internal class WalletTokenCurrencyItemConverter( onClick = if (earnApyInfo.apy != null) { { clickIntents.onApyLabelClick( - userWalletId = selectedWallet.walletId, + accountId = accountId, currencyStatus = currencyStatus, apySource = earnApyInfo.source, apy = earnApyInfo.apy, @@ -261,6 +262,7 @@ internal class WalletTokenCurrencyItemConverter( } private fun toPromoBannerUM( + accountId: AccountId, currencyStatus: CryptoCurrencyStatus, earnApyInfo: EarnApyConverter.EarnApyInfo?, ): TangemTokenRowUM.PromoBannerUM { @@ -281,7 +283,7 @@ internal class WalletTokenCurrencyItemConverter( onPromoBannerClick = { clickIntents.onYieldPromoClicked(currency) clickIntents.onApyLabelClick( - userWalletId = selectedWallet.walletId, + accountId = accountId, currencyStatus = currencyStatus, apySource = earnApyInfo.source, apy = earnApyInfo.apy, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt index b4cf26bf7e..005d2bb298 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt @@ -35,7 +35,7 @@ internal class WalletTokensListUMConverter( private val yieldModuleApyMap: Map, private val isAccountsModeEnabled: Boolean, private val expandedAccounts: Set, - stakingAvailabilityMap: Map, + private val stakingAvailabilityMap: Map, shouldShowMainPromo: Boolean, ) : Converter { @@ -48,15 +48,6 @@ internal class WalletTokensListUMConverter( ) } - private val currencyRowConverter by lazy(LazyThreadSafetyMode.NONE) { - WalletTokenCurrencyItemConverter( - appCurrency = appCurrency, - selectedWallet = selectedWallet, - yieldModuleApyMap = yieldModuleApyMap, - stakingAvailabilityMap = stakingAvailabilityMap, - clickIntents = clickIntents, - ) - } private val yieldSupplyPromoBannerConverter by lazy(LazyThreadSafetyMode.NONE) { YieldSupplyPromoBannerConverter( yieldModuleApyMap, @@ -64,6 +55,20 @@ internal class WalletTokensListUMConverter( ) } + private fun currencyRowConverter( + accountId: AccountId, + shouldShowPromo: Boolean, + ): WalletTokenCurrencyItemConverter { + return WalletTokenCurrencyItemConverter( + appCurrency = appCurrency, + accountId = accountId, + shouldShowPromo = shouldShowPromo, + yieldModuleApyMap = yieldModuleApyMap, + stakingAvailabilityMap = stakingAvailabilityMap, + clickIntents = clickIntents, + ) + } + override fun convert(value: AccountStatusList): WalletTokensListUM { val promoCryptoCurrency = yieldSupplyPromoBannerConverter.convert2(value = value) return if (value.accountStatuses.isEmpty()) { @@ -85,13 +90,13 @@ internal class WalletTokensListUMConverter( isExpanded = isExpanded || !isCollapsable, isCollapsable = isCollapsable, tokenList = getTokenListItems( - accountStatus.tokenList, + accountStatus, promoCryptoCurrency, ).toPersistentList(), ), ) } else { - getTokenListItems(accountStatus.tokenList, promoCryptoCurrency) + getTokenListItems(accountStatus, promoCryptoCurrency) } }.toPersistentList() @@ -103,10 +108,10 @@ internal class WalletTokensListUMConverter( } private fun getTokenListItems( - tokenList: TokenList, + accountStatus: AccountStatus.CryptoPortfolio, promoCryptoCurrency: CryptoCurrencyStatus?, ): Sequence { - return when (tokenList) { + return when (val tokenList = accountStatus.tokenList) { TokenList.Empty -> emptySequence() is TokenList.GroupedByNetwork -> { tokenList.groups.asSequence().flatMap { (network, currencies) -> @@ -120,7 +125,10 @@ internal class WalletTokensListUMConverter( currencies.asSequence().map { currencyStatus -> val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id TokensListItemUM2.Token( - tokenRowUM = currencyRowConverter.convert(currencyStatus to shouldShowPromo), + tokenRowUM = currencyRowConverter( + accountStatus.accountId, + shouldShowPromo, + ).convert(currencyStatus), ) }.toList(), ) @@ -131,7 +139,10 @@ internal class WalletTokensListUMConverter( tokenList.currencies.asSequence().map { currencyStatus -> val shouldShowPromo = promoCryptoCurrency?.currency?.id == currencyStatus.currency.id TokensListItemUM2.Token( - tokenRowUM = currencyRowConverter.convert(currencyStatus to shouldShowPromo), + tokenRowUM = currencyRowConverter( + accountStatus.accountId, + shouldShowPromo, + ).convert(currencyStatus), ) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt index 5c944d4473..8bff35ab6f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicAccountListSubscriber.kt @@ -6,7 +6,6 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.core.lce.Lce import com.tangem.domain.core.utils.getOrElse -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.tokenlist.TokenList @@ -66,7 +65,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { singleAccountTransform( maybeTokenList = maybeTokenList, appCurrency = appCurrency, - portfolioId = PortfolioId(mainAccount.accountId), + accountId = mainAccount.accountId, yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, @@ -111,7 +110,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { private fun singleAccountTransform( maybeTokenList: Lce, appCurrency: AppCurrency, - portfolioId: PortfolioId, + accountId: AccountId, yieldSupplyApyMap: Map = emptyMap(), stakingAvailabilityMap: Map = emptyMap(), shouldShowMainPromo: Boolean, @@ -140,7 +139,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { ) updateContent( - params = TokenConverterParams.Wallet(portfolioId, tokenList), + params = TokenConverterParams.Wallet(accountId, tokenList), appCurrency = appCurrency, yieldSupplyApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt index dfeb7d4b35..c00cc2569d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletButtonsSubscriber.kt @@ -2,7 +2,6 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.usecase.GetCryptoCurrencyActionsUseCaseV2 -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents @@ -33,17 +32,17 @@ internal class SingleWalletButtonsSubscriber @AssistedInject constructor( getCryptoCurrencyActionsUseCaseV2(accountId = accountId, currency = it.currency) } .onEach { - updateContent(tokenActionsState = it, portfolioId = PortfolioId(userWallet.walletId)) + updateContent(tokenActionsState = it) } } - private fun updateContent(tokenActionsState: TokenActionsState, portfolioId: PortfolioId) { + private fun updateContent(tokenActionsState: TokenActionsState) { stateController.update( SetCryptoCurrencyActionsTransformer( tokenActionsState = tokenActionsState, userWallet = userWallet, clickIntents = clickIntents, - portfolioId = portfolioId, + accountId = accountId, ), ) } 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 29616c5969..f8349a879c 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 @@ -3,22 +3,23 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrenc import androidx.compose.animation.* import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.SpacerH16 +import com.tangem.core.ui.components.SpacerH24 +import com.tangem.core.ui.components.buttons.SecondarySmallButton +import com.tangem.core.ui.components.buttons.SmallButtonConfig import com.tangem.core.ui.components.tokenlist.PortfolioListItem import com.tangem.core.ui.components.tokenlist.PortfolioTokensListItem +import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration import com.tangem.core.ui.res.TangemTheme @@ -58,7 +59,8 @@ internal fun LazyListScope.portfolioTokensList( portfolioIndex = portfolioIndex, isBalanceHidden = isBalanceHidden, ) - if (tokens.isEmpty()) { + val portfolioContent = portfolio.content + if (portfolioContent is PortfolioItemContentUM.Empty) { item( key = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${portfolio.id}", @@ -76,9 +78,7 @@ internal fun LazyListScope.portfolioTokensList( ), visible = isExpanded, ) { - NonContentItemContent( - modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing28), - ) + EmptyAccountContent(portfolioContent) } } return @@ -114,6 +114,28 @@ internal fun LazyListScope.portfolioTokensList( ) } +@Composable +private fun EmptyAccountContent(emptyConent: PortfolioItemContentUM.Empty, modifier: Modifier = Modifier) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerH16() + NonContentItemContent() + val emptyAction = emptyConent.action + if (emptyAction != null) { + SpacerH16() + SecondarySmallButton( + config = SmallButtonConfig( + text = emptyAction.text, + onClick = { emptyAction.onClick() }, + ), + ) + } + SpacerH24() + } +} + @Suppress("MagicNumber") private fun LazyListScope.portfolioItem( portfolio: TokensListItemUM.Portfolio, diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt index efb582f714..cbdb878996 100644 --- a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt @@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.convert import com.google.common.truth.Truth.assertThat import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -22,7 +23,7 @@ class YieldSupplyPromoBannerConverterTest { val status = createLoadedStatus(token = token, amount = BigDecimal.ONE, isYieldActive = false) val tokenList = ungroupedTokenList(status) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = tokenList, ) val converter = YieldSupplyPromoBannerConverter( @@ -40,7 +41,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xA1") val status = createLoadedStatus(token = token, amount = BigDecimal("2.0"), isYieldActive = false) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( @@ -58,7 +59,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xAA") val statusActive = createLoadedStatus(token = token, amount = BigDecimal("5"), isYieldActive = true) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(statusActive), ) val converter = YieldSupplyPromoBannerConverter( @@ -86,7 +87,7 @@ class YieldSupplyPromoBannerConverterTest { ) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(statusSmall, statusBig), ) val converter = YieldSupplyPromoBannerConverter( @@ -109,7 +110,7 @@ class YieldSupplyPromoBannerConverterTest { val apyMap = mapOf(mismatchedKey to BigDecimal("0.07")) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( @@ -127,7 +128,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = "ethereum", backendId = "ethereum", contract = "0xCUSTOM") val status = createCustomStatus(token = token, amount = BigDecimal("5.0"), isYieldActive = false) val params = TokenConverterParams.Wallet( - portfolioId = PortfolioId.Wallet(UserWalletId("00")), + accountId = AccountId.forMainCryptoPortfolio(UserWalletId("00")), tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( From 665d56c9e8ba5e5c1d2ae91b020d92645b961cab Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 2 Mar 2026 13:35:44 +0400 Subject: [PATCH 83/97] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 7 +- .../com/tangem/common/routing/AppRoute.kt | 8 +- .../DefaultManageTokensRepository.kt | 131 ++---------------- .../managetokens/di/ManageTokensDataModule.kt | 9 -- .../utils/ManagedCryptoCurrencyFactory.kt | 47 ++----- .../model/ManageTokensListConfig.kt | 24 +--- .../createedit/AccountCreateEditModel.kt | 3 +- .../account/details/AccountDetailsModel.kt | 3 +- features/manage-tokens/api/build.gradle.kts | 1 + .../component/ManageTokensSource.kt | 18 ++- .../impl/detekt-baseline-debug.xml | 26 ---- .../model/ChooseManagedTokensModel.kt | 30 ++-- .../impl/DefaultManageTokensComponent.kt | 11 +- .../preview/PreviewAddCustomTokenComponent.kt | 6 +- .../PreviewCustomTokenSelectorComponent.kt | 4 +- .../preview/PreviewManageTokensComponent.kt | 10 +- .../ManageTokensBottomSheetConfig.kt | 19 --- .../model/CustomTokenFormModel.kt | 14 +- .../model/CustomTokenSelectorModel.kt | 22 +-- .../managetokens/model/ManageTokensModel.kt | 27 ++-- .../model/OnboardingManageTokensModel.kt | 21 +-- .../ui/AddCustomTokenBottomSheet.kt | 2 +- .../ui/CustomTokenSelectorContent.kt | 2 +- .../managetokens/ui/ManageTokensScreen.kt | 4 +- .../list/CustomTokenFormUseCasesFacade.kt | 40 ++---- .../utils/list/ManageTokensListManager.kt | 38 ++--- .../utils/list/ManageTokensUseCasesFacade.kt | 37 +---- .../utils/list/ManageTokensWarningDelegate.kt | 4 +- .../model/WalletSettingsModel.kt | 16 +-- .../router/DefaultWalletRouter.kt | 3 +- 30 files changed, 135 insertions(+), 452 deletions(-) delete mode 100644 features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.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 631b5f6ba3..4131a1c99f 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,7 +2,6 @@ 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 @@ -143,11 +142,7 @@ internal class ChildFactory @Inject constructor( AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT } - val mode = when (val portfolio = route.portfolioId) { - is PortfolioId.Account -> ManageTokensMode.Account(portfolio.accountId) - is PortfolioId.Wallet -> ManageTokensMode.Wallet(portfolio.userWalletId) - null -> ManageTokensMode.None - } + val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None createComponentChild( context = context, 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 a926dcb3ec..07b256a7eb 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 @@ -7,13 +7,12 @@ import android.os.Bundle import com.tangem.common.routing.bundle.RouteBundleParams import com.tangem.common.routing.bundle.bundle import com.tangem.common.routing.entity.InitScreenLaunchMode -import com.tangem.domain.staking.model.StakingIntegrationID 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.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.serialization.SerializedBigDecimal @@ -21,6 +20,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTAsset import com.tangem.domain.onramp.model.OnrampSource import com.tangem.domain.pay.TangemPayDetailsConfig +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.tokens.model.details.NavigationAction import kotlinx.serialization.Serializable @@ -128,8 +128,8 @@ sealed class AppRoute(val path: String) : Route { @Serializable data class ManageTokens( val source: Source, - val portfolioId: PortfolioId? = null, - ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${portfolioId?.stringValue}") { + val accountId: AccountId? = null, + ) : AppRoute(path = "${source.name.lowercase()}/manage_tokens/${accountId?.value}") { /** * Source of launching the screen. diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt index 98f8097fbc..28b44ce20f 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/DefaultManageTokensRepository.kt @@ -7,10 +7,6 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.account.WalletAccountsFetcher -import com.tangem.data.common.api.safeApiCall -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.network.NetworkFactory import com.tangem.data.common.utils.retryOnError import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher @@ -22,7 +18,6 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.orDefault import com.tangem.datasource.local.config.testnet.TestnetTokensStorage import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.TapWorkarounds.isTestCard import com.tangem.domain.card.common.extensions.* import com.tangem.domain.card.common.util.cardTypesResolver @@ -48,24 +43,19 @@ import com.tangem.utils.coroutines.runSuspendCatching internal class DefaultManageTokensRepository( private val tangemTechApi: TangemTechApi, private val userWalletsListRepository: UserWalletsListRepository, - private val userTokenSaver: UserTokensSaver, private val manageTokensUpdateFetcher: ManageTokensUpdateFetcher, private val userTokensResponseStore: UserTokensResponseStore, private val testnetTokensStorage: TestnetTokensStorage, private val excludedBlockchains: ExcludedBlockchains, - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val dispatchers: CoroutineDispatcherProvider, private val walletAccountsFetcher: WalletAccountsFetcher, - private val accountsFeatureToggles: AccountsFeatureToggles, networkFactory: NetworkFactory, ) : ManageTokensRepository { private val managedCryptoCurrencyFactory = ManagedCryptoCurrencyFactory( networkFactory = networkFactory, excludedBlockchains = excludedBlockchains, - accountsFeatureToggles = accountsFeatureToggles, ) - private val userTokensResponseFactory = UserTokensResponseFactory() // region getTokenListBatchFlow override fun getTokenListBatchFlow( @@ -92,10 +82,7 @@ internal class DefaultManageTokensRepository( val userWallet = request.params.userWalletId?.let(userWalletsListRepository::getSyncStrict) if (userWallet is UserWallet.Cold && userWallet.scanResponse.card.isTestCard) { - when (val params = request.params) { - is ManageTokensListConfig.Account -> fetchTestnetCurrencies(userWallet, params) - is ManageTokensListConfig.Wallet -> fetchTestnetCurrenciesLegacy(userWallet, params) - } + fetchTestnetCurrencies(userWallet, request.params) } else { fetchCurrencies( userWallet = userWallet, @@ -139,24 +126,14 @@ internal class DefaultManageTokensRepository( coins = coinsResponse.coins.filterNot { l2BlockchainsCoinIds.contains(it.id) }, ) - val items = when (val params = request.params) { - is ManageTokensListConfig.Account -> createManagedCryptoCurrencyList( - params = params, - userWallet = userWallet, - isFirstBatchFetching = isFirstBatchFetching, - loadUserTokensFromRemote = loadUserTokensFromRemote, - query = query, - updatedCoinsResponse = updatedCoinsResponse, - ) - is ManageTokensListConfig.Wallet -> createManagedCryptoCurrencyListLegacy( - params = params, - userWallet = userWallet, - isFirstBatchFetching = isFirstBatchFetching, - loadUserTokensFromRemote = loadUserTokensFromRemote, - query = query, - updatedCoinsResponse = updatedCoinsResponse, - ) - } + val items = createManagedCryptoCurrencyList( + params = request.params, + userWallet = userWallet, + isFirstBatchFetching = isFirstBatchFetching, + loadUserTokensFromRemote = loadUserTokensFromRemote, + query = query, + updatedCoinsResponse = updatedCoinsResponse, + ) return BatchFetchResult.Success( data = items, @@ -167,7 +144,7 @@ internal class DefaultManageTokensRepository( @Suppress("CyclomaticComplexMethod") private suspend fun createManagedCryptoCurrencyList( - params: ManageTokensListConfig.Account, + params: ManageTokensListConfig, userWallet: UserWallet?, isFirstBatchFetching: Boolean, loadUserTokensFromRemote: Boolean, @@ -238,56 +215,9 @@ internal class DefaultManageTokensRepository( return items } - private suspend fun createManagedCryptoCurrencyListLegacy( - params: ManageTokensListConfig.Wallet, - userWallet: UserWallet?, - isFirstBatchFetching: Boolean, - loadUserTokensFromRemote: Boolean, - query: String?, - updatedCoinsResponse: CoinsResponse, - ): List { - val tokensResponse = params.userWalletId?.let { userWalletId -> - if (loadUserTokensFromRemote && userWallet != null) { - safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) { - // save tokens response only if loadUserTokensFromRemote is true and it means onboarding call - createAndSaveDefaultUserTokensResponse(userWallet = userWallet) - } - } else { - getSavedUserTokensResponseSync(userWalletId) - } - } - - val isCreateWithCustom = isFirstBatchFetching && - tokensResponse != null && - userWallet != null && - query == null - - return if (isCreateWithCustom) { - managedCryptoCurrencyFactory.createWithCustomTokens( - coinsResponse = updatedCoinsResponse, - tokensResponse = tokensResponse, - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - } else { - managedCryptoCurrencyFactory.create( - coinsResponse = updatedCoinsResponse, - tokensResponse = tokensResponse, - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - } - } - - private suspend fun createAndSaveDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse { - val userTokensResponse = createDefaultUserTokensResponse(userWallet) - userTokenSaver.store(userWallet.walletId, userTokensResponse, useEnricher = false) - return userTokensResponse - } - private suspend fun fetchTestnetCurrencies( userWallet: UserWallet, - params: ManageTokensListConfig.Account, + params: ManageTokensListConfig, ): BatchFetchResult.Success> { val searchText = params.searchText val testnetTokensConfig = testnetTokensStorage.getConfig() @@ -342,45 +272,6 @@ internal class DefaultManageTokensRepository( ) } - private suspend fun fetchTestnetCurrenciesLegacy( - userWallet: UserWallet, - params: ManageTokensListConfig.Wallet, - ): BatchFetchResult.Success> { - val searchText = params.searchText - val testnetTokensConfig = testnetTokensStorage.getConfig() - - val items = managedCryptoCurrencyFactory.createTestnetWithCustomTokens( - testnetTokensConfig = if (!searchText.isNullOrBlank()) { - testnetTokensConfig.copy( - tokens = testnetTokensConfig.tokens.filter { token -> - token.symbol.contains(other = searchText, ignoreCase = true) || - token.name.contains(other = searchText, ignoreCase = true) - }, - ) - } else { - testnetTokensConfig - }, - tokensResponse = getSavedUserTokensResponseSync(userWallet.walletId), - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - - return BatchFetchResult.Success( - data = items, - empty = items.isEmpty(), - last = true, - ) - } - - private fun createDefaultUserTokensResponse(userWallet: UserWallet) = - userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet( - userWallet = userWallet, - ), - isGroupedByNetwork = false, - isSortedByBalance = false, - ) - private fun getSupportedBlockchains(userWallet: UserWallet?): List { return userWallet?.supportedBlockchains(excludedBlockchains) ?: Blockchain.entries.filter { diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt index bd5efcfcc6..01922b4996 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/di/ManageTokensDataModule.kt @@ -2,8 +2,6 @@ package com.tangem.data.managetokens.di import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.account.WalletAccountsFetcher -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.common.network.NetworkFactory import com.tangem.data.managetokens.DefaultCustomTokensRepository import com.tangem.data.managetokens.DefaultManageTokensRepository @@ -11,7 +9,6 @@ import com.tangem.data.managetokens.utils.ManageTokensUpdateFetcher import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.config.testnet.TestnetTokensStorage import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.managetokens.repository.CustomTokensRepository import com.tangem.domain.managetokens.repository.ManageTokensRepository @@ -33,13 +30,10 @@ internal object ManageTokensDataModule { userWalletsListRepository: UserWalletsListRepository, manageTokensUpdateFetcher: ManageTokensUpdateFetcher, userTokensResponseStore: UserTokensResponseStore, - userTokensSaver: UserTokensSaver, testnetTokensStorage: TestnetTokensStorage, dispatchers: CoroutineDispatcherProvider, excludedBlockchains: ExcludedBlockchains, - cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, networkFactory: NetworkFactory, - accountsFeatureToggles: AccountsFeatureToggles, walletAccountsFetcher: WalletAccountsFetcher, ): ManageTokensRepository { return DefaultManageTokensRepository( @@ -47,13 +41,10 @@ internal object ManageTokensDataModule { userWalletsListRepository = userWalletsListRepository, manageTokensUpdateFetcher = manageTokensUpdateFetcher, userTokensResponseStore = userTokensResponseStore, - userTokenSaver = userTokensSaver, testnetTokensStorage = testnetTokensStorage, excludedBlockchains = excludedBlockchains, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, networkFactory = networkFactory, dispatchers = dispatchers, - accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, ) } diff --git a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt index 22beb60476..efdfc59129 100644 --- a/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt +++ b/data/manage-tokens/src/main/kotlin/com/tangem/data/managetokens/utils/ManagedCryptoCurrencyFactory.kt @@ -13,7 +13,6 @@ import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.CoinsResponse import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.config.testnet.models.TestnetTokensConfig -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.extensions.canHandleToken import com.tangem.domain.managetokens.model.ManagedCryptoCurrency import com.tangem.domain.managetokens.model.ManagedCryptoCurrency.SourceNetwork @@ -21,7 +20,6 @@ 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.UserWallet -import com.tangem.domain.wallets.derivations.DerivationStyleProvider import com.tangem.domain.wallets.derivations.derivationStyleProvider import com.tangem.lib.crypto.BlockchainUtils import timber.log.Timber @@ -29,7 +27,6 @@ import timber.log.Timber internal class ManagedCryptoCurrencyFactory( private val networkFactory: NetworkFactory, private val excludedBlockchains: ExcludedBlockchains, - private val accountsFeatureToggles: AccountsFeatureToggles, ) { fun create( @@ -120,30 +117,15 @@ internal class ManagedCryptoCurrencyFactory( ?.takeUnless { it in excludedBlockchains } ?: return null - val network = if (accountsFeatureToggles.isFeatureEnabled) { - val network = networkFactory.create( - blockchain = blockchain, - extraDerivationPath = token.derivationPath, - userWallet = userWallet, - accountIndex = accountIndex, - ) ?: return null + val network = networkFactory.create( + blockchain = blockchain, + extraDerivationPath = token.derivationPath, + userWallet = userWallet, + accountIndex = accountIndex, + ) ?: return null - if (!checkIsCustomToken(token, network.derivationPath)) { - return null - } - - network - } else { - if (!checkIsCustomToken(token, blockchain, userWallet.derivationStyleProvider)) { - return null - } - - networkFactory.create( - blockchain = blockchain, - extraDerivationPath = token.derivationPath, - userWallet = userWallet, - accountIndex = accountIndex, - ) ?: return null + if (!checkIsCustomToken(token, network.derivationPath)) { + return null } val contractAddress = token.contractAddress @@ -285,23 +267,10 @@ internal class ManagedCryptoCurrencyFactory( return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png" } - private fun checkIsCustomToken( - token: UserTokensResponse.Token, - blockchain: Blockchain, - derivationStyleProvider: DerivationStyleProvider, - ): Boolean = token.id.isNullOrBlank() || - checkIsCustomDerivationPath(token.derivationPath, blockchain, derivationStyleProvider) - private fun checkIsCustomToken(token: UserTokensResponse.Token, derivationPath: Network.DerivationPath): Boolean { return token.id.isNullOrBlank() || derivationPath is Network.DerivationPath.Custom } - private fun checkIsCustomDerivationPath( - derivationPath: String?, - blockchain: Blockchain, - derivationStyleProvider: DerivationStyleProvider, - ): Boolean = derivationPath != blockchain.derivationPath(derivationStyleProvider.getDerivationStyle())?.rawPath - /** * Filter tokens for TerraV1 (Terra Classic) network. * Only native coin (LUNC) and TerraClassicUSD (USTC) are allowed. diff --git a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt index 8ac66e350c..fad77173a5 100644 --- a/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt +++ b/domain/manage-tokens/src/main/kotlin/com/tangem/domain/managetokens/model/ManageTokensListConfig.kt @@ -3,24 +3,10 @@ package com.tangem.domain.managetokens.model import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId -sealed interface ManageTokensListConfig { - +data class ManageTokensListConfig( + val accountId: AccountId?, + val searchText: String?, +) { val userWalletId: UserWalletId? - val searchText: String? - - // old way - data class Wallet( - override val userWalletId: UserWalletId?, - override val searchText: String?, - ) : ManageTokensListConfig - - // new way - data class Account( - val accountId: AccountId?, - override val searchText: String?, - ) : ManageTokensListConfig { - - override val userWalletId: UserWalletId? - get() = accountId?.userWalletId - } + get() = accountId?.userWalletId } \ No newline at end of file diff --git a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt index 5870bd4851..5ef205ef80 100644 --- a/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt +++ b/features/account/impl/src/main/java/com/tangem/features/account/createedit/AccountCreateEditModel.kt @@ -24,7 +24,6 @@ import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.usecase.AddCryptoPortfolioUseCase import com.tangem.domain.account.usecase.GetUnoccupiedAccountIndexUseCase import com.tangem.domain.account.usecase.UpdateCryptoPortfolioUseCase -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.wallet.UserWalletId @@ -141,7 +140,7 @@ internal class AccountCreateEditModel @Inject constructor( showMessage(R.string.account_create_success_message) val route = AppRoute.ManageTokens( source = AppRoute.ManageTokens.Source.ACCOUNT, - portfolioId = PortfolioId(account.accountId), + accountId = account.accountId, ) router.replaceCurrent(route) } 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 ee77d31aa2..d9b4788e38 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 @@ -16,7 +16,6 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.ToastMessage import com.tangem.domain.account.status.usecase.ArchiveCryptoPortfolioUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier -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 @@ -65,7 +64,7 @@ internal class AccountDetailsModel @Inject constructor( private fun onManageTokensClick(account: Account.CryptoPortfolio) { val route = AppRoute.ManageTokens( source = AppRoute.ManageTokens.Source.ACCOUNT, - portfolioId = PortfolioId(account.accountId), + accountId = account.accountId, ) analyticsEventHandler.send( AccountSettingsAnalyticEvents.ButtonManageTokens(account.derivationIndex.value), diff --git a/features/manage-tokens/api/build.gradle.kts b/features/manage-tokens/api/build.gradle.kts index b1eb0e3830..0cf7ab210c 100644 --- a/features/manage-tokens/api/build.gradle.kts +++ b/features/manage-tokens/api/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) id("configuration") } 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 ca4d8956b2..fcce6daea7 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 @@ -2,6 +2,7 @@ package com.tangem.features.managetokens.component import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId +import kotlinx.serialization.Serializable enum class ManageTokensSource(val analyticsName: String) { STORIES(analyticsName = "Stories"), @@ -12,18 +13,15 @@ enum class ManageTokensSource(val analyticsName: String) { } sealed interface ManageTokensMode { - data class Wallet(val userWalletId: UserWalletId) : ManageTokensMode - data class Account(val accountId: AccountId) : ManageTokensMode + data class Account(val accountId: AccountId) : ManageTokensMode { + constructor(userWalletId: UserWalletId) : this(AccountId.forMainCryptoPortfolio(userWalletId)) + } data object None : ManageTokensMode } -sealed interface AddCustomTokenMode { +@Serializable +data class AddCustomTokenMode(val accountId: AccountId) { + val userWalletId: UserWalletId = accountId.userWalletId - val userWalletId: UserWalletId - - data class Wallet(override val userWalletId: UserWalletId) : AddCustomTokenMode - - data class Account(val accountId: AccountId) : AddCustomTokenMode { - override val userWalletId: UserWalletId = accountId.userWalletId - } + constructor(userWalletId: UserWalletId) : this(AccountId.forMainCryptoPortfolio(userWalletId)) } \ No newline at end of file diff --git a/features/manage-tokens/impl/detekt-baseline-debug.xml b/features/manage-tokens/impl/detekt-baseline-debug.xml index 6c3a7af5c8..d179bf1933 100644 --- a/features/manage-tokens/impl/detekt-baseline-debug.xml +++ b/features/manage-tokens/impl/detekt-baseline-debug.xml @@ -4,47 +4,21 @@ BooleanPropertyNaming:AddCustomTokenUM.kt$AddCustomTokenUM$val showBackButton: Boolean BooleanPropertyNaming:CustomCurrencyValidator.kt$CustomCurrencyValidator.Status.Validated$val fillForm: Boolean - BooleanPropertyNaming:CustomTokenFormModel.kt$CustomTokenFormModel$val needColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke( userWalletId = params.mode.userWalletId, networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), ) - BooleanPropertyNaming:ManageTokensListManager.kt$ManageTokensListManager$val loadUserTokensFromRemote = when (mode) { is ManageTokensMode.Wallet, is ManageTokensMode.Account, -> source == ManageTokensSource.ONBOARDING ManageTokensMode.None, -> false } - BooleanPropertyNaming:ManageTokensModel.kt$ManageTokensModel$val needToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) BooleanPropertyNaming:ManageTokensUM.kt$ManageTokensUM.ManageContent$val needToInteractWithColdWallet: Boolean - BooleanPropertyNaming:OnboardingManageTokensModel.kt$OnboardingManageTokensModel$val showTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) BooleanPropertyNaming:OnboardingManageTokensUM.kt$OnboardingManageTokensUM.ActionButtonConfig$abstract val showProgress: Boolean BooleanPropertyNaming:OnboardingManageTokensUM.kt$OnboardingManageTokensUM.ActionButtonConfig.Continue$val showTangemIcon: Boolean - MaxChainedCallsOnSameLine:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$params.initialCurrency.network.id.rawId.value MultilineLambdaItParameter:ChooseManagedTokenContent.kt${ add( CurrencyItemUM.Basic( id = ManagedCryptoCurrency.ID( value = "ID+$it", ), name = "Bitcoin", symbol = "BTC", icon = CurrencyIconState.Loading, networks = CurrencyItemUM.Basic.NetworksUM.Collapsed, onExpandClick = {}, ), ) } - MultilineLambdaItParameter:ChooseManagedTokensModel.kt$ChooseManagedTokensModel${ it.copy( notificationUM = null, ) } MultilineLambdaItParameter:CurrencyItemMapper.kt${ it.toCurrencyNetworkModel( isSelected = it.network in addedIn, isEditable = false, onSelectedStateChange = { _, _ -> }, onLongTap = { _ -> }, ) } MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.CONTRACT_ADDRESS] = it.getValue(Field.CONTRACT_ADDRESS).copy( error = when (exception) { CustomTokenFormValidationException.ContractAddress.Invalid -> { resourceReference(R.string.custom_token_creation_error_invalid_contract_address) } }, ) } MultilineLambdaItParameter:CustomCurrencyFormOperations.kt${ it[Field.DECIMALS] = it.getValue(Field.DECIMALS).copy( error = when (exception) { is CustomTokenFormValidationException.Decimals.Empty -> { null // Should not display this error } is CustomTokenFormValidationException.Decimals.Invalid -> { resourceReference( R.string.custom_token_creation_error_wrong_decimals, wrappedList(ValidateTokenFormUseCase.MAX_DECIMALS), ) } }, ) } MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", error = stringReference("Contract address is invalid"), placeholder = stringReference("0x1234567890"), ) } MultilineLambdaItParameter:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider${ it[Field.CONTRACT_ADDRESS] = it[Field.CONTRACT_ADDRESS]!!.copy( label = stringReference("Contract address"), value = "0x1234567890", placeholder = stringReference("0x1234567890"), ) } - MultilineLambdaItParameter:CustomTokenFormModel.kt$CustomTokenFormModel${ Timber.e(it, "Failed to add currency") showErrorDialog() return@resource } - MultilineLambdaItParameter:CustomTokenFormModel.kt$CustomTokenFormModel${ Timber.e(it, "Failed to derive public keys") showErrorDialog() return@resource } - MultilineLambdaItParameter:CustomTokenSelectorModel.kt$CustomTokenSelectorModel${ when (it) { is AccountStatus.CryptoPortfolio -> it.sameNodeAndNotMain() } } - MultilineLambdaItParameter:ManageTokensListManager.kt$ManageTokensListManager${ Timber.e( it, """ Failed to check currency unsupported state |- Mode: $mode |- Source Network: $sourceNetwork """.trimIndent(), ) val message = SnackbarMessage( message = it.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) messageSender.send(message) null } - MultilineLambdaItParameter:ManageTokensListManager.kt$ManageTokensListManager${ Timber.e( it, """ Failed to check linked tokens |- Mode: $mode |- Network ID: ${network.id} """.trimIndent(), ) val message = SnackbarMessage( message = it.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) messageSender.send(message) false } - MultilineLambdaItParameter:ManageTokensModel.kt$ManageTokensModel${ Timber.e(it, "Failed to save changes") return@resource } - MultilineLambdaItParameter:ManageTokensUseCasesFacade.kt$ManageTokensUseCasesFacade${ it is CryptoCurrency.Token && it.network.backendId == network.backendId && it.network.derivationPath == network.derivationPath } - MultilineLambdaItParameter:OnboardingManageTokensModel.kt$OnboardingManageTokensModel${ Timber.e(it, "Failed to save changes") return@resource } - MultilineLambdaItParameter:PreviewManageTokensComponent.kt$PreviewManageTokensComponent${ it.fastForEachIndexed { index, network -> if (index == networkIndex) { it[index] = network.copy( iconResId = if (isSelected) { R.drawable.img_eth_22 } else { R.drawable.ic_eth_16 }, isSelected = isSelected, ) } } } NamedArguments:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$updateChangedItems(currency, network, currenciesToAdd, currenciesToRemove) NamedArguments:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$updateChangedItems(currency, network, currenciesToRemove, currenciesToAdd) - NamedArguments:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$TokenSearched( params.analyticsCategoryName, token = null, blockchain = null, isTokenChosen = false, ) - NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$selectNetwork(currencyBatch.key, currency, networkId, isSelected) - NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) - NamedArguments:ManageTokensListManager.kt$ManageTokensListManager$sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true) - NestedScopeFunctions:CustomTokenSelectorModel.kt$CustomTokenSelectorModel$let { recognizer.recognize(it) } NullableBooleanCheck:CustomCurrencyFormOperations.kt$this.tokenForm?.wasFilled ?: false - PropertyUsedBeforeDeclaration:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$bottomSheetNavigation - PropertyUsedBeforeDeclaration:ChooseManagedTokensModel.kt$ChooseManagedTokensModel$uiState UnsafeCallOnNullableType:CustomTokenFormContent.kt$PreviewCustomTokenFormComponentProvider$it[Field.CONTRACT_ADDRESS]!! - UnsafeCallOnNullableType:PreviewAddCustomTokenComponent.kt$PreviewAddCustomTokenComponent$config.selectedDerivationPath!! - UnsafeCallOnNullableType:PreviewAddCustomTokenComponent.kt$PreviewAddCustomTokenComponent$config.selectedNetwork!! UseEmptyCounterpart:CustomTokenAnalyticsEvent.kt$CustomTokenAnalyticsEvent$mapOf() UseEmptyCounterpart:ManageTokensAnalyticEvent.kt$ManageTokensAnalyticEvent$mapOf() UseOrEmpty:ChangedCurrenciesManager.kt$ChangedCurrenciesManager$items[currency] ?: emptySet() - UseOrEmpty:PreviewCustomTokenSelectorComponent.kt$PreviewCustomTokenSelectorComponent$d.id?.rawId?.value ?: "" - VarCouldBeVal:CustomTokenFormModel.kt$CustomTokenFormModel$private var useCasesFacade: CustomTokenFormUseCasesFacade = customTokenFormUseCasesFacadeFactory.create(params.mode) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt index 7c1d1b2f84..43fbc9472f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/choosetoken/model/ChooseManagedTokensModel.kt @@ -17,8 +17,6 @@ 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.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.models.account.AccountId import com.tangem.domain.notifications.SetShouldShowNotificationUseCase import com.tangem.features.managetokens.choosetoken.entity.ChooseManageTokensBottomSheetConfig import com.tangem.features.managetokens.choosetoken.entity.ChooseManagedTokenUM @@ -45,7 +43,6 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -import kotlin.collections.isNotEmpty @Suppress("LongParameterList") @ModelScoped @@ -55,7 +52,6 @@ internal class ChooseManagedTokensModel @Inject constructor( private val uiMessageSender: UiMessageSender, private val setShouldShowNotificationUseCase: SetShouldShowNotificationUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - accountsFeatureToggles: AccountsFeatureToggles, paramsContainer: ParamsContainer, manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, manageTokensListManagerFactory: ManageTokensListManager.Factory, @@ -63,16 +59,15 @@ internal class ChooseManagedTokensModel @Inject constructor( private val params: ChooseManagedTokensComponent.Params = paramsContainer.require() - private val manageTokensMode = if (accountsFeatureToggles.isFeatureEnabled) { - val accountId = AccountId.forMainCryptoPortfolio(userWalletId = params.userWalletId) - ManageTokensMode.Account(accountId = accountId) - } else { - ManageTokensMode.Wallet(params.userWalletId) - } + private val manageTokensMode = ManageTokensMode.Account(params.userWalletId) private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory .create(mode = manageTokensMode) + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val uiState: StateFlow + field = MutableStateFlow(createReadContentModel()) + private val manageTokensListManager = manageTokensListManagerFactory.create( scope = modelScope, source = ManageTokensSource.SEND_VIA_SWAP, @@ -91,10 +86,6 @@ internal class ChooseManagedTokensModel @Inject constructor( }, ) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val uiState: StateFlow - field = MutableStateFlow(createReadContentModel()) - init { manageTokensListManager.uiItems .onEach { items -> updateItems(items) } @@ -146,11 +137,7 @@ internal class ChooseManagedTokensModel @Inject constructor( private fun removeNotification() { modelScope.launch { setShouldShowNotificationUseCase(NotificationId.SendViaSwapTokenSelectorNotification.key, false) - uiState.update { - it.copy( - notificationUM = null, - ) - } + uiState.update { it.copy(notificationUM = null) } } } @@ -161,7 +148,7 @@ internal class ChooseManagedTokensModel @Inject constructor( if (!new.readContent.search.isActive && old.readContent.search.isActive) { analyticsEventHandler.send( CommonManageTokensAnalyticEvents.TokenSearched( - params.analyticsCategoryName, + categoryName = params.analyticsCategoryName, token = null, blockchain = null, isTokenChosen = false, @@ -195,8 +182,9 @@ internal class ChooseManagedTokensModel @Inject constructor( val isToken = currency.id.value == params.initialCurrency.id.rawCurrencyId?.value // Ensure that initial token network is filtered out and network list is empty + val paramsRawId = params.initialCurrency.network.id.rawId val isEmptyNetworks = availableNetworks?.networks?.filterNot { network -> - network.id == params.initialCurrency.network.id.rawId.value + network.id == paramsRawId.value }.isNullOrEmpty() // Filter out currency from display diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt index bddf89e1cf..918483d121 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/impl/DefaultManageTokensComponent.kt @@ -16,11 +16,11 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.SnackbarMessage import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName import com.tangem.features.managetokens.component.AddCustomTokenComponent import com.tangem.features.managetokens.component.AddCustomTokenMode import com.tangem.features.managetokens.component.ManageTokensComponent -import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.impl.R import com.tangem.features.managetokens.model.ManageTokensModel import com.tangem.features.managetokens.ui.ManageTokensScreen @@ -38,7 +38,7 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( private val bottomSheetSlot = childSlot( source = model.bottomSheetNavigation, - serializer = ManageTokensBottomSheetConfig.serializer(), + serializer = AccountId.serializer(), handleBackButton = false, childFactory = ::bottomSheetChild, ) @@ -57,13 +57,10 @@ internal class DefaultManageTokensComponent @AssistedInject constructor( } private fun bottomSheetChild( - config: ManageTokensBottomSheetConfig, + accountId: AccountId, componentContext: ComponentContext, ): ComposableBottomSheetComponent { - val mode = when (config) { - is ManageTokensBottomSheetConfig.AddWalletCustomToken -> AddCustomTokenMode.Wallet(config.userWalletId) - is ManageTokensBottomSheetConfig.AddAccountCustomToken -> AddCustomTokenMode.Account(config.accountId) - } + val mode = AddCustomTokenMode(accountId) return addCustomTokenComponentFactory.create( context = childByContext(componentContext), params = AddCustomTokenComponent.Params( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt index 7427de4342..b142c79097 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewAddCustomTokenComponent.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class PreviewAddCustomTokenComponent( initialState: AddCustomTokenConfig = AddCustomTokenConfig( - mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")), + mode = AddCustomTokenMode(UserWalletId(stringValue = "321")), step = AddCustomTokenConfig.Step.INITIAL_NETWORK_SELECTOR, ), ) : AddCustomTokenComponent { @@ -61,8 +61,8 @@ internal class PreviewAddCustomTokenComponent( PreviewCustomTokenSelectorComponent( params = CustomTokenSelectorComponent.Params.DerivationPathSelector( mode = config.mode, - selectedNetwork = config.selectedNetwork!!, - selectedDerivationPath = config.selectedDerivationPath!!, + selectedNetwork = requireNotNull(config.selectedNetwork), + selectedDerivationPath = requireNotNull(config.selectedDerivationPath), onDerivationPathSelected = { _, _ -> }, ), ).Content(modifier) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index bcdfa62787..ac0bd61222 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -19,7 +19,7 @@ import kotlinx.collections.immutable.toImmutableList internal class PreviewCustomTokenSelectorComponent( private val params: Params = Params.NetworkSelector( - mode = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")), + mode = AddCustomTokenMode(UserWalletId(stringValue = "321")), selectedNetwork = null, onNetworkSelected = {}, ), @@ -39,7 +39,7 @@ internal class PreviewCustomTokenSelectorComponent( ) DerivationPathUM( - id = d.id?.rawId?.value ?: "", + id = d.id?.rawId?.value.orEmpty(), value = d.value.value.orEmpty(), networkName = stringReference(d.name), isSelected = d.value == params.selectedDerivationPath?.value, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index bf0778fc87..ed67875a8e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -40,9 +40,7 @@ internal class PreviewManageTokensComponent( popBack = {}, items = items, topBar = when (params.mode) { - is ManageTokensMode.Account, - is ManageTokensMode.Wallet, - -> ManageTokensTopBarUM.ManageContent( + is ManageTokensMode.Account -> ManageTokensTopBarUM.ManageContent( title = resourceReference(id = R.string.main_manage_tokens), onBackButtonClick = {}, endButton = TopAppBarButtonUM.Icon( @@ -206,10 +204,10 @@ internal class PreviewManageTokensComponent( is CurrencyItemUM.Basic -> { val updatedNetworks = (item.networks as? CurrencyItemUM.Basic.NetworksUM.Expanded) ?.copy( - networks = item.networks.networks.toPersistentList().mutate { - it.fastForEachIndexed { index, network -> + networks = item.networks.networks.toPersistentList().mutate { networks -> + networks.fastForEachIndexed { index, network -> if (index == networkIndex) { - it[index] = network.copy( + networks[index] = network.copy( iconResId = if (isSelected) { R.drawable.img_eth_22 } else { diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt deleted file mode 100644 index 91a4b7fba3..0000000000 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/entity/managetokens/ManageTokensBottomSheetConfig.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.tangem.features.managetokens.entity.managetokens - -import com.tangem.domain.models.account.AccountId -import com.tangem.domain.models.wallet.UserWalletId -import kotlinx.serialization.Serializable - -@Serializable -internal sealed class ManageTokensBottomSheetConfig { - - @Serializable - data class AddWalletCustomToken( - val userWalletId: UserWalletId, - ) : ManageTokensBottomSheetConfig() - - @Serializable - data class AddAccountCustomToken( - val accountId: AccountId, - ) : ManageTokensBottomSheetConfig() -} \ No newline at end of file diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index 1ce00a7b83..ea9d7eaa60 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -179,7 +179,7 @@ internal class CustomTokenFormModel @Inject constructor( isAlreadyAdded: Boolean, isCustom: Boolean, ) = modelScope.launch { - val needColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke( + val isNeedColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke( userWalletId = params.mode.userWalletId, networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), ) @@ -193,7 +193,7 @@ internal class CustomTokenFormModel @Inject constructor( clearNotifications = true, clearFieldErrors = true, disableSecondaryFields = !isCustom, - walletInteractionIcon = R.drawable.ic_tangem_24.takeIf { needColdWalletInteraction }, + walletInteractionIcon = R.drawable.ic_tangem_24.takeIf { isNeedColdWalletInteraction }, ) if (fillForm) { @@ -353,14 +353,8 @@ internal class CustomTokenFormModel @Inject constructor( ) analyticsEventHandler.send(event) - useCasesFacade.derivePublicKeysUseCase(listOf(currency)).getOrElse { - Timber.e(it, "Failed to derive public keys") - showErrorDialog() - return@resource - } - - useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { - Timber.e(it, "Failed to add currency") + useCasesFacade.addCryptoCurrenciesUseCase(currency).getOrElse { throwable -> + Timber.e(throwable, "Failed to add currency") showErrorDialog() return@resource } 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 7aa77b2a2b..731f1757df 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 @@ -15,7 +15,6 @@ 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.Account @@ -52,7 +51,6 @@ internal class CustomTokenSelectorModel @Inject constructor( private val getSupportedNetworksUseCase: GetSupportedNetworksUseCase, private val messageSender: UiMessageSender, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val accountsFeatureToggles: AccountsFeatureToggles, paramsContainer: ParamsContainer, ) : Model() { @@ -171,7 +169,7 @@ internal class CustomTokenSelectorModel @Inject constructor( } private suspend fun getSupportedNetworks(mode: AddCustomTokenMode): List { - return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { e -> + return getSupportedNetworksUseCase(mode.userWalletId).getOrElse { _ -> val message = SnackbarMessage(message = resourceReference(R.string.common_unknown_error)) messageSender.send(message) @@ -194,19 +192,23 @@ internal class CustomTokenSelectorModel @Inject constructor( fun selectCustomDerivationPath(value: SelectedDerivationPath) { when (params) { is NetworkSelector -> return - is DerivationPathSelector -> if (accountsFeatureToggles.isFeatureEnabled) { - params.checkAccountDerivation(value) - } else { - params.onDerivationPathSelected(value, null) - } + is DerivationPathSelector -> params.checkAccountDerivation(value) } } private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) = modelScope.launch { val account = derivationPath.id - ?.let { Blockchain.fromId(it.rawId.value) }?.let(::AccountNodeRecognizer) - ?.let { recognizer -> derivationPath.value.value?.let { recognizer.recognize(it) } } + ?.let { Blockchain.fromId(it.rawId.value) } + ?.let(::AccountNodeRecognizer) + ?.let { recognizer -> + val derivationPathValue = derivationPath.value.value + if (derivationPathValue != null) { + recognizer.recognize(derivationPathValue) + } else { + null + } + } ?.let { accountNode -> fun AccountStatus.CryptoPortfolio.sameNodeAndNotMain() = !this.account.isMainAccount && this.account.derivationIndex.value.toLong() == accountNode 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 7c5611d12e..960347fc65 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 @@ -18,12 +18,12 @@ 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.domain.models.account.AccountId import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent import com.tangem.features.managetokens.component.ManageTokensComponent import com.tangem.features.managetokens.component.ManageTokensMode import com.tangem.features.managetokens.entity.item.CurrencyItemUM -import com.tangem.features.managetokens.entity.managetokens.ManageTokensBottomSheetConfig import com.tangem.features.managetokens.entity.managetokens.ManageTokensTopBarUM import com.tangem.features.managetokens.entity.managetokens.ManageTokensUM import com.tangem.features.managetokens.impl.R @@ -66,7 +66,7 @@ internal class ManageTokensModel @Inject constructor( ) val state: MutableStateFlow = MutableStateFlow(getInitialState()) - val bottomSheetNavigation: SlotNavigation = SlotNavigation() + val bottomSheetNavigation: SlotNavigation = SlotNavigation() init { manageTokensListManager.uiItems @@ -101,15 +101,12 @@ internal class ManageTokensModel @Inject constructor( analyticsEventHandler.send(ManageTokensAnalyticEvent.ScreenOpened(params.source)) return when (params.mode) { - is ManageTokensMode.Wallet, - is ManageTokensMode.Account, - -> createManageContentModel() + is ManageTokensMode.Account -> createManageContentModel() ManageTokensMode.None -> createReadContentModel() } } 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, @@ -196,9 +193,7 @@ internal class ManageTokensModel @Inject constructor( state.update { it.copySealed(topBar = manageContentTopBar()) } } } - ManageTokensMode.None, - is ManageTokensMode.Wallet, - -> Unit // use init state + ManageTokensMode.None -> Unit // use init state } } @@ -299,12 +294,12 @@ internal class ManageTokensModel @Inject constructor( .flatten() .toSet() .associate { network -> network.backendId to network.derivationPath.value } - val needToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) + val isNeedToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) state.update { state -> state.copySealed( hasChanges = currenciesToAdd.isNotEmpty() || currenciesToRemove.isNotEmpty(), - needToInteractWithColdWallet = needToInteractWithColdWallet, + needToInteractWithColdWallet = isNeedToInteractWithColdWallet, ) } } @@ -324,12 +319,8 @@ internal class ManageTokensModel @Inject constructor( private fun navigateToAddCustomToken() { analyticsEventHandler.send(CustomTokenAnalyticsEvent.ButtonCustomToken(params.source)) when (val portfolio = params.mode) { - is ManageTokensMode.Wallet -> - bottomSheetNavigation - .activate(ManageTokensBottomSheetConfig.AddWalletCustomToken(portfolio.userWalletId)) is ManageTokensMode.Account -> - bottomSheetNavigation - .activate(ManageTokensBottomSheetConfig.AddAccountCustomToken(portfolio.accountId)) + bottomSheetNavigation.activate(portfolio.accountId) ManageTokensMode.None -> Unit } } @@ -347,8 +338,8 @@ internal class ManageTokensModel @Inject constructor( useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, - ).getOrElse { - Timber.e(it, "Failed to save changes") + ).getOrElse { throwable -> + Timber.e(throwable, "Failed to save changes") return@resource } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index e2aaa97cca..02a234043f 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -13,8 +13,6 @@ 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.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.models.account.AccountId import com.tangem.domain.redux.OnboardingManageTokensAction import com.tangem.domain.redux.ReduxStateHolder import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent @@ -45,18 +43,13 @@ internal class OnboardingManageTokensModel @Inject constructor( private val messageSender: UiMessageSender, private val reduxStateHolder: ReduxStateHolder, private val analyticsEventHandler: AnalyticsEventHandler, - accountsFeatureToggles: AccountsFeatureToggles, manageTokensListManagerFactory: ManageTokensListManager.Factory, manageTokensUseCasesFacadeFactory: ManageTokensUseCasesFacade.Factory, paramsContainer: ParamsContainer, ) : Model() { private val params: OnboardingManageTokensComponent.Params = paramsContainer.require() - private val portfolio = if (accountsFeatureToggles.isFeatureEnabled) { - ManageTokensMode.Account(accountId = AccountId.forMainCryptoPortfolio(params.userWalletId)) - } else { - ManageTokensMode.Wallet(params.userWalletId) - } + private val portfolio = ManageTokensMode.Account(params.userWalletId) private val useCasesFacade: ManageTokensUseCasesFacade = manageTokensUseCasesFacadeFactory .create(mode = portfolio) private val manageTokensListManager = manageTokensListManagerFactory.create( @@ -223,12 +216,12 @@ internal class OnboardingManageTokensModel @Inject constructor( .flatten() .toSet() .associate { network -> network.backendId to network.derivationPath.value } - val showTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) + val shouldShowTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) state.update { state -> state.copy( actionButtonConfig = OnboardingManageTokensUM.ActionButtonConfig.Continue( onClick = ::saveChanges, - showTangemIcon = showTangemIcon, + showTangemIcon = shouldShowTangemIcon, ), ) } @@ -267,8 +260,8 @@ internal class OnboardingManageTokensModel @Inject constructor( useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, - ).getOrElse { - Timber.e(it, "Failed to save changes") + ).getOrElse { throwable -> + Timber.e(throwable, "Failed to save changes") return@resource } @@ -292,8 +285,8 @@ internal class OnboardingManageTokensModel @Inject constructor( useCasesFacade.saveManagedTokensUseCase( currenciesToAdd = manageTokensListManager.currenciesToAdd.value, currenciesToRemove = manageTokensListManager.currenciesToRemove.value, - ).getOrElse { - Timber.e(it, "Failed to save changes") + ).getOrElse { throwable -> + Timber.e(throwable, "Failed to save changes") return@resource } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt index 4699454540..9665917bbb 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/AddCustomTokenBottomSheet.kt @@ -69,7 +69,7 @@ private fun Preview_AddCustomTokenBottomSheet( } private class AddCustomTokenComponentPreviewProvider : PreviewParameterProvider { - private val mode: AddCustomTokenMode get() = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")) + private val mode: AddCustomTokenMode get() = AddCustomTokenMode(UserWalletId(stringValue = "321")) override val values: Sequence get() = sequenceOf( PreviewAddCustomTokenComponent(), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt index 29cb995a99..4922a66af7 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/ui/CustomTokenSelectorContent.kt @@ -269,7 +269,7 @@ private class CustomTokenNetworkSelectorComponentPreviewProvider : PreviewParameterProvider { private val derivationPath = Network.DerivationPath.Card("m/44'/0'/0'/0/0") - private val mode: AddCustomTokenMode get() = AddCustomTokenMode.Wallet(UserWalletId(stringValue = "321")) + private val mode: AddCustomTokenMode get() = AddCustomTokenMode(UserWalletId(stringValue = "321")) override val values: Sequence get() = sequenceOf( 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 2b20edf084..82579b41d9 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 @@ -443,7 +443,7 @@ private class PreviewManageTokensComponentProvider : PreviewParameterProvider { @@ -45,35 +38,18 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( } } - suspend fun derivePublicKeysUseCase(currencies: List): Either { - return if (accountsFeatureToggles.isFeatureEnabled) { - Unit.right() - } else { - derivePublicKeysUseCase.invoke(userWalletId = userWalletId, currencies = currencies) - } - } - suspend fun checkIsCurrencyNotAddedUseCase( networkId: Network.ID, derivationPath: Network.DerivationPath, contractAddress: String?, - ): Either = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = userWalletId, - networkId = networkId, - derivationPath = derivationPath, - contractAddress = contractAddress, - ) - .fold(ifEmpty = { true }, ifSome = { false }) - .right() - } else { - checkIsCurrencyNotAddedUseCase.invoke( - userWalletId = userWalletId, - networkId = networkId, - derivationPath = derivationPath, - contractAddress = contractAddress, - ) - } + ): Either = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = userWalletId, + networkId = networkId, + derivationPath = derivationPath, + contractAddress = contractAddress, + ) + .fold(ifEmpty = { true }, ifSome = { false }) + .right() private suspend fun Raise.getAccountId(currency: CryptoCurrency): AccountId { val accountList = singleAccountListSupplier.getSyncOrNull( diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt index d0221350ce..6265318696 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensListManager.kt @@ -89,12 +89,9 @@ internal class ManageTokensListManager @AssistedInject constructor( * @param isCollapsed set initial display state of networks. !!! WARNING !!! Use `false` flag with cation */ suspend fun launchPagination(isCollapsed: Boolean) = coroutineScope { - val loadUserTokensFromRemote = when (mode) { - is ManageTokensMode.Wallet, - is ManageTokensMode.Account, - -> source == ManageTokensSource.ONBOARDING - ManageTokensMode.None, - -> false + val shouldLoadTokensFromRemote = when (mode) { + is ManageTokensMode.Account -> source == ManageTokensSource.ONBOARDING + ManageTokensMode.None -> false } val batchFlow = useCasesFacade.getManagedTokensUseCase( context = ManageTokensListBatchingContext( @@ -102,7 +99,7 @@ internal class ManageTokensListManager @AssistedInject constructor( coroutineScope = this, ), // only for onboarding case, change carefully and check repository implementation - loadUserTokensFromRemote = loadUserTokensFromRemote, + loadUserTokensFromRemote = shouldLoadTokensFromRemote, ) batchFlow.state @@ -185,9 +182,7 @@ internal class ManageTokensListManager @AssistedInject constructor( } val canEditItems = when (state.mode) { - is ManageTokensMode.Account, - is ManageTokensMode.Wallet, - -> true + is ManageTokensMode.Account -> true ManageTokensMode.None -> false } state.copy( @@ -210,7 +205,7 @@ internal class ManageTokensListManager @AssistedInject constructor( override fun addCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { changedCurrenciesManager.addCurrency(currency, network) - sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = true) + sendSelectCurrencyAction(batchKey = batchKey, currencyId = currency.id, network = network, isSelected = true) sendSelectCurrencyAnalyticsEvent(currency, isSelected = true) } @@ -218,7 +213,7 @@ internal class ManageTokensListManager @AssistedInject constructor( override fun removeCurrency(batchKey: Int, currency: ManagedCryptoCurrency.Token, network: Network) { changedCurrenciesManager.removeCurrency(currency, network) - sendSelectCurrencyAction(batchKey, currency.id, network, isSelected = false) + sendSelectCurrencyAction(batchKey = batchKey, currencyId = currency.id, network = network, isSelected = false) sendSelectCurrencyAnalyticsEvent(currency, isSelected = false) } @@ -270,9 +265,9 @@ internal class ManageTokensListManager @AssistedInject constructor( network = network, tempAddedTokens = changedCurrenciesManager.currenciesToAdd.value, tempRemovedTokens = changedCurrenciesManager.currenciesToRemove.value, - ).getOrElse { + ).getOrElse { throwable -> Timber.e( - it, + throwable, """ Failed to check linked tokens |- Mode: $mode @@ -281,7 +276,7 @@ internal class ManageTokensListManager @AssistedInject constructor( ) val message = SnackbarMessage( - message = it.localizedMessage + message = throwable.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) @@ -296,9 +291,9 @@ internal class ManageTokensListManager @AssistedInject constructor( ): CurrencyUnsupportedState? { return useCasesFacade.checkCurrencyUnsupportedUseCase( sourceNetwork = sourceNetwork, - ).getOrElse { + ).getOrElse { throwable -> Timber.e( - it, + throwable, """ Failed to check currency unsupported state |- Mode: $mode @@ -307,7 +302,7 @@ internal class ManageTokensListManager @AssistedInject constructor( ) val message = SnackbarMessage( - message = it.localizedMessage + message = throwable.localizedMessage ?.let(::stringReference) ?: resourceReference(R.string.common_error), ) @@ -334,7 +329,12 @@ internal class ManageTokensListManager @AssistedInject constructor( toRemove = currenciesToRemove.value, ), onSelectCurrencyNetwork = { networkId, isSelected -> - selectNetwork(currencyBatch.key, currency, networkId, isSelected) + selectNetwork( + batchKey = currencyBatch.key, + currency = currency, + source = networkId, + isSelected = isSelected, + ) }, onLongTap = ::copyContractAddress, ) diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt index e291de38c9..63cab69a36 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensUseCasesFacade.kt @@ -3,12 +3,10 @@ package com.tangem.features.managetokens.utils.list import arrow.core.Either import arrow.core.left import arrow.core.right -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.producer.SingleAccountProducer import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase -import com.tangem.domain.managetokens.CheckHasLinkedTokensUseCase import com.tangem.domain.managetokens.GetDistinctManagedCurrenciesUseCase import com.tangem.domain.managetokens.GetManagedTokensUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState @@ -29,12 +27,10 @@ import dagger.assisted.AssistedInject internal class ManageTokensUseCasesFacade @AssistedInject constructor( val getManagedTokensUseCase: GetManagedTokensUseCase, val getDistinctManagedTokensUseCase: GetDistinctManagedCurrenciesUseCase, - private val checkHasLinkedTokensUseCase: CheckHasLinkedTokensUseCase, private val checkCurrencyUnsupportedUseCase: CheckCurrencyUnsupportedUseCase, private val coldWalletAndHasMissedDerivationsUseCase: ColdWalletAndHasMissedDerivationsUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val customTokensRepository: CustomTokensRepository, - private val accountsFeatureToggles: AccountsFeatureToggles, private val singleAccountSupplier: SingleAccountSupplier, @Assisted private val mode: ManageTokensMode, ) { @@ -45,17 +41,10 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( fun manageTokensListConfig(searchText: String?): ManageTokensListConfig { return when (mode) { is ManageTokensMode.Account -> { - ManageTokensListConfig.Account(accountId = mode.accountId, searchText = searchText) - } - is ManageTokensMode.Wallet -> { - ManageTokensListConfig.Wallet(userWalletId = mode.userWalletId, searchText = searchText) + ManageTokensListConfig(accountId = mode.accountId, searchText = searchText) } ManageTokensMode.None -> { - if (accountsFeatureToggles.isFeatureEnabled) { - ManageTokensListConfig.Account(accountId = null, searchText = searchText) - } else { - ManageTokensListConfig.Wallet(userWalletId = null, searchText = searchText) - } + ManageTokensListConfig(accountId = null, searchText = searchText) } } } @@ -70,7 +59,6 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( manageCryptoCurrenciesUseCase(accountId = mode.accountId, remove = currency) } - is ManageTokensMode.Wallet -> error("Unsupported") ManageTokensMode.None -> nonePortfolioError.left() } } @@ -90,18 +78,12 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( ) as? Account.CryptoPortfolio ?: return IllegalStateException("Account not found").left() - (account.cryptoCurrencies + added - removed).any { - it is CryptoCurrency.Token && it.network.backendId == network.backendId && - it.network.derivationPath == network.derivationPath + (account.cryptoCurrencies + added - removed).any { currency -> + currency is CryptoCurrency.Token && currency.network.backendId == network.backendId && + currency.network.derivationPath == network.derivationPath } .right() } - is ManageTokensMode.Wallet -> checkHasLinkedTokensUseCase.invoke( - userWalletId = mode.userWalletId, - network = network, - tempAddedTokens = tempAddedTokens, - tempRemovedTokens = tempRemovedTokens, - ) ManageTokensMode.None -> nonePortfolioError.left() } } @@ -114,10 +96,6 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( userWalletId = mode.accountId.userWalletId, sourceNetwork = sourceNetwork, ) - is ManageTokensMode.Wallet -> checkCurrencyUnsupportedUseCase.invoke( - userWalletId = mode.userWalletId, - sourceNetwork = sourceNetwork, - ) ManageTokensMode.None -> nonePortfolioError.left() } } @@ -127,10 +105,6 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( userWalletId = mode.accountId.userWalletId, networksWithDerivationPath = network, ) - is ManageTokensMode.Wallet -> coldWalletAndHasMissedDerivationsUseCase.invoke( - userWalletId = mode.userWalletId, - networksWithDerivationPath = network, - ) ManageTokensMode.None -> false } @@ -145,7 +119,6 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( remove = currenciesToRemove.mapToCryptoCurrencies(userWalletId = mode.accountId.userWalletId), ) } - is ManageTokensMode.Wallet -> error("Unsupported") ManageTokensMode.None -> nonePortfolioError.left() } diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt index cbc53ded2c..146f16ebbc 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/utils/list/ManageTokensWarningDelegate.kt @@ -29,9 +29,7 @@ internal class ManageTokensWarningDelegate @AssistedInject constructor( ) { val isNonePortfolio = when (mode) { ManageTokensMode.None -> true - is ManageTokensMode.Account, - is ManageTokensMode.Wallet, - -> false + is ManageTokensMode.Account -> false } val hasLinkedTokens = if (isNonePortfolio || !isCoin) { false diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 796d045499..f174c69a07 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -24,12 +24,10 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.bottomSheetMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase -import com.tangem.domain.models.PortfolioId import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.scan.CardDTO import com.tangem.domain.models.scan.ScanResponse @@ -87,7 +85,6 @@ internal class WalletSettingsModel @Inject constructor( private val permissionsRepository: PermissionRepository, private val notificationsRepository: NotificationsRepository, private val unlockHotWalletContextualUseCase: UnlockHotWalletContextualUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val singleAccountListSupplier: SingleAccountListSupplier, private val accountListSortingSaver: AccountListSortingSaver, @@ -159,8 +156,7 @@ internal class WalletSettingsModel @Inject constructor( accountList = accountList, ), accountReorderUM = AccountReorderUM( - isDragEnabled = accountsFeatureToggles.isFeatureEnabled && - accountList.count { it is WalletSettingsAccountsUM.Account } > 1, + isDragEnabled = accountList.count { it is WalletSettingsAccountsUM.Account } > 1, onMove = ::onAccountReorder, onDragStopped = ::onAccountDragStopped, ), @@ -237,13 +233,7 @@ internal class WalletSettingsModel @Inject constructor( router.push( AppRoute.ManageTokens( source = Source.SETTINGS, - portfolioId = if (accountsFeatureToggles.isFeatureEnabled) { - PortfolioId( - accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId), - ) - } else { - PortfolioId(userWalletId = userWallet.walletId) - }, + accountId = AccountId.forMainCryptoPortfolio(userWalletId = userWallet.walletId), ), ) }, @@ -378,7 +368,7 @@ internal class WalletSettingsModel @Inject constructor( if (!state.value.isWalletBackedUp) { showMakeBackupAtFirstAlertBS() } else { - unlockWalletIfNeedAndProceed { authorizationRequired -> + unlockWalletIfNeedAndProceed { _ -> router.push( route = AppRoute.UpdateAccessCode( userWalletId = params.userWalletId, 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 625fe4fa31..ab2b53ed60 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 @@ -6,7 +6,6 @@ import com.tangem.common.routing.AppRoute 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.account.AccountId import com.tangem.domain.models.currency.CryptoCurrency @@ -55,7 +54,7 @@ internal class DefaultWalletRouter @Inject constructor( override fun openManageTokensScreen(accountId: AccountId) { val route = AppRoute.ManageTokens( source = AppRoute.ManageTokens.Source.ACCOUNT, - portfolioId = PortfolioId(accountId), + accountId = accountId, ) router.push(route) } From 7d461390e073ae82f2e32a35c317960d0f777e40 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 10:39:48 +0400 Subject: [PATCH 84/97] Updated on 2026-08-14 --- .../tangem/tap/di/domain/NFTDomainModule.kt | 5 - .../data/wallets/DefaultWalletsRepository.kt | 88 ++-- .../data/wallets/di/WalletsDataModule.kt | 8 - .../wallets/DefaultWalletsRepositoryTest.kt | 440 ++++++++---------- .../domain/nft/GetNFTCollectionsUseCase.kt | 47 +- features/nft/impl/detekt-baseline-debug.xml | 11 - .../collections/model/NFTCollectionsModel.kt | 64 +-- .../nft/common/DefaultNFTComponent.kt | 31 +- .../staking/impl/detekt-baseline-debug.xml | 16 - .../impl/presentation/model/StakingModel.kt | 126 +++-- .../impl/detekt-baseline-debug.xml | 15 - .../DefaultTokenDetailsDeepLinkHandler.kt | 29 +- .../model/ExpressTransactionsModel.kt | 24 +- .../tokendetails/model/TokenDetailsModel.kt | 81 ++-- .../impl/detekt-baseline-debug.xml | 5 - .../impl/DefaultWalletSettingsComponent.kt | 8 +- .../utils/AccountItemsDelegate.kt | 4 +- .../implementors/MultiWalletContentLoader.kt | 2 +- ...criberV2.kt => WalletNFTListSubscriber.kt} | 6 +- 19 files changed, 387 insertions(+), 623 deletions(-) delete mode 100644 features/wallet-settings/impl/detekt-baseline-debug.xml rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/{WalletNFTListSubscriberV2.kt => WalletNFTListSubscriber.kt} (94%) diff --git a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt index cf1a7707c6..4cbe877c01 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/NFTDomainModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.di.domain -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.nft.* @@ -25,15 +24,11 @@ internal object NFTDomainModule { @Provides @Singleton fun providesGetNFTCollectionsUseCase( - currenciesRepository: CurrenciesRepository, nftRepository: NFTRepository, singleAccountListSupplier: SingleAccountListSupplier, - accountsFeatureToggles: AccountsFeatureToggles, ): GetNFTCollectionsUseCase = GetNFTCollectionsUseCase( - currenciesRepository = currenciesRepository, nftRepository = nftRepository, singleAccountListSupplier = singleAccountListSupplier, - accountsFeatureToggles = accountsFeatureToggles, ) @Provides diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 83af408c47..8d90aedc75 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -5,17 +5,14 @@ import arrow.core.left import arrow.core.right import com.tangem.data.common.wallet.WalletServerBinder import com.tangem.data.wallets.converters.UserWalletRemoteInfoConverter -import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.common.response.fold import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.common.response.isNetworkError import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.converters.WalletIdBodyConverter import com.tangem.datasource.api.tangemTech.models.* import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Status -import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys @@ -24,7 +21,6 @@ import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.getSyncOrNull import com.tangem.datasource.local.preferences.utils.store -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.common.wallets.getSyncStrict @@ -52,10 +48,7 @@ internal class DefaultWalletsRepository( private val userWalletsListRepository: UserWalletsListRepository, private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, - private val authProvider: AuthProvider, private val walletServerBinder: WalletServerBinder, - private val appsFlyerStore: AppsFlyerStore, - private val accountsFeatureToggles: AccountsFeatureToggles, private val moshi: com.squareup.moshi.Moshi, ) : WalletsRepository { @@ -368,59 +361,36 @@ internal class DefaultWalletsRepository( override suspend fun associateWallets(applicationId: String, wallets: List) = withContext(dispatchers.io) { - if (accountsFeatureToggles.isFeatureEnabled) { - val associateApplicationIdWithWallets: suspend () -> ApiResponse = { - tangemTechApi.associateApplicationIdWithWalletsV2( - applicationId = applicationId, - body = AssociateApplicationIdWithWalletsBody( - walletIds = wallets.map { it.walletId.stringValue }.distinct(), - ), - ) - } - - val apiResponse = associateApplicationIdWithWallets() - - if (apiResponse is ApiResponse.Success) return@withContext - - if (apiResponse is ApiResponse.Error && - apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST) - ) { - val errorBody = (apiResponse.cause as? HttpException)?.errorBody - ?: error("Bad Request must have error body") - - val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java) - val errorResponse = adapter.fromJson(errorBody) - ?: error("Cannot parse error body: $errorBody") - - errorResponse.missingWalletIds - .map { - async { createWallet(userWalletId = UserWalletId(it)) } - } - .awaitAll() - - associateApplicationIdWithWallets().getOrThrow() - } - } else { - val conversionData = appsFlyerStore.get() - val publicKeys = authProvider.getCardsPublicKeys() - val walletsBody = wallets.map { userWallet -> - WalletIdBodyConverter.convert( - userWallet = userWallet, - conversionData = conversionData, - publicKeys = if (userWallet is UserWallet.Cold) { - publicKeys.filterKeys { - userWallet.cardsInWallet.contains(it) - } - } else { - emptyMap() - }, - ) - } - - tangemTechApi.associateApplicationIdWithWallets( + val associateApplicationIdWithWallets: suspend () -> ApiResponse = { + tangemTechApi.associateApplicationIdWithWalletsV2( applicationId = applicationId, - body = walletsBody, - ).getOrThrow() + body = AssociateApplicationIdWithWalletsBody( + walletIds = wallets.map { it.walletId.stringValue }.distinct(), + ), + ) + } + + val apiResponse = associateApplicationIdWithWallets() + + if (apiResponse is ApiResponse.Success) return@withContext + + if (apiResponse is ApiResponse.Error && + apiResponse.cause.isNetworkError(HttpException.Code.BAD_REQUEST) + ) { + val errorBody = (apiResponse.cause as? HttpException)?.errorBody + ?: error("Bad Request must have error body") + + val adapter = moshi.adapter(AssociateAppWithWalletsErrorResponse::class.java) + val errorResponse = adapter.fromJson(errorBody) + ?: error("Cannot parse error body: $errorBody") + + errorResponse.missingWalletIds + .map { + async { createWallet(userWalletId = UserWalletId(it)) } + } + .awaitAll() + + associateApplicationIdWithWallets().getOrThrow() } } diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt index c580d60ae1..c7561aab8b 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/di/WalletsDataModule.kt @@ -9,13 +9,11 @@ import com.tangem.data.wallets.cold.DefaultColdMapDerivationsRepository import com.tangem.data.wallets.derivations.DefaultDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotMapDerivationsRepository import com.tangem.data.wallets.hot.DefaultHotWalletAccessCodeAttemptsRepository -import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.wallets.derivations.ColdMapDerivationsRepository import com.tangem.domain.wallets.derivations.DerivationsRepository @@ -43,10 +41,7 @@ internal object WalletsDataModule { tangemTechApi: TangemTechApi, userWalletsListRepository: UserWalletsListRepository, dispatchers: CoroutineDispatcherProvider, - authProvider: AuthProvider, walletServerBinder: WalletServerBinder, - appsFlyerStore: AppsFlyerStore, - accountsFeatureToggles: AccountsFeatureToggles, @NetworkMoshi moshi: Moshi, ): WalletsRepository { return DefaultWalletsRepository( @@ -55,10 +50,7 @@ internal object WalletsDataModule { userWalletsListRepository = userWalletsListRepository, seedPhraseNotificationVisibilityStore = RuntimeStateStore(defaultValue = emptyMap()), dispatchers = dispatchers, - authProvider = authProvider, walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, - accountsFeatureToggles = accountsFeatureToggles, moshi = moshi, ) } diff --git a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt index 71182a19c0..74d60d237c 100644 --- a/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt +++ b/data/wallets/src/test/java/com/tangem/data/wallets/DefaultWalletsRepositoryTest.kt @@ -5,149 +5,149 @@ import androidx.datastore.preferences.core.Preferences import com.google.common.truth.Truth.assertThat import com.squareup.moshi.Moshi import com.tangem.data.common.wallet.WalletServerBinder -import com.tangem.datasource.api.common.AuthProvider import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError.HttpException import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.PromocodeActivationBody import com.tangem.datasource.api.tangemTech.models.PromocodeActivationResponse import com.tangem.datasource.api.tangemTech.models.WalletResponse -import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError -import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +/** + * Tests for [DefaultWalletsRepository] + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultWalletsRepositoryTest { - private lateinit var repository: DefaultWalletsRepository - private val preferencesDataStore = mockk>(relaxed = true) + + private val preferencesDataStore: DataStore = mockk(relaxed = true) + private val tangemTechApi: TangemTechApi = mockk() + private val walletServerBinder: WalletServerBinder = mockk() + private val appPreferenceStore = AppPreferencesStore( moshi = Moshi.Builder().build(), dispatchers = TestingCoroutineDispatcherProvider(), preferencesDataStore = preferencesDataStore, ) - private lateinit var tangemTechApi: TangemTechApi - private lateinit var dispatchers: CoroutineDispatcherProvider - private lateinit var walletServerBinder: WalletServerBinder - private lateinit var appsFlyerStore: AppsFlyerStore + + private val repository = DefaultWalletsRepository( + appPreferencesStore = appPreferenceStore, + tangemTechApi = tangemTechApi, + userWalletsListRepository = mockk(), + seedPhraseNotificationVisibilityStore = mockk(), + dispatchers = TestingCoroutineDispatcherProvider(), + walletServerBinder = walletServerBinder, + moshi = mockk(), + ) private val testWalletId = UserWalletId("1234567890abcdef") - @Before - fun setup() { - tangemTechApi = mockk() - walletServerBinder = mockk() - appsFlyerStore = mockk() - dispatchers = TestingCoroutineDispatcherProvider() - repository = DefaultWalletsRepository( - appPreferencesStore = appPreferenceStore, - tangemTechApi = tangemTechApi, - userWalletsListRepository = mockk(), - seedPhraseNotificationVisibilityStore = mockk(), - dispatchers = dispatchers, - authProvider = mockk(), - walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, - accountsFeatureToggles = mockk(), - moshi = mockk(), - ) + @AfterEach + fun tearDown() { + clearMocks(tangemTechApi, preferencesDataStore) } - @Test - fun `GIVEN local storage has value WHEN isNotificationsEnabled THEN should return local value`() = runTest { - // GIVEN - val expectedPreferences = """{"${testWalletId.stringValue}":true}""" - val preferences = mockk() - coEvery { preferencesDataStore.data } returns flowOf(preferences) - coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns expectedPreferences + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class IsNotificationsEnabled { - // WHEN - val result = repository.isNotificationsEnabled(testWalletId) + @Test + fun `should return local value when local storage has value`() = runTest { + // Arrange + val expectedPreferences = """{"${testWalletId.stringValue}":true}""" + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns expectedPreferences - // THEN - assertThat(result).isTrue() + // Act + val result = repository.isNotificationsEnabled(testWalletId) + + // Assert + assertThat(result).isTrue() + } + + @Test + fun `should return false when local storage is empty`() = runTest { + // Arrange + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + + // Act + val result = repository.isNotificationsEnabled(testWalletId) + + // Assert + assertThat(result).isFalse() + } } - @Test - fun `GIVEN local storage is empty WHEN isNotificationsEnabled THEN should return false`() = runTest { - // GIVEN - val preferences = mockk() - coEvery { preferencesDataStore.data } returns flowOf(preferences) - coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class SetNotificationsEnabled { - // WHEN - val result = repository.isNotificationsEnabled(testWalletId) + @Test + fun `should update local storage when enabled status`() = runTest { + // Arrange + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + coEvery { preferencesDataStore.updateData(any()) } returns mockk() - // THEN - assertThat(result).isFalse() + // Act + repository.setNotificationsEnabled(testWalletId, isEnabled = true) + + // Assert + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } + + @Test + fun `should update local storage when disabled status`() = runTest { + // Arrange + val preferences = mockk() + coEvery { preferencesDataStore.data } returns flowOf(preferences) + coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" + coEvery { preferencesDataStore.updateData(any()) } returns mockk() + + // Act + repository.setNotificationsEnabled(testWalletId, isEnabled = false) + + // Assert + coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } + } } - @Test - fun `GIVEN enabled status WHEN setNotificationsEnabled THEN should update local storage`() = runTest { - // GIVEN - val preferences = mockk() - coEvery { preferencesDataStore.data } returns flowOf(preferences) - coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" - coEvery { preferencesDataStore.updateData(any()) } returns mockk() + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class GetWalletsInfo { - // WHEN - repository.setNotificationsEnabled(testWalletId, isEnabled = true) - - // THEN - coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } - } - - @Test - fun `GIVEN disabled status WHEN setNotificationsEnabled THEN should update local storage`() = runTest { - // GIVEN - val preferences = mockk() - coEvery { preferencesDataStore.data } returns flowOf(preferences) - coEvery { preferences[PreferencesKeys.NOTIFICATIONS_ENABLED_STATES_KEY] } returns "{}" - coEvery { preferencesDataStore.updateData(any()) } returns mockk() - - // WHEN - repository.setNotificationsEnabled(testWalletId, isEnabled = false) - - // THEN - coVerify(exactly = 1) { preferencesDataStore.updateData(any()) } - } - - @Test - fun `GIVEN API returns wallets WHEN getWalletsInfo THEN should return converted wallets and update cache if requested`() = - runTest { - // GIVEN + @Test + fun `should return converted wallets and update cache when updateCache is true`() = runTest { + // Arrange val applicationId = "test_app_id" val wallet1Id = "1234567890abcdef" val wallet2Id = "fedcba0987654321" val walletResponses = listOf( - WalletResponse( - id = wallet1Id, - notifyStatus = true, - ), - WalletResponse( - id = wallet2Id, - notifyStatus = false, - ), + WalletResponse(id = wallet1Id, notifyStatus = true), + WalletResponse(id = wallet2Id, notifyStatus = false), ) coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses) coEvery { preferencesDataStore.updateData(any()) } returns mockk() - // WHEN + // Act val result = repository.getWalletsInfo(applicationId, updateCache = true) - // THEN + // Assert assertThat(result).hasSize(2) assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id) assertThat(result[0].isNotificationsEnabled).isTrue() @@ -158,24 +158,20 @@ class DefaultWalletsRepositoryTest { coVerify(exactly = 2) { preferencesDataStore.updateData(any()) } } - @Test - fun `GIVEN API returns wallets WHEN getWalletsInfo with updateCache false THEN should return converted wallets without updating cache`() = - runTest { - // GIVEN + @Test + fun `should return converted wallets without updating cache when updateCache is false`() = runTest { + // Arrange val applicationId = "test_app_id" val wallet1Id = "1234567890abcdef" val walletResponses = listOf( - WalletResponse( - id = wallet1Id, - notifyStatus = true, - ), + WalletResponse(id = wallet1Id, notifyStatus = true), ) coEvery { tangemTechApi.getWallets(applicationId) } returns ApiResponse.Success(walletResponses) - // WHEN + // Act val result = repository.getWalletsInfo(applicationId, updateCache = false) - // THEN + // Assert assertThat(result).hasSize(1) assertThat(result[0].walletId.stringValue).isEqualTo(wallet1Id) assertThat(result[0].isNotificationsEnabled).isTrue() @@ -183,152 +179,126 @@ class DefaultWalletsRepositoryTest { coVerify(exactly = 1) { tangemTechApi.getWallets(applicationId) } coVerify(exactly = 0) { preferencesDataStore.updateData(any()) } } + } - @Test - fun `GIVEN user wallets and application ID WHEN associateWallets THEN should convert and send to API`() = runTest { - // GIVEN - val applicationId = "test_app_id" - val wallet1Id = "1234567890abcdef" - val wallet2Id = "fedcba0987654321" - val card1PublicKey = "card1_public_key" - val card2PublicKey = "card2_public_key" + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class AssociateWallets { - val userWallets = listOf( - mockk { - every { cardsInWallet } returns setOf(card1PublicKey) - every { walletId } returns UserWalletId(wallet1Id) - every { name } returns "Wallet 1" - }, - mockk { - every { cardsInWallet } returns setOf(card2PublicKey) - every { walletId } returns UserWalletId(wallet2Id) - every { name } returns "Wallet 2" - }, - ) + @Test + fun `should convert and send to API V2`() = runTest { + // Arrange + val applicationId = "test_app_id" + val wallet1Id = "1234567890abcdef" + val wallet2Id = "fedcba0987654321" - val publicKeys = mapOf( - card1PublicKey to "public_key_1", - card2PublicKey to "public_key_2", - ) - - val authProvider = mockk { - coEvery { getCardsPublicKeys() } returns publicKeys - } - - val accountsFeatureToggles = mockk { - every { isFeatureEnabled } returns false - } - - repository = DefaultWalletsRepository( - appPreferencesStore = appPreferenceStore, - tangemTechApi = tangemTechApi, - userWalletsListRepository = mockk(), - seedPhraseNotificationVisibilityStore = mockk(), - dispatchers = dispatchers, - authProvider = authProvider, - walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, - accountsFeatureToggles = accountsFeatureToggles, - moshi = mockk(), - ) - - coEvery { appsFlyerStore.get() } returns null - - coEvery { - tangemTechApi.associateApplicationIdWithWallets( - eq(applicationId), - any(), - ) - } returns ApiResponse.Success(Unit) - - // WHEN - repository.associateWallets(applicationId, userWallets) - - // THEN - coVerify(exactly = 1) { - tangemTechApi.associateApplicationIdWithWallets( - applicationId = eq(applicationId), - body = match { body -> - body.size == 2 && - body.any { - it.walletId == wallet1Id && - it.cards!!.any { card -> card.cardPublicKey == "public_key_1" } && - it.name == "Wallet 1" - } && - body.any { - it.walletId == wallet2Id && - it.cards!!.any { card -> card.cardPublicKey == "public_key_2" } && - it.name == "Wallet 2" - } + val userWallets = listOf( + mockk { + every { walletId } returns UserWalletId(wallet1Id) + }, + mockk { + every { walletId } returns UserWalletId(wallet2Id) }, ) + + coEvery { + tangemTechApi.associateApplicationIdWithWalletsV2(eq(applicationId), any()) + } returns ApiResponse.Success(Unit) + + // Act + repository.associateWallets(applicationId, userWallets) + + // Assert + coVerify(exactly = 1) { + tangemTechApi.associateApplicationIdWithWalletsV2( + applicationId = eq(applicationId), + body = match { body -> + body.walletIds.size == 2 && + body.walletIds.contains(wallet1Id) && + body.walletIds.contains(wallet2Id) + }, + ) + } } } - @Test - fun `GIVEN valid data WHEN activatePromoCode THEN returns Right with status and calls API`() = runTest { - // GIVEN - val walletId = UserWalletId("1234567890abcdef") - val promoCode = "PROMO123" - val address = "bc1qexampleaddress" - coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success( - PromocodeActivationResponse(status = "activated"), - ) + @Nested + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + inner class ActivatePromoCode { - // WHEN - val result = repository.activatePromoCode( - userWalletId = walletId, - promoCode = promoCode, - bitcoinAddress = address - ) - - // THEN - var right: String? = null - var left: ActivatePromoCodeError? = null - result.fold({ left = it }, { right = it }) - assertThat(left).isNull() - assertThat(right).isEqualTo("activated") - - coVerify(exactly = 1) { - tangemTechApi.activatePromoCode( - match { it is PromocodeActivationBody && it.promoCode == promoCode && it.address == address }, + @Test + fun `should return Right with status when API returns success`() = runTest { + // Arrange + val walletId = UserWalletId("1234567890abcdef") + val promoCode = "PROMO123" + val address = "bc1qexampleaddress" + coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Success( + PromocodeActivationResponse(status = "activated"), ) - } - } - @Test - fun `GIVEN NOT_FOUND error WHEN activatePromoCode THEN returns Left InvalidPromoCode`() = runTest { - // GIVEN - val walletId = UserWalletId("1234567890abcdef") - coEvery { tangemTechApi.activatePromoCode(any()) } returns - ApiResponse.Error( + // Act + val result = repository.activatePromoCode( + userWalletId = walletId, + promoCode = promoCode, + bitcoinAddress = address, + ) + + // Assert + var right: String? = null + var left: ActivatePromoCodeError? = null + result.fold({ left = it }, { right = it }) + assertThat(left).isNull() + assertThat(right).isEqualTo("activated") + + coVerify(exactly = 1) { + tangemTechApi.activatePromoCode( + match { it.promoCode == promoCode && it.address == address }, + ) + } + } + + @Test + fun `should return Left InvalidPromoCode when API returns NOT_FOUND`() = runTest { + // Arrange + val walletId = UserWalletId("1234567890abcdef") + @Suppress("UNCHECKED_CAST") + coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Error( HttpException(code = HttpException.Code.NOT_FOUND, message = null, errorBody = null), ) as ApiResponse - // WHEN - val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr") + // Act + val result = repository.activatePromoCode( + userWalletId = walletId, + promoCode = "PROMO", + bitcoinAddress = "addr", + ) - // THEN - var error: ActivatePromoCodeError? = null - result.fold({ error = it }, { }) - assertThat(error).isEqualTo(ActivatePromoCodeError.InvalidPromoCode) - } + // Assert + var error: ActivatePromoCodeError? = null + result.fold({ error = it }, { }) + assertThat(error).isEqualTo(ActivatePromoCodeError.InvalidPromoCode) + } - @Test - fun `GIVEN CONFLICT error WHEN activatePromoCode THEN returns Left PromocodeAlreadyUsed`() = runTest { - // GIVEN - val walletId = UserWalletId("1234567890abcdef") - coEvery { tangemTechApi.activatePromoCode(any()) } returns - ApiResponse.Error( + @Test + fun `should return Left PromocodeAlreadyUsed when API returns CONFLICT`() = runTest { + // Arrange + val walletId = UserWalletId("1234567890abcdef") + @Suppress("UNCHECKED_CAST") + coEvery { tangemTechApi.activatePromoCode(any()) } returns ApiResponse.Error( HttpException(code = HttpException.Code.CONFLICT, message = null, errorBody = null), ) as ApiResponse - // WHEN - val result = repository.activatePromoCode(userWalletId = walletId, promoCode = "PROMO", bitcoinAddress = "addr") + // Act + val result = repository.activatePromoCode( + userWalletId = walletId, + promoCode = "PROMO", + bitcoinAddress = "addr", + ) - // THEN - var error: ActivatePromoCodeError? = null - result.fold({ error = it }, { }) - assertThat(error).isEqualTo(ActivatePromoCodeError.PromocodeAlreadyUsed) + // Assert + var error: ActivatePromoCodeError? = null + result.fold({ error = it }, { }) + assertThat(error).isEqualTo(ActivatePromoCodeError.PromocodeAlreadyUsed) + } } } \ No newline at end of file diff --git a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt index 95b2fd53cb..f8432faa75 100644 --- a/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt +++ b/domain/nft/src/main/kotlin/com/tangem/domain/nft/GetNFTCollectionsUseCase.kt @@ -1,6 +1,5 @@ package com.tangem.domain.nft -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency @@ -8,52 +7,42 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.nft.models.NFTCollections import com.tangem.domain.nft.models.WalletNFTCollections import com.tangem.domain.nft.repository.NFTRepository -import com.tangem.domain.tokens.repository.CurrenciesRepository import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* class GetNFTCollectionsUseCase( - private val currenciesRepository: CurrenciesRepository, private val nftRepository: NFTRepository, private val singleAccountListSupplier: SingleAccountListSupplier, - private val accountsFeatureToggles: AccountsFeatureToggles, ) { - @Deprecated("Use invokeForAccounts instead") @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): Flow> = - if (accountsFeatureToggles.isFeatureEnabled) { - invokeForAccounts(userWalletId).map { it.flattenCollections } - } else { - currenciesRepository - .getWalletCurrenciesUpdates(userWalletId) - .flatMapLatest { - nftCollections(userWalletId, it) - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - fun invokeForAccounts(userWalletId: UserWalletId): Flow { - fun Account.flowOfNFTCollections(): Flow>>? { - val currencies = (this as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty() - if (currencies.isEmpty()) return null - return nftCollections(userWalletId = userWalletId, cryptoCurrencies = currencies.toList()) - .map { nfts -> this to nfts } - } - + operator fun invoke(userWalletId: UserWalletId): Flow { return singleAccountListSupplier(userWalletId) - .mapLatest { statusList -> statusList.accounts.mapNotNull { it.flowOfNFTCollections() } } - .flatMapLatest { flows -> combine(flows) { WalletNFTCollections(it.toMap()) } } + .mapLatest { statusList -> statusList.accounts.mapNotNull(::flowOfNFTCollections) } + .flatMapLatest { flows -> + combine(flows) { WalletNFTCollections(it.toMap()) } + } } - private fun nftCollections( + private fun flowOfNFTCollections(account: Account): Flow>>? { + val currencies = (account as? Account.CryptoPortfolio)?.cryptoCurrencies.orEmpty() + + if (currencies.isEmpty()) return null + + return getNftCollections(userWalletId = account.userWalletId, cryptoCurrencies = currencies.toList()) + .map { nfts -> account to nfts } + } + + private fun getNftCollections( userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow> { val networks = cryptoCurrencies - .map { cryptoCurrency -> cryptoCurrency.network } + .map(CryptoCurrency::network) .distinct() + if (networks.isEmpty()) return flowOf(emptyList()) + return nftRepository.observeCollections(userWalletId, networks) } } \ No newline at end of file diff --git a/features/nft/impl/detekt-baseline-debug.xml b/features/nft/impl/detekt-baseline-debug.xml index 97fee15b2b..cadeff498d 100644 --- a/features/nft/impl/detekt-baseline-debug.xml +++ b/features/nft/impl/detekt-baseline-debug.xml @@ -5,34 +5,23 @@ BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM$val showAllTraitsButton: Boolean BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM.BlockItem$val showInfoButton: Boolean BooleanPropertyNaming:NFTAssetUM.kt$NFTAssetUM.Rarity.Content$val showDivider: Boolean - BooleanPropertyNaming:NFTCollectionsModel.kt$NFTCollectionsModel$val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } - BooleanPropertyNaming:NFTCollectionsModel.kt$NFTCollectionsModel$val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true BooleanPropertyNaming:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$val custom = derivationPath is Network.DerivationPath.Custom MultilineLambdaItParameter:ChangeCollectionExpandedStateTransformer.kt$ChangeCollectionExpandedStateTransformer${ val collectionId = collection.collectionIdProvider() if (it.id == collectionId && it is NFTCollectionUM) { if (!it.isExpanded) { onFirstExpanded() } it.copy(isExpanded = !it.isExpanded) } else { it } } MultilineLambdaItParameter:NFTCollectionsContent.kt${ key(it.id) { NFTCollectionWarning( modifier = Modifier .padding(top = TangemTheme.dimens.spacing16), state = it, ) } } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ ChangeCollectionExpandedStateTransformer( collection = collection, collectionIdProvider = collectionIdProvider, onFirstExpanded = { onFirstExpanded(collection) }, ).transform(it) } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ UpdateDataStateTransformer( nftCollections = listOf(), isAccountMode = isAccountMode, walletNFTCollections = nftCollections.copy(collections = filteredNFTs), onReceiveClick = { params.onReceiveClick() }, onRetryClick = ::onRefresh, onExpandCollectionClick = ::onExpandCollectionClick, onRetryAssetsClick = ::onRetryAssetsClick, onAssetClick = { asset, collection -> params.onAssetClick(asset, collection) }, initialSearchBarFactory = ::getInitialSearchBar, collectionIdProvider = collectionIdProvider, ).transform(it) } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ UpdateDataStateTransformer( nftCollections = nftCollections.filter(query), onReceiveClick = { params.onReceiveClick() }, onRetryClick = ::onRefresh, onExpandCollectionClick = ::onExpandCollectionClick, onRetryAssetsClick = ::onRetryAssetsClick, onAssetClick = { asset, collection -> params.onAssetClick(asset, collection) }, initialSearchBarFactory = ::getInitialSearchBar, collectionIdProvider = collectionIdProvider, ).transform(it) } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ it.copy( content = when (val content = it.content) { is NFTCollections.Content.Collections -> content.copy( collections = content.collections.orEmpty().filter { val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery }, ) is NFTCollections.Content.Error -> it.content }, ) } - MultilineLambdaItParameter:NFTCollectionsModel.kt$NFTCollectionsModel${ val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery } MultilineLambdaItParameter:NFTDetailsUMFactory.kt$NFTDetailsUMFactory${ NFTAssetUM.BlockItem( title = stringReference(it.name), value = it.value, showInfoButton = false, ) } MultilineLambdaItParameter:NFTDetailsUMFactory.kt$NFTDetailsUMFactory${ NFTAssetUM.Media.Content( url = it, ) } MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ ShowReceiveBottomSheetTransformer( network = network, networkAddress = value.address, onDismissBottomSheet = ::onReceiveBottomSheetDismiss, onCopyClick = { text -> onCopyClick(text, network) }, onShareClick = { text -> onShareClick(text, network) }, ).transform(it) } MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ UpdateDataStateTransformer( networks = filteredNetworks, onNetworkClick = ::onNetworkClick, ).transform(it) } MultilineLambdaItParameter:NFTReceiveModel.kt$NFTReceiveModel${ it.copy( bottomSheetConfig = it.bottomSheetConfig?.copy(isShown = false), ) } MultilineLambdaItParameter:UpdateDataStateTransformer.kt$UpdateDataStateTransformer${ NFTCollectionUM( id = it.collectionIdProvider(), networkIconId = getActiveIconRes(it.network.rawId), name = it.name.orEmpty(), description = TextReference.PluralRes( R.plurals.nft_collections_count, it.count, wrappedList(it.count), ), logoUrl = it.logoUrl, assets = it.transformAssets(), onExpandClick = { onExpandCollectionClick(it) }, isExpanded = it.isExpanded(state), ) } - NoNameShadowing:NFTCollectionsModel.kt$NFTCollectionsModel${ val assetsFulfillQuery = if (query.isEmpty()) { true } else { when (val assets = it.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, -> false is NFTCollection.Assets.Value -> { assets.items.any { asset -> asset.name?.lowercase()?.contains(query.lowercase()) == true } } } } val collectionFulfillQuery = query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true collectionFulfillQuery || assetsFulfillQuery } NullableBooleanCheck:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$(state.content as? NFTCollectionsUM.Content) ?.collections ?.filterIsInstance<NFTCollectionUM>() ?.firstOrNull { it.id == this.collectionIdProvider() } ?.isExpanded ?: false NullableToStringCall:NFTCollectionsContent.kt$${item2?.id} - NullableToStringCall:NFTCollectionsModel.kt$NFTCollectionsModel$${network.derivationPath.value} PropertyUsedBeforeDeclaration:NFTDetailsModel.kt$NFTDetailsModel$_state ReusedModifierInstance:NFTCollectionsContent.kt$Box( modifier = modifier .fillMaxSize() .padding(bottom = bottomPadding), ) { Text( modifier = Modifier .align(Alignment.Center), text = stringResourceSafe(id = R.string.nft_empty_search), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, textAlign = TextAlign.Center, ) } ReusedModifierInstance:NFTCollectionsLoading.kt$Card( modifier = modifier .fillMaxWidth() .padding( top = TangemTheme.dimens.spacing16, ), shape = RoundedCornerShape(TangemTheme.dimens.radius16), colors = CardDefaults.cardColors( containerColor = TangemTheme.colors.background.primary, contentColor = TangemTheme.colors.text.primary1, disabledContainerColor = TangemTheme.colors.background.primary, disabledContentColor = TangemTheme.colors.text.primary1, ), ) { Column { repeat(SHIMMER_ITEMS_COUNT) { CollectionPlaceholder() } } } ReusedModifierInstance:NFTDetailsAsset.kt$Column( modifier = modifier .verticalScroll(scrollState) .padding( start = TangemTheme.dimens.spacing16, top = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing16, bottom = bottomPadding, ) .fillMaxSize(), ) { NFTDetailsLogo( state = state.media, modifier = Modifier .aspectRatio(1f), ) NFTDetailsInfoGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), state = state.topInfo, onReadMoreClick = onReadMoreClick, ) NFTDetailsBlocksGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), items = state.traits, title = resourceReference(R.string.nft_details_traits), action = if (state.showAllTraitsButton) { { NFTBlocksGroupAction( text = resourceReference(R.string.common_see_all), startIcon = { }, onClick = onSeeAllTraitsClick, ) } } else { null }, ) NFTDetailsBlocksGroup( modifier = Modifier .padding(top = TangemTheme.dimens.spacing12), items = state.baseInfoItems, title = resourceReference(R.string.nft_details_base_information), action = { NFTBlocksGroupAction( text = resourceReference(R.string.common_explore), startIcon = { NFTBlocksGroupActionIcon(iconRes = R.drawable.ic_compass_24) }, onClick = onExploreClick, ) }, ) } ReusedModifierInstance:NFTDetailsInfoGroup.kt$Column( modifier = modifier .padding( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), ) { TextShimmer( modifier = Modifier .width(TangemTheme.dimens.size158), style = TangemTheme.typography.head, textSizeHeight = true, ) TextShimmer( modifier = Modifier .padding(top = TangemTheme.dimens.spacing4) .width(TangemTheme.dimens.size90), style = TangemTheme.typography.caption2, textSizeHeight = true, ) } ReusedModifierInstance:NFTDetailsInfoGroup.kt$Column( modifier = modifier .padding( start = TangemTheme.dimens.spacing12, end = TangemTheme.dimens.spacing12, ), verticalArrangement = Arrangement.SpaceAround, ) { Text( text = state.cryptoPrice.resolveReference(), style = TangemTheme.typography.head, color = TangemTheme.colors.text.primary1, ) Text( modifier = Modifier .padding(top = TangemTheme.dimens.spacing4) .flicker(state.isFlickering), text = state.fiatPrice.resolveReference(), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, ) } - UnsafeCallOnNullableType:DefaultNFTComponent.kt$DefaultNFTComponent$portfolioFetcher!! - UseEmptyCounterpart:NFTCollectionsModel.kt$NFTCollectionsModel$listOf() UseEmptyCounterpart:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$listOf() UseOrEmpty:UpdateDataStateTransformer.kt$UpdateDataStateTransformer$walletNFTCollections.collections.values.firstOrNull() ?: listOf() diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt index 20f1fc3414..2f6f77b6c7 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/collections/model/NFTCollectionsModel.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.components.fields.entity.SearchBarUM import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.nft.FetchNFTCollectionAssetsUseCase import com.tangem.domain.nft.GetNFTCollectionsUseCase @@ -33,7 +32,6 @@ internal class NFTCollectionsModel @Inject constructor( private val getNFTCollectionsUseCase: GetNFTCollectionsUseCase, private val fetchNFTCollectionAssetsUseCase: FetchNFTCollectionAssetsUseCase, private val refreshAllNFTUseCase: RefreshAllNFTUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, paramsContainer: ParamsContainer, ) : Model() { @@ -66,51 +64,21 @@ internal class NFTCollectionsModel @Inject constructor( } init { - if (accountsFeatureToggles.isFeatureEnabled) { - subscribeToNFTCollectionsNew() - } else { - subscribeToNFTCollections() - } + subscribeToNFTCollections() } private fun subscribeToNFTCollections() { combine( flow = getNFTCollectionsUseCase(params.userWalletId), flow2 = searchManager.query.distinctUntilChanged(), - ) { nftCollections, query -> - _state.update { - UpdateDataStateTransformer( - nftCollections = nftCollections.filter(query), - onReceiveClick = { - params.onReceiveClick() - }, - onRetryClick = ::onRefresh, - onExpandCollectionClick = ::onExpandCollectionClick, - onRetryAssetsClick = ::onRetryAssetsClick, - onAssetClick = { asset, collection -> - params.onAssetClick(asset, collection) - }, - initialSearchBarFactory = ::getInitialSearchBar, - collectionIdProvider = collectionIdProvider, - ).transform(it) - } - } - .onStart { onRefresh() } - .launchIn(modelScope) - } - - private fun subscribeToNFTCollectionsNew() { - combine( - flow = getNFTCollectionsUseCase.invokeForAccounts(params.userWalletId), - flow2 = searchManager.query.distinctUntilChanged(), flow3 = isAccountsModeEnabledUseCase(), ) { nftCollections, query, isAccountMode -> val filteredNFTs = nftCollections.collections .mapValues { (_, nfts) -> nfts.filter(query) } - _state.update { + _state.update { stateUM -> UpdateDataStateTransformer( - nftCollections = listOf(), + nftCollections = emptyList(), isAccountMode = isAccountMode, walletNFTCollections = nftCollections.copy(collections = filteredNFTs), onReceiveClick = { @@ -124,22 +92,22 @@ internal class NFTCollectionsModel @Inject constructor( }, initialSearchBarFactory = ::getInitialSearchBar, collectionIdProvider = collectionIdProvider, - ).transform(it) + ).transform(stateUM) } } .onStart { onRefresh() } .launchIn(modelScope) } - private fun List.filter(query: String): List = map { - it.copy( - content = when (val content = it.content) { + private fun List.filter(query: String): List = map { collections -> + collections.copy( + content = when (val content = collections.content) { is NFTCollections.Content.Collections -> content.copy( - collections = content.collections.orEmpty().filter { - val assetsFulfillQuery = if (query.isEmpty()) { + collections = content.collections.orEmpty().filter { collection: NFTCollection -> + val isAssetsFulfillQuery = if (query.isEmpty()) { true } else { - when (val assets = it.assets) { + when (val assets = collection.assets) { is NFTCollection.Assets.Empty, is NFTCollection.Assets.Failed, is NFTCollection.Assets.Loading, @@ -152,13 +120,13 @@ internal class NFTCollectionsModel @Inject constructor( } } - val collectionFulfillQuery = - query.isEmpty() || it.name?.lowercase()?.contains(query.lowercase()) == true + val isCollectionFulfillQuery = + query.isEmpty() || collection.name?.lowercase()?.contains(query.lowercase()) == true - collectionFulfillQuery || assetsFulfillQuery + isCollectionFulfillQuery || isAssetsFulfillQuery }, ) - is NFTCollections.Content.Error -> it.content + is NFTCollections.Content.Error -> collections.content }, ) } @@ -198,12 +166,12 @@ internal class NFTCollectionsModel @Inject constructor( } private fun onExpandCollectionClick(collection: NFTCollection) { - _state.update { + _state.update { stateUM -> ChangeCollectionExpandedStateTransformer( collection = collection, collectionIdProvider = collectionIdProvider, onFirstExpanded = { onFirstExpanded(collection) }, - ).transform(it) + ).transform(stateUM) } } diff --git a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt index 6c9581913e..0cbb637c1c 100644 --- a/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt +++ b/features/nft/impl/src/main/kotlin/com/tangem/features/nft/common/DefaultNFTComponent.kt @@ -20,7 +20,6 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.PortfolioId import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorComponent @@ -53,7 +52,6 @@ internal class DefaultNFTComponent @AssistedInject constructor( private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, private val portfolioSelectorController: PortfolioSelectorController, portfolioFetcherFactory: PortfolioFetcher.Factory, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : NFTComponent, AppComponentContext by appComponentContext { private val stackNavigation = StackNavigation() @@ -66,14 +64,10 @@ internal class DefaultNFTComponent @AssistedInject constructor( private val initialRoute: NFTRoute = NFTRoute.Collections(params.userWalletId) private val currentRoute = MutableStateFlow(initialRoute) private val onReceiveClickJob = JobHolder() - private val portfolioFetcher: PortfolioFetcher? = if (accountsFeatureToggles.isFeatureEnabled) { - portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), - scope = componentScope, - ) - } else { - null - } + private val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), + scope = componentScope, + ) private val bottomSheetNavigation: SlotNavigation = SlotNavigation() private val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback { override val onDismiss: () -> Unit = { bottomSheetNavigation.dismiss() } @@ -83,7 +77,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( source = bottomSheetNavigation, serializer = Unit.serializer(), handleBackButton = false, - childFactory = { configuration, context -> bottomSheetChild(context) }, + childFactory = { _, context -> bottomSheetChild(context) }, ) private val childStack = childStack( @@ -146,17 +140,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( params = NFTCollectionsComponent.Params( userWalletId = route.userWalletId, onBackClick = ::onChildBack, - onReceiveClick = { - if (accountsFeatureToggles.isFeatureEnabled) { - onReceiveClick(route) - } else { - innerRouter.push( - NFTRoute.Receive( - portfolioId = PortfolioId(route.userWalletId), - ), - ) - } - }, + onReceiveClick = { onReceiveClick(route) }, onAssetClick = { asset, collection -> innerRouter.push( NFTRoute.Details( @@ -170,7 +154,6 @@ internal class DefaultNFTComponent @AssistedInject constructor( ) private fun onReceiveClick(route: NFTRoute.Collections) = componentScope.launch { - val portfolioFetcher = requireNotNull(portfolioFetcher) portfolioSelectorController.selectAccount(null) portfolioFetcher.updateMode(mode = PortfolioFetcher.Mode.Wallet(route.userWalletId)) val portfolioData = portfolioFetcher.data.first() @@ -245,7 +228,7 @@ internal class DefaultNFTComponent @AssistedInject constructor( portfolioSelectorComponentFactory.create( context = childByContext(componentContext), params = PortfolioSelectorComponent.Params( - portfolioFetcher = portfolioFetcher!!, + portfolioFetcher = portfolioFetcher, controller = portfolioSelectorController, bsCallback = portfolioSelectorCallback, ), diff --git a/features/staking/impl/detekt-baseline-debug.xml b/features/staking/impl/detekt-baseline-debug.xml index 113f53de5d..8cd2dca55b 100644 --- a/features/staking/impl/detekt-baseline-debug.xml +++ b/features/staking/impl/detekt-baseline-debug.xml @@ -4,7 +4,6 @@ BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount > balance BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean - BooleanPropertyNaming:StakingModel.kt$StakingModel$val noBalanceState = balanceState == null BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean CastNullableToNonNullableType:SetApprovalBottomSheetInProgressTransformer.kt$SetApprovalBottomSheetInProgressTransformer$as @@ -14,28 +13,13 @@ MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Error) { Text( text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) } } MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size90, ), ) } } MultilineLambdaItParameter:StakingInfoNotificationsFactory.kt$StakingInfoNotificationsFactory${ it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || it.type == BalanceType.LOCKED } - MultilineLambdaItParameter:StakingModel.kt$StakingModel${ EnterAmountBoundary( amount = it, fiatRate = status.value.fiatRate.orZero(), ) } - MultilineLambdaItParameter:StakingModel.kt$StakingModel${ Timber.e(it) false } - MultilineLambdaItParameter:StakingModel.kt$StakingModel${ isBalanceHiddenFlow.value = it.isBalanceHidden stateController.update( transformer = HideBalanceStateTransformer( isBalanceHidden = it.isBalanceHidden, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrency = appCurrency, ), ) } - MultilineLambdaItParameter:StakingModel.kt$StakingModel${ stateController.update( SetFeeToTonInitializeBottomSheetTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, fee = it.normal, isFeeApproximate = false, ), ) } MultilineLambdaItParameter:StakingStateController.kt$StakingStateController${ it.copy( showColdWalletInteractionIcon = userWallet is UserWallet.Cold, ) } - NullCheckOnMutableProperty:StakingModel.kt$StakingModel$if (feeCryptoCurrencyStatus != null && fee != null) { getBalanceNotEnoughForFeeWarningUseCase( fee = fee, userWalletId = userWalletId, tokenStatus = cryptoCurrencyStatus, coinStatus = feeCryptoCurrencyStatus ?: cryptoCurrencyStatus, ).getOrNull() } else { null } NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId PropertyUsedBeforeDeclaration:StakingFeeBlock.kt$FeeBlockPreviewProvider$contentState - PropertyUsedBeforeDeclaration:StakingModel.kt$StakingModel$isAmountSubtractAvailable PropertyUsedBeforeDeclaration:StakingStateController.kt$StakingStateController$uiState - SuspendFunSwallowedCancellation:StakingModel.kt$StakingModel$runCatching UnnecessaryEventHandlerParameter:StakingInitialInfoContent.kt$onClick: (BalanceState) -> Unit - UnnecessaryLet:StakingModel.kt$StakingModel$let(::add) UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) } UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) } - UnsafeCallOnNullableType:StakingModel.kt$StakingModel$tonAccountInitializeTransaction!! - VarCouldBeVal:StakingModel.kt$StakingModel$private var actionsJobHolder: JobHolder = JobHolder() - VarCouldBeVal:StakingModel.kt$StakingModel$private var approvalJobHolder: JobHolder = JobHolder() - VarCouldBeVal:StakingModel.kt$StakingModel$private var feeJobHolder: JobHolder = JobHolder() - VarCouldBeVal:StakingModel.kt$StakingModel$private var sendTransactionJobHolder = JobHolder() - VarCouldBeVal:StakingModel.kt$StakingModel$private var stakingStateRouter: StakingStateRouter = StakingStateRouter( appRouter = appRouter, stateController = stateController, analyticsEventsHandler = analyticsEventHandler, ) - VarCouldBeVal:StakingModel.kt$StakingModel$private var stepChangesJobHolder = JobHolder() diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt index 2ef0911b14..9a5c3044f1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingModel.kt @@ -7,7 +7,6 @@ import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.getValidatorsCount import com.tangem.common.routing.AppRouter -import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary @@ -28,7 +27,6 @@ import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.DialogMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -113,7 +111,6 @@ internal class StakingModel @Inject constructor( private val stateController: StakingStateController, override val dispatchers: CoroutineDispatcherProvider, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getMinimumTransactionAmountSyncUseCase: GetMinimumTransactionAmountSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -149,7 +146,6 @@ internal class StakingModel @Inject constructor( @DelayedWork private val coroutineScope: CoroutineScope, private val innerRouter: InnerStakingRouter, private val messageSender: UiMessageSender, - private val accountsFeatureToggles: AccountsFeatureToggles, appRouter: AppRouter, ) : Model(), StakingClickIntents { @@ -158,7 +154,7 @@ internal class StakingModel @Inject constructor( private val params = paramsContainer.require() - private var stakingStateRouter: StakingStateRouter = StakingStateRouter( + private val stakingStateRouter: StakingStateRouter = StakingStateRouter( appRouter = appRouter, stateController = stateController, analyticsEventsHandler = analyticsEventHandler, @@ -237,6 +233,7 @@ internal class StakingModel @Inject constructor( ) } + @Suppress("PropertyUsedBeforeDeclaration") private val transactionSender: StakingTransactionSender by lazy(LazyThreadSafetyMode.NONE) { stakingOperationsFactory.createTransactionSender( cryptoCurrencyStatus = cryptoCurrencyStatus, @@ -298,17 +295,17 @@ internal class StakingModel @Inject constructor( override fun onNextClick(balanceState: BalanceState?) { modelScope.launch { val isInitialInfoStep = value.currentStep == StakingStep.InitialInfo - val noBalanceState = balanceState == null + val isBalanceAbsent = balanceState == null val hasNoYieldBalanceData = cryptoCurrencyStatus.value.stakingBalance !is StakingBalance.Data.StakeKit when { - isInitialInfoStep && noBalanceState && integration.areAllTargetsFull && hasNoYieldBalanceData -> { + isInitialInfoStep && isBalanceAbsent && integration.areAllTargetsFull && hasNoYieldBalanceData -> { stakingEventFactory.createStakingValidatorsUnavailableAlert() return@launch } - isInitialInfoStep && noBalanceState -> { + isInitialInfoStep && isBalanceAbsent -> { val list = buildList { - SetConfirmationStateInitTransformer( + val setConfirmationStateInitTransformer = SetConfirmationStateInitTransformer( isEnter = true, isExplicitExit = false, balanceState = null, @@ -316,13 +313,17 @@ internal class StakingModel @Inject constructor( stakingApproval = stakingApproval, stakingAllowance = stakingAllowance, integration = integration, - ).let(::add) + ) + + add(setConfirmationStateInitTransformer) + if (integration.isPartialAmountDisabled) { - ValidatorSelectChangeTransformer( + val validatorSelectChangeTransformer = ValidatorSelectChangeTransformer( selectedTarget = integration.preferredTargets.firstOrNull(), integration = integration, - ).let(::add) - SetAmountDataTransformer( + ) + + val setAmountDataTransformer = SetAmountDataTransformer( clickIntents = this@StakingModel, cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, userWalletProvider = Provider { userWallet }, @@ -330,15 +331,25 @@ internal class StakingModel @Inject constructor( isBalanceHidden = isBalanceHiddenFlow.value, isAccountsModeEnabled = isAccountsModeEnabled, account = account, - ).let(::add) - AmountMaxValueStateTransformer( + ) + + val amountMaxValueStateTransformer = AmountMaxValueStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, minimumTransactionAmount = minimumTransactionAmount, actionType = uiState.value.actionType, integration = integration, - ).let(::add) + ) + + addAll( + listOf( + validatorSelectChangeTransformer, + setAmountDataTransformer, + amountMaxValueStateTransformer, + ), + ) } } + stateController.updateAll(*list.toTypedArray()) } } @@ -900,7 +911,7 @@ internal class StakingModel @Inject constructor( AmountReduceByStateTransformer( cryptoCurrencyStatus = cryptoCurrencyStatus, minimumTransactionAmount = minimumTransactionAmount, - value = AmountReduceByTransformer.ReduceByData( + value = ReduceByData( reduceAmountBy = reduceAmountBy, reduceAmountByDiff = reduceAmountByDiff, ), @@ -953,7 +964,7 @@ internal class StakingModel @Inject constructor( task = PeriodicTask( delay = ALLOWANCE_UPDATE_DELAY, task = { - runCatching { + runSuspendCatching { getAllowanceUseCase( userWalletId = userWalletId, cryptoCurrency = cryptoCurrencyStatus.currency, @@ -1073,17 +1084,20 @@ internal class StakingModel @Inject constructor( network = cryptoCurrencyStatus.currency.network, memo = null, ) - tonAccountInitializeTransaction = transaction.getOrElse { + + val initialTransaction = transaction.getOrElse { stateController.update( SetFeeErrorToTonInitializeBottomSheetTransformer(), ) return@launch } + tonAccountInitializeTransaction = initialTransaction + val transactionFee = getFeeUseCase( userWallet = userWallet, network = cryptoCurrencyStatus.currency.network, - transactionData = tonAccountInitializeTransaction!!, + transactionData = initialTransaction, ) transactionFee.fold( @@ -1092,12 +1106,12 @@ internal class StakingModel @Inject constructor( SetFeeErrorToTonInitializeBottomSheetTransformer(), ) }, - ifRight = { + ifRight = { fee -> stateController.update( SetFeeToTonInitializeBottomSheetTransformer( appCurrencyProvider = Provider { appCurrency }, feeCryptoCurrencyStatus = feeCryptoCurrencyStatus, - fee = it.normal, + fee = fee.normal, isFeeApproximate = false, ), ) @@ -1190,41 +1204,20 @@ internal class StakingModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( - userWalletId = params.userWalletId, - currency = params.cryptoCurrency, - ).conflate().distinctUntilChanged() - .filter { - value.currentStep == StakingStep.InitialInfo || isTopHeatupCase() - }.onEach { (maybeAccount, maybeStatus) -> - isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() - account = maybeAccount - onDataLoaded(maybeStatus) - }.flowOn(dispatchers.main) - .launchIn(modelScope) - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrencyId, - isSingleWalletWithTokens = false, - ).conflate().distinctUntilChanged() - .filter { - value.currentStep == StakingStep.InitialInfo || isTopHeatupCase() - } - .onEach { maybeStatus -> - maybeStatus.fold( - ifRight = { onDataLoaded(it) }, - ifLeft = { error -> - stakingEventFactory.createGenericErrorAlert(error.toString()) - stateController.update( - SetConfirmationStateResetAssentTransformer(cryptoCurrencyStatus = cryptoCurrencyStatus), - ) - }, - ) - }.flowOn(dispatchers.main) - .launchIn(modelScope) - } + getAccountCurrencyStatusUseCase( + userWalletId = params.userWalletId, + currency = params.cryptoCurrency, + ) + .conflate() + .distinctUntilChanged() + .filter { value.currentStep == StakingStep.InitialInfo || isTopHeatupCase() } + .onEach { (maybeAccount, maybeStatus) -> + isAccountsModeEnabled = isAccountsModeEnabledUseCase.invokeSync() + account = maybeAccount + onDataLoaded(maybeStatus) + } + .flowOn(dispatchers.main) + .launchIn(modelScope) } private suspend fun onDataLoaded(status: CryptoCurrencyStatus) { @@ -1241,12 +1234,11 @@ internal class StakingModel @Inject constructor( ) } - feeCryptoCurrencyStatus = - getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() - minimumTransactionAmount = - getMinimumTransactionAmountSyncUseCase(userWalletId, status).getOrNull()?.let { + feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, status).getOrNull() + minimumTransactionAmount = getMinimumTransactionAmountSyncUseCase(userWalletId, status).getOrNull() + ?.let { amount -> EnterAmountBoundary( - amount = it, + amount = amount, fiatRate = status.value.fiatRate.orZero(), ) } @@ -1265,11 +1257,11 @@ internal class StakingModel @Inject constructor( getBalanceHidingSettingsUseCase() .conflate() .distinctUntilChanged() - .onEach { - isBalanceHiddenFlow.value = it.isBalanceHidden + .onEach { settings -> + isBalanceHiddenFlow.value = settings.isBalanceHidden stateController.update( transformer = HideBalanceStateTransformer( - isBalanceHidden = it.isBalanceHidden, + isBalanceHidden = settings.isBalanceHidden, cryptoCurrencyStatus = cryptoCurrencyStatus, appCurrency = appCurrency, ), @@ -1403,8 +1395,8 @@ internal class StakingModel @Inject constructor( val isAccountInitializedNewValue = checkAccountInitializedUseCase.invoke( userWalletId = userWalletId, network = cryptoCurrencyStatus.currency.network, - ).getOrElse { - Timber.e(it) + ).getOrElse { throwable -> + Timber.e(throwable) false } diff --git a/features/tokendetails/impl/detekt-baseline-debug.xml b/features/tokendetails/impl/detekt-baseline-debug.xml index 71ab124fb2..23e783a751 100644 --- a/features/tokendetails/impl/detekt-baseline-debug.xml +++ b/features/tokendetails/impl/detekt-baseline-debug.xml @@ -13,17 +13,8 @@ BooleanPropertyNaming:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$val showProviderLink = getShowProviderLink(notification, statusModel) BooleanPropertyNaming:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$val showProviderLink = getShowProviderLink(notification, transaction.status) BooleanPropertyNaming:TokenDetailsTopAppBar.kt$var showDropdownMenu by rememberSaveable { mutableStateOf(false) } - MultilineLambdaItParameter:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler${ val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true val isCorrectDerivation = isDefaultDerivation || isCustomDerivation isNetwork && isCurrency && isCorrectDerivation } MultilineLambdaItParameter:ExpressStatusFactory.kt$ExpressStatusFactory${ when (it) { is ExpressTransactionStateUM.OnrampUM -> it.activeStatus.isHidden else -> false } } MultilineLambdaItParameter:OnrampStatusFactory.kt$OnrampStatusFactory${ Timber.e("Couldn't update onramp status. $it") onrampTx } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ Timber.e(it.cause?.localizedMessage.orEmpty()) "" } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsEventsHandler.send( TokenReceiveAnalyticsEvent.ButtonShareAddress(cryptoCurrency.symbol), ) shareManager.shareText(text = it) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsEventsHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(cryptoCurrency.symbol)) clipboardManager.setText(text = it, isSensitive = true) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent( exception = it, params = mapOf( "blockchainId" to cryptoCurrency.network.id.rawId.value, "networkId" to cryptoCurrency.network.backendId, ), ), ) Timber.e( /* t = */ it, /* message = */ "Unable to get wallet manager for user wallet %s and network %s", /* ...args = */ userWalletId, cryptoCurrency.network, ) false } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(it)) Timber.e(it) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ internalUiState.value = stateFactory.getStateWithUpdatedHidden( isBalanceHidden = it.isBalanceHidden, ) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ sendButtonsEvents(it.states) internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) } - MultilineLambdaItParameter:TokenDetailsModel.kt$TokenDetailsModel${ val updatedState = stateFactory.getStateWithNotifications(it) notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) internalUiState.value = updatedState } MultilineLambdaItParameter:TokenDetailsScreen.kt${ Notification( modifier = itemModifier.animateItem(), config = it.config, iconTint = when (it) { is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent is TokenDetailsNotification.UsedOutdatedData -> TangemTheme.colors.text.attention else -> null }, ) } MultilineLambdaItParameter:TokenDetailsTopAppBar.kt${ TangemDropdownItem( item = it, dismissParent = { showDropdownMenu = false }, ) } MultilineLambdaItParameter:TokenStakingBlock.kt${ when (it) { is StakingBlockUM.TemporaryUnavailable -> StakingTemporaryUnavailableBlock() is StakingBlockUM.Loading -> StakingLoading() is StakingBlockUM.Staked -> StakingBalanceBlock( state = it, isBalanceHidden = isBalanceHidden, ) is StakingBlockUM.StakeAvailable -> StakingAvailableContent( state = it, ) } } @@ -31,21 +22,15 @@ NamedArguments:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$createStateInfo( transaction, toCryptoCurrency, fromCryptoCurrency, toFiatAmount, fromFiatAmount, ) NestedScopeFunctions:TokenDetailsBalanceSelectStateConverter.kt$TokenDetailsBalanceSelectStateConverter$let { cryptoCurrencyStatus.value.fiatRate?.multiply(it) } NullableBooleanCheck:TokenDetailsSwapTransactionsStateConverter.kt$TokenDetailsSwapTransactionsStateConverter$transaction.status?.hasLongTime ?: false - NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$networkId - NullableToStringCall:DefaultTokenDetailsDeepLinkHandler.kt$DefaultTokenDetailsDeepLinkHandler$$tokenId NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingCryptoAmount NullableToStringCall:TokenDetailsStakingInfoConverter.kt$TokenDetailsStakingInfoConverter$$stakingEntryInfo PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$network PropertyUsedBeforeDeclaration:ExpressStatusBottomSheetStateProvider.kt$ExpressStatusBottomSheetStateProvider$token - PropertyUsedBeforeDeclaration:TokenDetailsModel.kt$TokenDetailsModel$uiState SuspendFunSwallowedCancellation:ExchangeStatusFactory.kt$ExchangeStatusFactory$runCatching - SuspendFunSwallowedCancellation:TokenDetailsModel.kt$TokenDetailsModel$runCatching - UnnecessaryLet:TokenDetailsModel.kt$TokenDetailsModel$let { internalUiState.value = stateFactory.getStateWithErrorDialog(message) } UnnecessaryLet:TokenDetailsSkeletonStateConverter.kt$TokenDetailsSkeletonStateConverter$let(::add) UnnecessaryLet:TokenDetailsStateFactory.kt$TokenDetailsStateFactory$let(::add) UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent$mapOf() UseEmptyCounterpart:TokenDetailsAnalyticsEvent.kt$TokenDetailsAnalyticsEvent.Notice$mapOf() UseOrEmpty:ExchangeStatusFactory.kt$ExchangeStatusFactory$savedTransactions ?.flatMap { setOf(it.fromCryptoCurrency.id, it.toCryptoCurrency.id) } ?.toSet() ?.getQuotesOrEmpty() ?: emptySet() - VarCouldBeVal:TokenDetailsModel.kt$TokenDetailsModel$private var expressTxStatusTaskScheduler = SingleTaskScheduler<PersistentList<ExpressTransactionStateUM>>() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index 4992503c8a..64c0be0735 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -9,7 +9,6 @@ import com.tangem.common.routing.deeplink.DeeplinkConst.TRANSACTION_ID_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.TYPE_KEY import com.tangem.common.routing.deeplink.DeeplinkConst.WALLET_ID_KEY import com.tangem.core.analytics.api.AnalyticsEventHandler -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -18,7 +17,10 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.notifications.models.NotificationType -import com.tangem.domain.tokens.* +import com.tangem.domain.tokens.FetchCurrencyStatusUseCase +import com.tangem.domain.tokens.GetCryptoCurrencyUseCase +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer +import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.wallet.WalletBalanceFetcher import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.SelectWalletUseCase @@ -40,7 +42,6 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( @Assisted private val isFromOnNewIntent: Boolean, private val appRouter: AppRouter, private val selectWalletUseCase: SelectWalletUseCase, - private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase, private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val tokenDetailsDeepLinkActionTrigger: TokenDetailsDeepLinkActionTrigger, @@ -48,7 +49,6 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val getUserWalletUseCase: GetUserWalletUseCase, private val walletBalanceFetcher: WalletBalanceFetcher, - private val accountsFeatureToggles: AccountsFeatureToggles, private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, ) : TokenDetailsDeepLinkHandler { @@ -142,12 +142,12 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( if (userWallet.isMultiCurrency) { val derivationPath = queryParams[DERIVATION_PATH_KEY] - getCryptoCurrencies(userWalletId = userWallet.walletId)?.firstOrNull { - val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) - val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true + getCryptoCurrencies(userWalletId = userWallet.walletId)?.firstOrNull { currency -> + val isNetwork = currency.network.backendId.equals(networkId, ignoreCase = true) + val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true - val isDefaultDerivation = it.network.derivationPath is Network.DerivationPath.Card - val isCustomDerivation = derivationPath?.equals(it.network.derivationPath.value) == true + val isDefaultDerivation = currency.network.derivationPath is Network.DerivationPath.Card + val isCustomDerivation = derivationPath?.equals(currency.network.derivationPath.value) == true val isCorrectDerivation = isDefaultDerivation || isCustomDerivation isNetwork && isCurrency && isCorrectDerivation } @@ -156,14 +156,9 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( } private suspend fun getCryptoCurrencies(userWalletId: UserWalletId): List? { - return if (accountsFeatureToggles.isFeatureEnabled) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), - ) - ?.toList() - } else { - getCryptoCurrenciesUseCase(userWalletId = userWalletId).getOrNull() - } + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), + )?.toList() } @AssistedFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt index 5e7a0d7096..bd24cb6cc5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/ExpressTransactionsModel.kt @@ -2,24 +2,20 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model import androidx.compose.runtime.Stable import arrow.core.getOrElse -import arrow.core.right import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.common.ui.expressStatus.state.ExpressTransactionsBlockState import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.account.Account 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.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.ExpressStateFactory @@ -43,10 +39,8 @@ internal class ExpressTransactionsModel @Inject constructor( paramsContainer: ParamsContainer, expressStatusFactory: ExpressStatusFactory.Factory, getUserWalletUseCase: GetUserWalletUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val router: InnerTokenDetailsRouter, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val expressTransactionsEventListener: ExpressTransactionsEventListener, ) : Model(), ExpressTransactionsClickIntents { @@ -163,22 +157,10 @@ internal class ExpressTransactionsModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) - .onEach { account = it.account } - .map { it.status.right() } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - } + getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) + .onEach { account = it.account } .distinctUntilChanged() - .onEach { maybeCurrencyStatus -> - maybeCurrencyStatus.onRight { status -> cryptoCurrencyStatus = status } - } + .onEach { cryptoCurrencyStatus = it.status } .flowOn(dispatchers.main) .launchIn(modelScope) .saveIn(marketPriceJobHolder) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 17c4f1ad60..2f8b9f76ed 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -33,7 +33,6 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.SnackbarMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -105,12 +104,11 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -@Suppress("LongParameterList", "LargeClass", "TooManyFunctions") +@Suppress("LongParameterList", "LargeClass", "TooManyFunctions", "PropertyUsedBeforeDeclaration") @Stable @ModelScoped internal class TokenDetailsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val getExploreUrlUseCase: GetExploreUrlUseCase, @@ -148,7 +146,6 @@ internal class TokenDetailsModel @Inject constructor( private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, private val needShowYieldSupplyDepositedWarningUseCase: NeedShowYieldSupplyDepositedWarningUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getAccountCryptoCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase, @@ -177,7 +174,7 @@ internal class TokenDetailsModel @Inject constructor( private var cryptoCurrencyStatus: CryptoCurrencyStatus? = null private var account: Account.CryptoPortfolio? = null private var isBalanceLoadedEventSent = false - private var expressTxStatusTaskScheduler = SingleTaskScheduler>() + private val expressTxStatusTaskScheduler = SingleTaskScheduler>() /** Transaction id to check for status */ private val waitForFirstExpressStatusEmmit = MutableStateFlow(false) @@ -265,21 +262,13 @@ internal class TokenDetailsModel @Inject constructor( private fun initButtons() { // we need also init buttons before start all loading to avoid buttons blocking modelScope.launch { - val currentCryptoCurrencyStatus = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCryptoCurrencyStatusUseCase.invokeSync( - userWalletId = userWalletId, - currency = cryptoCurrency, - ) - .onSome { account = it.account } - .getOrNull() - ?.status - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( - userWalletId = userWalletId, - cryptoCurrencyId = cryptoCurrency.id, - isSingleWalletWithTokens = false, - ).getOrNull() - } + val currentCryptoCurrencyStatus = getAccountCryptoCurrencyStatusUseCase.invokeSync( + userWalletId = userWalletId, + currency = cryptoCurrency, + ) + .onSome { account = it.account } + .getOrNull() + ?.status currentCryptoCurrencyStatus?.let { status -> cryptoCurrencyStatus = status @@ -296,9 +285,9 @@ internal class TokenDetailsModel @Inject constructor( private fun handleBalanceHiding() { getBalanceHidingSettingsUseCase() - .onEach { + .onEach { settings -> internalUiState.value = stateFactory.getStateWithUpdatedHidden( - isBalanceHidden = it.isBalanceHidden, + isBalanceHidden = settings.isBalanceHidden, ) } .launchIn(modelScope) @@ -311,9 +300,9 @@ internal class TokenDetailsModel @Inject constructor( ) .conflate() .distinctUntilChanged() - .onEach { - sendButtonsEvents(it.states) - internalUiState.value = stateFactory.getManageButtonsState(actions = it.states) + .onEach { state -> + sendButtonsEvents(state.states) + internalUiState.value = stateFactory.getManageButtonsState(actions = state.states) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -345,8 +334,8 @@ internal class TokenDetailsModel @Inject constructor( userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), ) .distinctUntilChanged() - .onEach { - val updatedState = stateFactory.getStateWithNotifications(it) + .onEach { warnings -> + val updatedState = stateFactory.getStateWithNotifications(warnings) notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) internalUiState.value = updatedState } @@ -356,18 +345,9 @@ internal class TokenDetailsModel @Inject constructor( } private fun subscribeOnCurrencyStatusUpdates() { - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) - .onEach { account = it.account } - .map { it.status.right() } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - } + getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) + .onEach { account = it.account } + .map { it.status.right() } .distinctUntilChanged() .onEach { maybeCurrencyStatus -> internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) @@ -402,7 +382,7 @@ internal class TokenDetailsModel @Inject constructor( isDelayFirst = false, delay = EXPRESS_STATUS_UPDATE_DELAY, task = { - runCatching { + runSuspendCatching { expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs) } }, @@ -508,10 +488,10 @@ internal class TokenDetailsModel @Inject constructor( userWalletId = userWalletId, network = cryptoCurrency.network, ) - .mapLeft { + .mapLeft { throwable -> analyticsExceptionHandler.sendException( event = ExceptionAnalyticsEvent( - exception = it, + exception = throwable, params = mapOf( "blockchainId" to cryptoCurrency.network.id.rawId.value, "networkId" to cryptoCurrency.network.backendId, @@ -520,7 +500,7 @@ internal class TokenDetailsModel @Inject constructor( ) Timber.e( - /* t = */ it, + /* t = */ throwable, /* message = */ "Unable to get wallet manager for user wallet %s and network %s", /* ...args = */ userWalletId, cryptoCurrency.network, @@ -674,8 +654,8 @@ internal class TokenDetailsModel @Inject constructor( userWalletId, cryptoCurrency.network, ).fold( - ifLeft = { - Timber.e(it.cause?.localizedMessage.orEmpty()) + ifLeft = { throwable -> + Timber.e(throwable.cause?.localizedMessage.orEmpty()) "" }, ifRight = { it }, @@ -976,9 +956,9 @@ internal class TokenDetailsModel @Inject constructor( } } } - message?.let { - internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(it)) - Timber.e(it) + if (message != null) { + internalUiState.value = stateFactory.getStateWithErrorDialog(stringReference(message)) + Timber.e(message) } }, ifRight = { @@ -1019,7 +999,10 @@ internal class TokenDetailsModel @Inject constructor( is SendTransactionError.UnknownError -> error.ex?.localizedMessage }?.let { stringReference(it) } } - message?.let { internalUiState.value = stateFactory.getStateWithErrorDialog(message) } + + if (message != null) { + internalUiState.value = stateFactory.getStateWithErrorDialog(message) + } }, ifRight = { internalUiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() }, ) diff --git a/features/wallet-settings/impl/detekt-baseline-debug.xml b/features/wallet-settings/impl/detekt-baseline-debug.xml deleted file mode 100644 index ecf2e0cce8..0000000000 --- a/features/wallet-settings/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt index 3b7692fe1f..34f48f98ba 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/component/impl/DefaultWalletSettingsComponent.kt @@ -22,7 +22,6 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.utils.requestPermission import com.tangem.datasource.local.accounts.AccountTokenMigrationStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.feature.walletsettings.component.NetworksAvailableForNotificationsComponent import com.tangem.feature.walletsettings.component.RenameWalletComponent import com.tangem.feature.walletsettings.component.WalletSettingsComponent @@ -49,7 +48,6 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor( private val renameWalletComponentFactory: RenameWalletComponent.Factory, private val networksAvailableForNotificationsComponent: NetworksAvailableForNotificationsComponent.Factory, private val accountTokenMigrationStore: AccountTokenMigrationStore, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : WalletSettingsComponent, AppComponentContext by context { private val model: WalletSettingsModel = getOrCreateModel(params) @@ -74,11 +72,7 @@ internal class DefaultWalletSettingsComponent @AssistedInject constructor( init { lifecycle.subscribe( - onResume = { - if (accountsFeatureToggles.isFeatureEnabled) { - showMigrationAlertIfNeeded() - } - }, + onResume = { showMigrationAlertIfNeeded() }, onPause = { accountMigrationJobHolder.cancel() }, ) } diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt index b6b47ea825..56be825aca 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/utils/AccountItemsDelegate.kt @@ -12,7 +12,6 @@ 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.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier @@ -46,13 +45,12 @@ internal class AccountItemsDelegate @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val accountListSortingSaver: AccountListSortingSaver, - private val accountsFeatureToggles: AccountsFeatureToggles, private val analyticsEventHandler: AnalyticsEventHandler, ) { private val userWalletId = paramsContainer.require().userWalletId - fun isAccountsSupported(wallet: UserWallet) = accountsFeatureToggles.isFeatureEnabled && wallet.isAccountsSupported + fun isAccountsSupported(wallet: UserWallet) = wallet.isAccountsSupported fun loadAccount(wallet: UserWallet): Flow> { if (!isAccountsSupported(wallet)) return flowOf(emptyList()) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt index 8abda2b315..69217500ac 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/loaders/implementors/MultiWalletContentLoader.kt @@ -11,7 +11,7 @@ import dagger.assisted.AssistedInject internal class MultiWalletContentLoader @AssistedInject constructor( @Assisted private val userWallet: UserWallet, private val accountListSubscriberFactory: AccountListSubscriber.Factory, - private val walletNFTListSubscriberFactory: WalletNFTListSubscriberV2.Factory, + private val walletNFTListSubscriberFactory: WalletNFTListSubscriber.Factory, private val checkWalletWithFundsSubscriberFactory: CheckWalletWithFundsSubscriber.Factory, private val multiWalletWarningsSubscriberFactory: MultiWalletWarningsSubscriber.Factory, private val walletNotificationsSubscriberFactory: WalletNotificationsSubscriber.Factory, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriberV2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt similarity index 94% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriberV2.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt index 88ccbc686f..500b3bcd98 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriberV2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/WalletNFTListSubscriber.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.* -internal class WalletNFTListSubscriberV2 @AssistedInject constructor( +internal class WalletNFTListSubscriber @AssistedInject constructor( @Assisted override val userWallet: UserWallet, override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val walletsRepository: WalletsRepository, @@ -35,7 +35,7 @@ internal class WalletNFTListSubscriberV2 @AssistedInject constructor( // if NFT is enabled for this wallet and there are currencies, // then start observing changes from store and apply transformer if need if (nftEnabled && currencies.isNotEmpty()) { - getNFTCollectionsUseCase.invokeForAccounts(userWallet.walletId) + getNFTCollectionsUseCase(userWallet.walletId) .shareIn( scope = coroutineScope, started = SharingStarted.WhileSubscribed(), @@ -61,6 +61,6 @@ internal class WalletNFTListSubscriberV2 @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(userWallet: UserWallet): WalletNFTListSubscriberV2 + fun create(userWallet: UserWallet): WalletNFTListSubscriber } } \ No newline at end of file From ba664f15575ee87b41bacb3945723548f95c466d Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 13:08:53 +0500 Subject: [PATCH 85/97] Updated on 2026-08-14 --- .../tokenActions/TokenActionsComponent.kt | 63 +++++++++++++++++ .../wallet/child/wallet/WalletComponent.kt | 10 +++ .../intents/WalletContentClickIntents.kt | 30 +++----- .../common/WalletPreviewDataLegacy.kt | 16 +---- .../router/DefaultWalletRouter.kt | 10 +++ .../presentation/router/InnerWalletRouter.kt | 5 ++ .../state/model/ActionsBottomSheetConfig.kt | 13 ---- ...ButtonConfig.kt => TokenActionButtonUM.kt} | 8 ++- .../wallet/state/model/WalletDialogConfig.kt | 6 ++ .../MultiWalletCurrencyActionsConverter.kt | 18 +++-- .../presentation/wallet/ui/WalletScreen.kt | 2 - .../ui/components/TokenActionsBottomSheet.kt | 68 ------------------- 12 files changed, 119 insertions(+), 130 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/ActionsBottomSheetConfig.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/{TokenActionButtonConfig.kt => TokenActionButtonUM.kt} (76%) delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt new file mode 100644 index 0000000000..e994cb9f17 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt @@ -0,0 +1,63 @@ +package com.tangem.feature.wallet.child.tokenActions + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.ui.components.SimpleSettingsRow +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent +import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet +import com.tangem.core.ui.components.getDefaultRowColors +import com.tangem.core.ui.components.getWarningRowColors +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +internal class TokenActionsComponent @AssistedInject constructor( + @Assisted appComponentContext: AppComponentContext, + @Assisted private val params: Params, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + TangemBottomSheet( + containerColor = TangemTheme.colors.background.primary, + config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = TangemBottomSheetConfigContent.Empty, + ), + ) { + Column { + params.actions.forEach { action -> + if (action.isEnabled) { + val rowColors = if (action.isWarning) { + getWarningRowColors() + } else { + getDefaultRowColors() + } + SimpleSettingsRow( + title = action.text.resolveReference(), + icon = action.iconResId, + enabled = action.isEnabled, + rowColors = rowColors, + onItemsClick = action.onClick, + ) + } + } + } + } + } + + data class Params( + val actions: List, + val onDismiss: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index 6bcf590744..f7c026a60f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt @@ -21,6 +21,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent import com.tangem.domain.tokens.model.details.TokenAction +import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent import com.tangem.feature.wallet.child.wallet.model.WalletModel import com.tangem.feature.wallet.navigation.WalletRoute import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig @@ -136,6 +137,15 @@ internal class WalletComponent @AssistedInject constructor( ), ) } + is WalletDialogConfig.TokenActionList -> { + TokenActionsComponent( + appComponentContext = childByContext(componentContext), + params = TokenActionsComponent.Params( + actions = dialogConfig.actionList, + onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + ), + ) + } } }, ) 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 9ccb19d1f6..8b1fd04f57 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 @@ -19,7 +19,6 @@ import com.tangem.domain.nft.analytics.NFTAnalyticsEvent import com.tangem.domain.settings.ShouldShowMarketsTooltipUseCase import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -29,7 +28,6 @@ import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAn import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory import com.tangem.feature.wallet.presentation.wallet.domain.unwrap import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAlertUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNFTItemUM @@ -142,8 +140,15 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( getCryptoCurrencyActionsUseCase(userWallet = userWallet, cryptoCurrencyStatus = cryptoCurrencyStatus) .take(count = 1) - .collectLatest { - showActionsBottomSheet(it, userWallet, accountId) + .collectLatest { actionsState -> + router.openTokenActionSheet( + userWallet = userWallet, + tokenActionList = MultiWalletCurrencyActionsConverter( + userWallet = userWallet, + accountId = accountId, + clickIntents = currencyActionsClickIntents, + ).convert(actionsState), + ) } } } @@ -259,23 +264,6 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( analyticsEventHandler.send(event) } - private fun showActionsBottomSheet( - tokenActionsState: TokenActionsState, - userWallet: UserWallet, - accountId: AccountId, - ) { - stateHolder.showBottomSheet( - ActionsBottomSheetConfig( - actions = MultiWalletCurrencyActionsConverter( - userWallet = userWallet, - accountId = accountId, - clickIntents = currencyActionsClickIntents, - ).convert(tokenActionsState), - ), - userWallet.walletId, - ) - } - override fun onTransactionClick(txHash: String) { modelScope.launch(dispatchers.main) { val currency = getSingleCryptoCurrencyStatusUseCase.unwrap( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt index e613117202..626da24cb8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/WalletPreviewDataLegacy.kt @@ -3,9 +3,10 @@ package com.tangem.feature.wallet.presentation.common import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.* +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTopBarConfig import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList @Suppress("LargeClass") internal object WalletPreviewDataLegacy { @@ -54,15 +55,4 @@ internal object WalletPreviewDataLegacy { UserWalletId(stringValue = "24") to walletCardErrorState, ) } - - val actionsBottomSheet = ActionsBottomSheetConfig( - actions = listOf( - TokenActionButtonConfig( - text = TextReference.Str("Send"), - iconResId = R.drawable.ic_share_24, - isWarning = false, - onClick = {}, - ), - ).toImmutableList(), - ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index ab2b53ed60..d4a58b4046 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 @@ -19,7 +19,9 @@ import com.tangem.domain.redux.StateDialog import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.feature.wallet.navigation.WalletRoute +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import javax.inject.Inject @@ -151,4 +153,12 @@ internal class DefaultWalletRouter @Inject constructor( ), ) } + + override fun openTokenActionSheet(userWallet: UserWallet, tokenActionList: ImmutableList) { + dialogNavigation.activate( + configuration = WalletDialogConfig.TokenActionList( + actionList = tokenActionList, + ), + ) + } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 3e9f99185e..54a04de46c 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 @@ -14,7 +14,9 @@ import com.tangem.domain.pay.TangemPayDetailsConfig import com.tangem.domain.tokens.model.details.NavigationAction import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.feature.wallet.navigation.WalletRoute +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletDialogConfig +import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.SharedFlow /** @@ -81,4 +83,7 @@ internal interface InnerWalletRouter { /** Open yield supply entry screen */ fun openYieldSupplyEntryScreen(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, apy: String) + + /** Open token action sheet */ + fun openTokenActionSheet(userWallet: UserWallet, tokenActionList: ImmutableList) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/ActionsBottomSheetConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/ActionsBottomSheetConfig.kt deleted file mode 100644 index 84ec1221bd..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/ActionsBottomSheetConfig.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import kotlinx.collections.immutable.ImmutableList - -/** - * Config for the token actions bottom sheet - * - * @property actions actions - */ -internal data class ActionsBottomSheetConfig( - val actions: ImmutableList, -) : TangemBottomSheetConfigContent \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt similarity index 76% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonConfig.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt index 12c3c648f1..1b678f372d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenActionButtonUM.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.annotation.DrawableRes import com.tangem.core.ui.extensions.TextReference +import kotlinx.serialization.Serializable /** * Action button config @@ -10,12 +11,13 @@ import com.tangem.core.ui.extensions.TextReference * @property iconResId icon resource id * @property onClick lambda be invoked when action component is clicked * @property isWarning if warning row - * @property enabled enabled + * @property isEnabled enabled */ -data class TokenActionButtonConfig( +@Serializable +data class TokenActionButtonUM( val text: TextReference, @DrawableRes val iconResId: Int, val onClick: () -> Unit, val isWarning: Boolean, - val enabled: Boolean = true, + val isEnabled: Boolean = true, ) \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index a482dc420f..e1144d4983 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.TokenReceiveConfig import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.details.TokenAction +import kotlinx.collections.immutable.ImmutableList import kotlinx.serialization.Serializable /** @@ -26,6 +27,11 @@ internal sealed interface WalletDialogConfig { @Serializable data class TokenReceive(val tokenReceiveConfig: TokenReceiveConfig) : WalletDialogConfig + @Serializable + data class TokenActionList( + val actionList: ImmutableList, + ) : WalletDialogConfig + @Serializable data class YieldSupplyWarning( val cryptoCurrency: CryptoCurrency, 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 0e3d985f61..4044cedde1 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 @@ -2,7 +2,6 @@ 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.account.AccountId import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet @@ -10,7 +9,8 @@ import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason import com.tangem.domain.tokens.model.TokenActionsState import com.tangem.feature.wallet.child.wallet.model.intents.WalletCurrencyActionsClickIntents import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig +import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM +import com.tangem.feature.wallet.presentation.wallet.state.utils.isSingleWalletWithToken import com.tangem.utils.converter.Converter import com.tangem.utils.isNullOrZero import kotlinx.collections.immutable.ImmutableList @@ -20,9 +20,9 @@ internal class MultiWalletCurrencyActionsConverter( private val userWallet: UserWallet, private val accountId: AccountId, private val clickIntents: WalletCurrencyActionsClickIntents, -) : Converter> { +) : Converter> { - override fun convert(value: TokenActionsState): ImmutableList { + override fun convert(value: TokenActionsState): ImmutableList { return value.states .filterIfSingleWithToken() .mapNotNull { @@ -32,9 +32,7 @@ internal class MultiWalletCurrencyActionsConverter( } private fun List.filterIfSingleWithToken(): List { - return if (userWallet is UserWallet.Cold && - userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - ) { + return if (userWallet.isSingleWalletWithToken()) { filter { it !is TokenActionsState.ActionState.HideToken } } else { this @@ -45,7 +43,7 @@ internal class MultiWalletCurrencyActionsConverter( private fun mapTokenActionState( actionsState: TokenActionsState.ActionState, cryptoCurrencyStatus: CryptoCurrencyStatus, - ): TokenActionButtonConfig? { + ): TokenActionButtonUM? { if (actionsState is TokenActionsState.ActionState.Send && cryptoCurrencyStatus.value.amount.isNullOrZero()) { return null } @@ -112,12 +110,12 @@ internal class MultiWalletCurrencyActionsConverter( } } - return TokenActionButtonConfig( + return TokenActionButtonUM( text = title, iconResId = icon, onClick = action, isWarning = actionsState is TokenActionsState.ActionState.HideToken, - enabled = actionsState.unavailabilityReason == noneReason, + isEnabled = actionsState.unavailabilityReason == noneReason, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt index 114a71988b..a094c17794 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen.kt @@ -73,7 +73,6 @@ import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreview import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewDataLegacy.walletScreenState import com.tangem.feature.wallet.presentation.wallet.state.model.* import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder -import com.tangem.feature.wallet.presentation.wallet.ui.components.TokenActionsBottomSheet import com.tangem.feature.wallet.presentation.wallet.ui.components.WalletsList import com.tangem.feature.wallet.presentation.wallet.ui.components.common.* import com.tangem.feature.wallet.presentation.wallet.ui.components.multicurrency.nftCollections @@ -716,7 +715,6 @@ internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modi private fun ShowBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) { if (bottomSheetConfig != null) { when (bottomSheetConfig.content) { - is ActionsBottomSheetConfig -> TokenActionsBottomSheet(config = bottomSheetConfig) is ChooseAddressBottomSheetConfig -> ChooseAddressBottomSheet(config = bottomSheetConfig) is ExpressStatusBottomSheetConfig -> ExpressStatusBottomSheet(config = bottomSheetConfig) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt deleted file mode 100644 index 6916bf40c8..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.ui.components - -import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider -import com.tangem.core.ui.components.SimpleSettingsRow -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet -import com.tangem.core.ui.components.getDefaultRowColors -import com.tangem.core.ui.components.getWarningRowColors -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.feature.wallet.presentation.common.WalletPreviewDataLegacy -import com.tangem.feature.wallet.presentation.wallet.state.model.ActionsBottomSheetConfig -import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonConfig -import kotlinx.collections.immutable.ImmutableList - -@Composable -internal fun TokenActionsBottomSheet(config: TangemBottomSheetConfig) { - TangemBottomSheet(config = config) { - ActionsBottomSheetContent(actions = it.actions) - } -} - -@Composable -private fun ActionsBottomSheetContent(actions: ImmutableList) { - Column(modifier = Modifier.background(TangemTheme.colors.background.primary)) { - actions.forEach { action -> - if (action.enabled) { - val rowColors = if (action.isWarning) { - getWarningRowColors() - } else { - getDefaultRowColors() - } - SimpleSettingsRow( - title = action.text.resolveReference(), - icon = action.iconResId, - enabled = action.enabled, - rowColors = rowColors, - onItemsClick = action.onClick, - ) - } - } - } -} - -@Preview -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun ActionsBottomSheetContent_Light( - @PreviewParameter(ActionsBottomSheetContentConfigProvider::class) - config: ActionsBottomSheetConfig, -) { - TangemThemePreview { - // Use preview of content because ModalBottomSheet isn't supported in Preview mode - ActionsBottomSheetContent(actions = config.actions) - } -} - -private class ActionsBottomSheetContentConfigProvider : CollectionPreviewParameterProvider( - collection = listOf(WalletPreviewDataLegacy.actionsBottomSheet), -) \ No newline at end of file From dff5b1f27c819399ffbd7221896346d1d2e7de46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 15:41:35 +0500 Subject: [PATCH 86/97] Updated on 2026-08-14 --- .../tangem/feature/wallet/child/wallet/model/WalletModel.kt | 5 +++++ .../wallet/analytics/WalletScreenAnalyticsEvent.kt | 2 ++ 2 files changed, 7 insertions(+) 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 6b19da9a9f..9adf3437d5 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 @@ -13,6 +13,8 @@ import com.tangem.core.decompose.model.Model import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase +import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.GetAppThemeModeUseCase import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -97,6 +99,7 @@ internal class WalletModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase, private val appsFlyerStore: AppsFlyerStore, + private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -219,6 +222,7 @@ internal class WalletModel @Inject constructor( } val result = getAppThemeModeUseCase().firstOrNull() val theme = result?.getOrElse { AppThemeMode.FOLLOW_SYSTEM } ?: AppThemeMode.FOLLOW_SYSTEM + val appCurrency = getSelectedAppCurrencyUseCase.invokeSync().getOrElse { AppCurrency.Default }.code analyticsEventsHandler.send( WalletScreenAnalyticsEvent.MainScreen.ScreenOpened( hasMobileWallet = hasMobileWallet, @@ -226,6 +230,7 @@ internal class WalletModel @Inject constructor( theme = theme.value, isImported = selectedWallet.isImported(), referralId = appsFlyerStore.get()?.refcode, + appCurrency = appCurrency, ), ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 11aaab2b47..257ce14de3 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -57,6 +57,7 @@ sealed class WalletScreenAnalyticsEvent { val theme: String, val isImported: Boolean, val referralId: String?, + val appCurrency: String, ) : MainScreen( event = "Screen opened", params = buildMap { @@ -69,6 +70,7 @@ sealed class WalletScreenAnalyticsEvent { "Seedless" } put("Wallet Type", seedPhrase) + put("App Currency", appCurrency) putAll(getReferralParams(referralId)) }, ), AppsFlyerIncludedEvent From 978d7c1da466749124a22eb7c06cb02e937dc976 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 11:21:00 +0400 Subject: [PATCH 87/97] Updated on 2026-08-14 --- .../swap/DefaultSwapTransactionRepository.kt | 9 +- .../com/tangem/data/swap/di/SwapDataModule.kt | 3 - .../feed/components/FeedEntryChildFactory.kt | 3 - .../DefaultMarketsTokenDetailsComponent.kt | 3 - .../detailed/MarketsTokenDetailsContent.kt | 7 +- .../components/TokenMarketDetailsBody.kt | 11 +- .../features/send/v2/send/model/SendModel.kt | 126 ++------- .../send/v2/sendnft/model/NFTSendModel.kt | 72 ++---- .../destination/model/SendDestinationModel.kt | 58 +---- .../sendviaswap/model/SendWithSwapModel.kt | 91 +------ .../swap/DefaultSwapTransactionRepository.kt | 9 +- .../tangem/feature/swap/di/SwapDataModule.kt | 3 - .../DefaultInitialToCurrencyResolver.kt | 27 +- .../swap/domain/InitialToCurrencyResolver.kt | 12 +- .../feature/swap/domain/SwapInteractor.kt | 8 +- .../feature/swap/domain/SwapInteractorImpl.kt | 111 +------- .../swap/converters/TokensDataConverter.kt | 155 +++++------- .../swap/converters/TokensDataConverterV2.kt | 80 ------ .../tangem/feature/swap/model/SwapModel.kt | 239 ++++++------------ .../tangem/feature/swap/ui/StateBuilder.kt | 30 +-- 20 files changed, 202 insertions(+), 855 deletions(-) delete mode 100644 features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt index 3734069fb2..37c6ab2dfb 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapTransactionRepository.kt @@ -15,7 +15,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.datasource.local.preferences.utils.getObjectMap -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId @@ -30,7 +29,6 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn internal class DefaultSwapTransactionRepository( @@ -39,7 +37,6 @@ internal class DefaultSwapTransactionRepository( private val networkFactory: NetworkFactory, private val dispatchers: CoroutineDispatcherProvider, private val multiAccountListSupplier: MultiAccountListSupplier, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : SwapTransactionRepository { private val listConverter by lazy(LazyThreadSafetyMode.NONE) { @@ -122,11 +119,7 @@ internal class DefaultSwapTransactionRepository( flow2 = appPreferencesStore.getObjectMap( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, ), - flow3 = if (accountsFeatureToggles.isFeatureEnabled) { - multiAccountListSupplier() - } else { - flowOf(emptyList()) - }, + flow3 = multiAccountListSupplier(), ) { savedTransactions, txStatuses, multiAccountList -> val currencyTxs = savedTransactions ?.filter { swapTxList -> diff --git a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt index fd3fadb12b..6fee629eea 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/di/SwapDataModule.kt @@ -12,7 +12,6 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.express.ExpressRepository import com.tangem.domain.quotes.single.SingleQuoteStatusFetcher @@ -71,7 +70,6 @@ internal object SwapDataModule { responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, networkFactory: NetworkFactory, multiAccountListSupplier: MultiAccountListSupplier, - accountsFeatureToggles: AccountsFeatureToggles, dispatchers: CoroutineDispatcherProvider, ): SwapTransactionRepository { return DefaultSwapTransactionRepository( @@ -79,7 +77,6 @@ internal object SwapDataModule { responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, networkFactory = networkFactory, multiAccountListSupplier = multiAccountListSupplier, - accountsFeatureToggles = accountsFeatureToggles, dispatchers = dispatchers, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index 02b1c82ab7..31b71bb3ea 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -6,7 +6,6 @@ import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.navigation.Route import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent @@ -20,7 +19,6 @@ import javax.inject.Inject internal class FeedEntryChildFactory @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, private val portfolioComponentFactory: MarketsPortfolioComponent.Factory, private val addToPortfolioPreselectedDataComponent: AddToPortfolioPreselectedDataComponent.Factory, ) { @@ -66,7 +64,6 @@ internal class FeedEntryChildFactory @Inject constructor( appComponentContext = appComponentContext, params = child.params, analyticsEventHandler = analyticsEventHandler, - accountsFeatureToggles = accountsFeatureToggles, portfolioComponentFactory = portfolioComponentFactory, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index c3fc430041..d43ace2233 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -15,7 +15,6 @@ import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.currency.CryptoCurrency @@ -33,7 +32,6 @@ internal class DefaultMarketsTokenDetailsComponent( appComponentContext: AppComponentContext, val params: Params, analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, portfolioComponentFactory: MarketsPortfolioComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { @@ -117,7 +115,6 @@ internal class DefaultMarketsTokenDetailsComponent( modifier = modifier, backgroundColor = LocalMainBottomSheetColor.current.value, state = state, - isAccountEnabled = accountsFeatureToggles.isFeatureEnabled, portfolioBlock = portfolioComponent?.let { component -> { blockModifier -> component.Content(blockModifier) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt index 77c6da0b24..67cc815484 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/MarketsTokenDetailsContent.kt @@ -42,7 +42,6 @@ import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.markets.PriceChangeInterval import com.tangem.features.feed.impl.R import com.tangem.features.feed.ui.market.detailed.components.* -import com.tangem.core.ui.R as CoreR import com.tangem.features.feed.ui.market.detailed.preview.MarketsTokenDetailsPreview import com.tangem.features.feed.ui.market.detailed.state.ExchangesBottomSheetContent import com.tangem.features.feed.ui.market.detailed.state.InfoBottomSheetContent @@ -50,13 +49,13 @@ import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.features.feed.ui.market.detailed.state.SecurityScoreBottomSheetContent import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.flow.distinctUntilChanged +import com.tangem.core.ui.R as CoreR @Suppress("LongParameterList") @Composable internal fun MarketsTokenDetailsContent( state: MarketsTokenDetailsUM, backgroundColor: Color, - isAccountEnabled: Boolean, modifier: Modifier = Modifier, portfolioBlock: @Composable ((Modifier) -> Unit)?, ) { @@ -65,7 +64,6 @@ internal fun MarketsTokenDetailsContent( backgroundColor = backgroundColor, state = state, portfolioBlock = portfolioBlock, - isAccountEnabled = isAccountEnabled, ) when (state.bottomSheetConfig.content) { @@ -80,7 +78,6 @@ internal fun MarketsTokenDetailsContent( private fun Content( state: MarketsTokenDetailsUM, backgroundColor: Color, - isAccountEnabled: Boolean, modifier: Modifier = Modifier, portfolioBlock: @Composable ((Modifier) -> Unit)?, ) { @@ -134,7 +131,6 @@ private fun Content( tokenMarketDetailsBody( state = state.body, - isAccountEnabled = isAccountEnabled, portfolioBlock = portfolioBlock, relatedNews = state.relatedNews, ) @@ -322,7 +318,6 @@ private fun MarketsTokenDetailsContent_Preview( state = params, backgroundColor = TangemTheme.colors.background.tertiary, portfolioBlock = {}, - isAccountEnabled = true, ) } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt index 01d5c21f9d..a0457c58ec 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/market/detailed/components/TokenMarketDetailsBody.kt @@ -20,10 +20,9 @@ import com.tangem.features.feed.ui.feed.state.NewsSliderConfig import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM import com.tangem.features.feed.ui.market.detailed.state.MarketsTokenDetailsUM.RelatedNews -@Suppress("CanBeNonNullable") // TODO will be removed after [REDACTED_JIRA] +@Suppress("CanBeNonNullable") internal fun LazyListScope.tokenMarketDetailsBody( state: MarketsTokenDetailsUM.Body, - isAccountEnabled: Boolean, portfolioBlock: @Composable ((Modifier) -> Unit)?, relatedNews: RelatedNews, ) { @@ -39,9 +38,7 @@ internal fun LazyListScope.tokenMarketDetailsBody( } } - if (isAccountEnabled) { - aboutCoinHeader() - } + aboutCoinHeader() loadingInfoBlocks() } @@ -60,9 +57,7 @@ internal fun LazyListScope.tokenMarketDetailsBody( relatedNews(relatedNews) } - if (isAccountEnabled) { - aboutCoinHeader() - } + aboutCoinHeader() infoBlocksList(state.infoBlocks) } 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 c8eeee19df..e9d194d455 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 @@ -5,7 +5,6 @@ import arrow.core.Either import arrow.core.getOrElse import arrow.core.left import com.tangem.blockchain.common.TransactionData -import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.navigationButtons.NavigationUM @@ -16,29 +15,23 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseBigDecimalOrNull -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase 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 -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.account.Account -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.models.wallet.isMultiCurrency import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase @@ -55,7 +48,6 @@ import com.tangem.features.send.v2.api.entity.FeeSelectorUM import com.tangem.features.send.v2.api.entity.PredefinedValues import com.tangem.features.send.v2.api.subcomponents.destination.SendDestinationComponent import com.tangem.features.send.v2.api.subcomponents.destination.entity.DestinationUM -import com.tangem.features.send.v2.common.CommonSendRoute import com.tangem.features.send.v2.common.CommonSendRoute.* import com.tangem.features.send.v2.common.SendConfirmAlertFactory import com.tangem.features.send.v2.common.ui.state.ConfirmUM @@ -74,7 +66,6 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject import kotlin.properties.Delegates -import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned internal interface SendComponentCallback : SendAmountComponent.ModelCallback, @@ -90,7 +81,6 @@ internal class SendModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, private val getUserWalletUseCase: GetUserWalletUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val listenToQrScanningUseCase: ListenToQrScanningUseCase, @@ -108,7 +98,6 @@ internal class SendModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val sendAmountUpdateTrigger: SendAmountUpdateTrigger, private val analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : Model(), SendComponentCallback { private val params: SendComponent.Params = paramsContainer.require() @@ -254,7 +243,7 @@ internal class SendModel @Inject constructor( showAlertError() } - suspend fun prepareTransferTransaction(): Either { + private suspend fun prepareTransferTransaction(): Either { val predefinedValues = predefinedValues val cryptoCurrencyStatus = cryptoCurrencyStatusFlow.value return if (predefinedValues is PredefinedValues.Content.Deeplink) { @@ -360,34 +349,24 @@ internal class SendModel @Inject constructor( ifRight = { wallet -> userWallet = wallet - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( + getAccountCurrencyStatusUseCase( + userWalletId = params.userWalletId, + currency = cryptoCurrency, + ).onEach { (account, cryptoCurrencyStatus) -> + isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync() + accountFlow.value = account + + cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus + feeCryptoCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = params.userWalletId, - currency = cryptoCurrency, - ).onEach { (account, cryptoCurrencyStatus) -> - isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync() - accountFlow.value = account + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: cryptoCurrencyStatus - cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus - feeCryptoCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: cryptoCurrencyStatus - - if (params.amount != null) { - router.replaceAll(Confirm) - } - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - val isSingleWalletWithToken = wallet is UserWallet.Cold && - wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() - val isMultiCurrency = wallet.isMultiCurrency - getCurrenciesStatusUpdates( - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ) - } + if (params.amount != null) { + router.replaceAll(Confirm) + } + }.flowOn(dispatchers.default) + .launchIn(modelScope) }, ifLeft = { error -> Timber.w(error.toString()) @@ -409,77 +388,6 @@ internal class SendModel @Inject constructor( .saveIn(balanceHidingJobHolder) } - private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean, isMultiCurrency: Boolean) { - getCurrencyStatus( - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ).onEach { maybeCryptoCurrency -> - maybeCryptoCurrency.fold( - ifRight = { cryptoCurrencyStatus -> - cryptoCurrencyStatusFlow.value = cryptoCurrencyStatus - feeCryptoCurrencyStatusFlow.value = getFeeCurrencyStatus(cryptoCurrencyStatus, isMultiCurrency) - - if (params.amount != null) { - router.replaceAll(CommonSendRoute.Confirm) - } - }, - ifLeft = { - sendConfirmAlertFactory.getGenericErrorState( - onFailedTxEmailClick = { - onFailedTxEmailClick(it.toString()) - }, - popBack = router::pop, - ) - }, - ) - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } - - private fun getCurrencyStatus( - isSingleWalletWithToken: Boolean, - isMultiCurrency: Boolean, - ): Flow> { - return when { - isSingleWalletWithToken -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = params.userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = true, - ) - isMultiCurrency -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = params.userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = false, - ) - else -> getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = params.userWalletId) - } - } - - fun getSelectedFeeToken(): CryptoCurrency { - val feeUMV2 = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content - val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended - val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency - return if (isFeeInTokenCurrency) { - feeUMV2.feeExtraInfo.feeCryptoCurrencyStatus.currency - } else { - feeCryptoCurrencyStatusFlow.value.currency - } - } - - private suspend fun getFeeCurrencyStatus( - cryptoCurrencyStatus: CryptoCurrencyStatus, - isMultiCurrency: Boolean, - ): CryptoCurrencyStatus { - return if (isMultiCurrency) { - getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: cryptoCurrencyStatus - } else { - cryptoCurrencyStatus - } - } - private fun subscribeOnQRScannerResult() { listenToQrScanningUseCase(SourceType.SEND) .getOrElse { emptyFlow() } diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 5128c82ff5..c7d65fec93 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -11,12 +11,10 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -27,7 +25,6 @@ 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.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.transaction.error.GetFeeError @@ -66,7 +63,6 @@ internal class NFTSendModel @Inject constructor( private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val createNFTTransferTransactionUseCase: CreateNFTTransferTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, @@ -77,7 +73,6 @@ internal class NFTSendModel @Inject constructor( private val nftSendSuccessTrigger: NFTSendSuccessTrigger, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : Model(), SendNFTComponentCallback, NFTSendSuccessComponent.ModelCallback { val params: NFTSendComponent.Params = paramsContainer.require() @@ -186,31 +181,24 @@ internal class NFTSendModel @Inject constructor( ?.firstOrNull { it is CryptoCurrency.Coin && it.network == nftAsset.network } ?: return@launch - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( - userWalletId, - cryptoCurrency, - ).onEach { (maybeAccount, cryptoStatus) -> - account = maybeAccount - isAccountsMode = isAccountsModeEnabledUseCase.invokeSync() + getAccountCurrencyStatusUseCase( + userWalletId, + cryptoCurrency, + ).onEach { (maybeAccount, cryptoStatus) -> + account = maybeAccount + isAccountsMode = isAccountsModeEnabledUseCase.invokeSync() - cryptoCurrencyStatus = cryptoStatus - feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoStatus, - ).getOrNull() ?: cryptoStatus + cryptoCurrencyStatus = cryptoStatus + feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = cryptoStatus, + ).getOrNull() ?: cryptoStatus - if (uiState.value.destinationUM is DestinationUM.Empty) { - router.replaceAll(Destination(isEditMode = false)) - } - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - getCurrenciesStatusUpdates( - isSingleWalletWithToken = wallet is UserWallet.Cold && - wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken(), - ) - } + if (uiState.value.destinationUM is DestinationUM.Empty) { + router.replaceAll(Destination(isEditMode = false)) + } + }.flowOn(dispatchers.default) + .launchIn(modelScope) }, ifLeft = { alertFactory.getGenericErrorState(::onFailedTxEmailClick) @@ -246,34 +234,6 @@ internal class NFTSendModel @Inject constructor( } } - private fun getCurrenciesStatusUpdates(isSingleWalletWithToken: Boolean) { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = isSingleWalletWithToken, - ).onEach { maybeCryptoCurrency -> - maybeCryptoCurrency.fold( - ifRight = { cryptoStatus -> - cryptoCurrencyStatus = cryptoStatus - feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = cryptoStatus, - ).getOrNull() ?: cryptoStatus - - if (uiState.value.destinationUM is DestinationUM.Empty) { - router.replaceAll(Destination(isEditMode = false)) - } - }, - ifLeft = { - alertFactory.getGenericErrorState( - onFailedTxEmailClick = { onFailedTxEmailClick(it.toString()) }, - popBack = { router.pop() }, - ) - }, - ) - }.launchIn(modelScope) - } - private fun initialState(): NFTSendUM = NFTSendUM( destinationUM = DestinationUM.Empty(), feeSelectorUM = FeeSelectorUM.Loading, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 96690cb647..e2c9a282c5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -11,18 +11,14 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.models.account.filterCryptoPortfolio import com.tangem.domain.models.network.CryptoCurrencyAddress -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked -import com.tangem.domain.models.wallet.isMultiCurrency import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase -import com.tangem.domain.tokens.GetCryptoCurrencyUseCase import com.tangem.domain.tokens.GetNetworkAddressesUseCase import com.tangem.domain.transaction.usecase.IsSelfSendAvailableUseCase import com.tangem.domain.transaction.usecase.ValidateWalletAddressUseCase @@ -63,7 +59,6 @@ internal class SendDestinationModel @Inject constructor( private val validateWalletAddressUseCase: ValidateWalletAddressUseCase, private val validateWalletMemoUseCase: ValidateWalletMemoUseCase, private val getWalletsUseCase: GetWalletsUseCase, - private val getCryptoCurrencyUseCase: GetCryptoCurrencyUseCase, private val getNetworkAddressesUseCase: GetNetworkAddressesUseCase, private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val isSelfSendAvailableUseCase: IsSelfSendAvailableUseCase, @@ -71,7 +66,6 @@ internal class SendDestinationModel @Inject constructor( private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, ) : Model(), SendDestinationClickIntents { private val params: SendDestinationComponentParams = paramsContainer.require() @@ -191,15 +185,7 @@ internal class SendDestinationModel @Inject constructor( private fun getWalletsAndRecent() { combine( - flow = if (accountsFeatureToggles.isFeatureEnabled) { - getAddedAddresses() - } else { - getWalletsUseCase().conflate().map { - waitForDelay(RECENT_LOAD_DELAY) { - it.toAvailableWallets() - } - } - }, + flow = getAddedAddresses(), flow2 = getFixedTxHistoryItemsUseCase( userWalletId = userWalletId, currency = cryptoCurrency, @@ -226,48 +212,6 @@ internal class SendDestinationModel @Inject constructor( }.flowOn(dispatchers.default).launchIn(modelScope) } - private suspend fun List.toAvailableWallets(): List { - return coroutineScope { - val cryptoCurrencyNetwork = cryptoCurrency.network - - return@coroutineScope filterNot { it.isLocked } - .map { wallet -> - async { - val addresses = if (!wallet.isMultiCurrency) { - getCryptoCurrencyUseCase(wallet.walletId).getOrNull()?.let { cryptoCurrency -> - if (cryptoCurrency.network.rawId == cryptoCurrencyNetwork.rawId) { - getNetworkAddressesUseCase.invokeSync( - userWalletId = wallet.walletId, - networkRawId = cryptoCurrency.network.id.rawId, - ) - } else { - null - } - } - } else { - getNetworkAddressesUseCase.invokeSync( - userWalletId = wallet.walletId, - networkRawId = cryptoCurrencyNetwork.id.rawId, - ) - } - wallet to addresses - } - }.awaitAll() - .asSequence() - .mapNotNull { (wallet, addresses) -> - addresses?.map { (cryptoCurrency, address) -> - DestinationWalletUM( - name = wallet.name, - address = address, - cryptoCurrency = cryptoCurrency, - userWalletId = wallet.walletId, - ) - } - }.flatten() - .toList() - } - } - private fun getAddedAddresses(): Flow> { return combine( flow = getWalletsUseCase().conflate(), diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt index 31b4ba8651..dc868296e8 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/model/SendWithSwapModel.kt @@ -1,29 +1,23 @@ package com.tangem.features.swap.v2.impl.sendviaswap.model -import arrow.core.Either import arrow.core.getOrElse import com.tangem.common.ui.navigationButtons.NavigationUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase 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 -import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.express.models.ExpressError import com.tangem.domain.models.account.Account 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.models.wallet.isMultiCurrency import com.tangem.domain.swap.models.SwapDirection import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase -import com.tangem.domain.tokens.error.CurrencyStatusError import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.features.send.v2.api.analytics.CommonSendAnalyticEvents import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -49,7 +43,6 @@ import kotlin.properties.Delegates internal class SendWithSwapModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, private val getUserWalletUseCase: GetUserWalletUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -57,7 +50,6 @@ internal class SendWithSwapModel @Inject constructor( private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val swapAlertFactory: SwapAlertFactory, - private val accountsFeatureToggles: AccountsFeatureToggles, paramsContainer: ParamsContainer, ) : Model(), SwapAmountComponent.ModelCallback, @@ -202,79 +194,20 @@ internal class SendWithSwapModel @Inject constructor( } private fun getPrimaryCurrencyStatusUpdates(cryptoCurrency: CryptoCurrency) { - val wallet = userWallet - val isMultiCurrency = wallet.isMultiCurrency - val isSingleWalletWithToken = wallet is UserWallet.Cold && - wallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() + getAccountCurrencyStatusUseCase( + userWalletId = params.userWalletId, + currency = cryptoCurrency, + ).onEach { (account, cryptoCurrencyStatus) -> + accountFlow.value = account + isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync() - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( + primaryCryptoCurrencyStatusFlow.value = cryptoCurrencyStatus + primaryFeePaidCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( userWalletId = params.userWalletId, - currency = cryptoCurrency, - ).onEach { (account, cryptoCurrencyStatus) -> - accountFlow.value = account - isAccountModeFlow.value = isAccountsModeEnabledUseCase.invokeSync() - - primaryCryptoCurrencyStatusFlow.value = cryptoCurrencyStatus - primaryFeePaidCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: cryptoCurrencyStatus - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } else { - getCurrencyStatus( - cryptoCurrency = cryptoCurrency, - isSingleWalletWithToken = isSingleWalletWithToken, - isMultiCurrency = isMultiCurrency, - ).onEach { maybeCryptoCurrency -> - maybeCryptoCurrency.fold( - ifRight = { cryptoCurrencyStatus -> - primaryCryptoCurrencyStatusFlow.value = cryptoCurrencyStatus - primaryFeePaidCurrencyStatusFlow.value = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = params.userWalletId, - cryptoCurrencyStatus = cryptoCurrencyStatus, - ).getOrNull() ?: cryptoCurrencyStatus - }, - ifLeft = { error -> - swapAlertFactory.getGenericErrorState( - expressError = ExpressError.UnknownError, - onFailedTxEmailClick = { - modelScope.launch { - swapAlertFactory.onFailedTxEmailClick( - userWallet = userWallet, - cryptoCurrency = params.currency, - errorMessage = error.toString(), - ) - } - }, - popBack = ::onBackClick, - ) - }, - ) - }.flowOn(dispatchers.default) - .launchIn(modelScope) - } - } - - private fun getCurrencyStatus( - cryptoCurrency: CryptoCurrency, - isSingleWalletWithToken: Boolean, - isMultiCurrency: Boolean, - ): Flow> { - return when { - isSingleWalletWithToken -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = params.userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = true, - ) - isMultiCurrency -> getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = params.userWalletId, - currencyId = cryptoCurrency.id, - isSingleWalletWithTokens = false, - ) - else -> getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = params.userWalletId) - } + cryptoCurrencyStatus = cryptoCurrencyStatus, + ).getOrNull() ?: cryptoCurrencyStatus + }.flowOn(dispatchers.default) + .launchIn(modelScope) } private fun subscribeOnBalanceHidden() { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index a04749322a..1d102e458e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -8,7 +8,6 @@ import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync import com.tangem.datasource.local.preferences.utils.getObjectMap -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.account.Account @@ -23,14 +22,12 @@ import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn internal class DefaultSwapTransactionRepository( private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, private val singleAccountListSupplier: SingleAccountListSupplier, - private val accountsFeatureToggles: AccountsFeatureToggles, responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, networkFactory: NetworkFactory, ) : SwapTransactionRepository { @@ -110,11 +107,7 @@ internal class DefaultSwapTransactionRepository( flow2 = appPreferencesStore.getObjectMap( key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, ), - flow3 = if (accountsFeatureToggles.isFeatureEnabled) { - singleAccountListSupplier(SingleAccountListProducer.Params(userWallet.walletId)) - } else { - flowOf(null) - }, + flow3 = singleAccountListSupplier(SingleAccountListProducer.Params(userWallet.walletId)), ) { savedTransactions, txStatuses, accountList -> val currencyToTxs = savedTransactions?.filter { savedTx -> diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 669ba6fcd4..6057e6344e 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -8,7 +8,6 @@ import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.walletmanager.WalletManagersFacade @@ -59,7 +58,6 @@ internal class SwapDataModule { responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, networkFactory: NetworkFactory, singleAccountListSupplier: SingleAccountListSupplier, - accountsFeatureToggles: AccountsFeatureToggles, dispatcherProvider: CoroutineDispatcherProvider, ): SwapTransactionRepository { return DefaultSwapTransactionRepository( @@ -67,7 +65,6 @@ internal class SwapDataModule { responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, networkFactory = networkFactory, singleAccountListSupplier = singleAccountListSupplier, - accountsFeatureToggles = accountsFeatureToggles, dispatchers = dispatcherProvider, ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt index a9764ab530..e8d4a35b72 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/DefaultInitialToCurrencyResolver.kt @@ -1,13 +1,11 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.AccountSwapCurrency import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress import com.tangem.feature.swap.domain.models.ui.getGroupWithReverse import com.tangem.utils.extensions.orZero -import java.math.BigDecimal internal class DefaultInitialToCurrencyResolver( private val swapTransactionRepository: SwapTransactionRepository, @@ -18,22 +16,6 @@ internal class DefaultInitialToCurrencyResolver( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, isReverseFromTo: Boolean, - ): CryptoCurrencyStatus? { - val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null - - return if (id != initialCryptoCurrency.id.value) { - val group = state.getGroupWithReverse(isReverseFromTo) - group.available.find { it.currencyStatus.currency.id.value == id }?.currencyStatus - } else { - null - } - } - - override suspend fun tryGetFromCacheV2( - userWallet: UserWallet, - initialCryptoCurrency: CryptoCurrency, - state: TokensDataStateExpress, - isReverseFromTo: Boolean, ): AccountSwapCurrency? { val id = swapTransactionRepository.getLastSwappedCryptoCurrencyId(userWallet.walletId) ?: return null @@ -48,14 +30,7 @@ internal class DefaultInitialToCurrencyResolver( } } - override fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus? { - val group = state.getGroupWithReverse(isReverseFromTo) - return group.available.maxByOrNull { - it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO - }?.currencyStatus - } - - override fun tryGetWithMaxAmountV2(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? { + override fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? { val group = state.getGroupWithReverse(isReverseFromTo) return group.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) -> currencyList.maxByOrNull { swapAccountCurrency -> diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt index 7759a61e98..da6436ee57 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/InitialToCurrencyResolver.kt @@ -1,7 +1,6 @@ package com.tangem.feature.swap.domain import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.swap.domain.models.ui.AccountSwapCurrency import com.tangem.feature.swap.domain.models.ui.TokensDataStateExpress @@ -13,16 +12,7 @@ interface InitialToCurrencyResolver { initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, isReverseFromTo: Boolean, - ): CryptoCurrencyStatus? - - fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): CryptoCurrencyStatus? - - suspend fun tryGetFromCacheV2( - userWallet: UserWallet, - initialCryptoCurrency: CryptoCurrency, - state: TokensDataStateExpress, - isReverseFromTo: Boolean, ): AccountSwapCurrency? - fun tryGetWithMaxAmountV2(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? + fun tryGetWithMaxAmount(state: TokensDataStateExpress, isReverseFromTo: Boolean): AccountSwapCurrency? } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index c1193cdb4c..98165323aa 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -105,12 +105,6 @@ interface SwapInteractor { */ fun getTokenBalance(token: CryptoCurrencyStatus): SwapAmount - suspend fun getInitialCurrencyToSwap( - initialCryptoCurrency: CryptoCurrency, - state: TokensDataStateExpress, - isReverseFromTo: Boolean, - ): CryptoCurrencyStatus? - /** * Returns initial currency to swap as AccountSwapCurrency * @@ -118,7 +112,7 @@ interface SwapInteractor { * @param state current tokens data state * @param isReverseFromTo flag indicating the direction of the swap */ - suspend fun getInitialCurrencyToSwapV2( + suspend fun getInitialCurrencyToSwap( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, isReverseFromTo: Boolean, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index c3793db861..1856211b21 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -19,7 +19,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -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.appcurrency.GetSelectedAppCurrencyUseCase @@ -75,7 +74,6 @@ import java.math.RoundingMode internal class SwapInteractorImpl @AssistedInject constructor( private val repository: SwapRepository, private val allowPermissionsHandler: AllowPermissionsHandler, - private val getMultiCryptoCurrencyStatusUseCase: GetMultiCryptoCurrencyStatusUseCase, private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase, private val sendTransactionUseCase: SendTransactionUseCase, private val createTransactionUseCase: CreateTransactionUseCase, @@ -106,7 +104,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( private val rampStateManager: RampStateManager, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val walletManagersFacade: WalletManagersFacade, @Assisted private val userWalletId: UserWalletId, ) : SwapInteractor { @@ -123,58 +120,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { - return if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyTokensDataState(currency) - } else { - getCurrencyTokensDataState(currency) - } - } - - private suspend fun getCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { - val walletCurrencyStatuses = getMultiCryptoCurrencyStatusUseCase - .invokeMultiWalletSync(userWalletId) - .getOrElse { emptyList() } - - val walletCurrencyStatusesExceptInitial = walletCurrencyStatuses - .filter { status -> - val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || - status.currency.getContractAddress() != currency.getContractAddress() - val hasValidStatus = - status.value is CryptoCurrencyStatus.Loaded || status.value is CryptoCurrencyStatus.NoAccount - val isNotCustomToken = !status.currency.isCustom - hasValidStatus && isDifferentCurrency && isNotCustomToken - } - - if (walletCurrencyStatusesExceptInitial.isEmpty()) { - return TokensDataStateExpress.EMPTY - } - - val pairsLeast = getPairs( - userWallet = userWallet, - initialCurrency = LeastTokenInfo( - contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = currency.network.backendId, - ), - currenciesList = walletCurrencyStatusesExceptInitial.map { it.currency }, - ) - - return TokensDataStateExpress( - fromGroup = getToCurrenciesGroup( - currency = currency, - leastPairs = pairsLeast.pairs, - cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, - tokenInfoForFilter = { it.to }, - tokenInfoForAvailable = { it.from }, - ), - toGroup = getToCurrenciesGroup( - currency = currency, - leastPairs = pairsLeast.pairs, - cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, - tokenInfoForFilter = { it.from }, - tokenInfoForAvailable = { it.to }, - ), - allProviders = pairsLeast.allProviders, - ) + return getAccountCurrencyTokensDataState(currency) } private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { @@ -212,14 +158,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) return TokensDataStateExpress( - fromGroup = getToCurrenciesGroupV2( + fromGroup = getToCurrenciesGroup( currency = currency, leastPairs = pairsLeast.pairs, cryptoCurrenciesList = walletAccountCurrencyStatusesExceptInitial, tokenInfoForFilter = { it.to }, tokenInfoForAvailable = { it.from }, ), - toGroup = getToCurrenciesGroupV2( + toGroup = getToCurrenciesGroup( currency = currency, leastPairs = pairsLeast.pairs, cryptoCurrenciesList = walletAccountCurrencyStatusesExceptInitial, @@ -242,39 +188,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private suspend fun getToCurrenciesGroup( - currency: CryptoCurrency, - leastPairs: List, - cryptoCurrenciesList: List, - tokenInfoForFilter: (SwapPairLeast) -> LeastTokenInfo, - tokenInfoForAvailable: (SwapPairLeast) -> LeastTokenInfo, - ): CurrenciesGroup { - val filteredPairs = leastPairs.filter { pair -> - tokenInfoForFilter(pair).contractAddress == currency.getContractAddress() && - tokenInfoForFilter(pair).network == currency.network.backendId - } - - val availableCryptoCurrencies = cryptoCurrenciesList.mapNotNull { cryptoCurrencyStatus -> - val providers = findProvidersForPair(cryptoCurrencyStatus, filteredPairs, tokenInfoForAvailable) - if (providers != null) { - CryptoCurrencySwapInfo(cryptoCurrencyStatus, providers) - } else { - null - } - } - - val unavailableCryptoCurrencies = cryptoCurrenciesList - availableCryptoCurrencies - .map { it.currencyStatus } - .toSet() - - return CurrenciesGroup( - available = availableCryptoCurrencies, - unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) }, - accountCurrencyList = emptyList(), - isAfterSearch = false, - ) - } - - private suspend fun getToCurrenciesGroupV2( currency: CryptoCurrency, leastPairs: List, cryptoCurrenciesList: Map>, @@ -1323,7 +1236,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( initialCryptoCurrency: CryptoCurrency, state: TokensDataStateExpress, isReverseFromTo: Boolean, - ): CryptoCurrencyStatus? { + ): AccountSwapCurrency? { val group = state.getGroupWithReverse(isReverseFromTo) return initialToCurrencyResolver.tryGetFromCache( userWallet = userWallet, @@ -1332,22 +1245,6 @@ internal class SwapInteractorImpl @AssistedInject constructor( isReverseFromTo = isReverseFromTo, ) ?: initialToCurrencyResolver.tryGetWithMaxAmount(state = state, isReverseFromTo = isReverseFromTo) - ?: group.available.firstOrNull()?.currencyStatus - } - - override suspend fun getInitialCurrencyToSwapV2( - initialCryptoCurrency: CryptoCurrency, - state: TokensDataStateExpress, - isReverseFromTo: Boolean, - ): AccountSwapCurrency? { - val group = state.getGroupWithReverse(isReverseFromTo) - return initialToCurrencyResolver.tryGetFromCacheV2( - userWallet = userWallet, - initialCryptoCurrency = initialCryptoCurrency, - state = state, - isReverseFromTo = isReverseFromTo, - ) - ?: initialToCurrencyResolver.tryGetWithMaxAmountV2(state = state, isReverseFromTo = isReverseFromTo) ?: group.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> accountSwapCurrency.isAvailable diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 791af35a8c..d54ae4ecc1 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -1,113 +1,80 @@ package com.tangem.feature.swap.converters -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.extensions.* -import com.tangem.core.ui.format.bigdecimal.crypto -import com.tangem.core.ui.format.bigdecimal.fiat -import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM +import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo -import com.tangem.feature.swap.models.CurrenciesGroupWithFromCurrency +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.TokenBalanceData +import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.TokenListUMData -import com.tangem.feature.swap.models.TokenToSelectState import com.tangem.feature.swap.presentation.R import com.tangem.utils.Provider -import com.tangem.utils.converter.Converter +import com.tangem.utils.transformer.Transformer import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList +import kotlinx.collections.immutable.plus +import kotlinx.collections.immutable.toPersistentList internal class TokensDataConverter( private val onSearchEntered: (String) -> Unit, private val onTokenSelected: (String) -> Unit, - private val isBalanceHiddenProvider: Provider, - private val appCurrencyProvider: Provider, -) : Converter { + private val tokensDataState: CurrenciesGroup, + private val isBalanceHidden: Boolean, + private val isAccountsMode: Boolean, + appCurrencyProvider: Provider, +) : Transformer { - override fun convert(value: CurrenciesGroupWithFromCurrency): SwapSelectTokenStateHolder { - val group = value.group - val availableTitle = TokenToSelectState.Title( - resourceReference(R.string.exchange_tokens_available_tokens_header), - ) - val allTokens = group.available + group.unavailable - return SwapSelectTokenStateHolder( - availableTokens = allTokens.map { tokenWithBalanceToTokenToSelect(it, true) } - .toMutableList() - .apply { - if (this.isNotEmpty()) { - this.add(0, availableTitle) + private val accountListItemConverter = AccountTokenItemConverter( + appCurrency = appCurrencyProvider(), + unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), + onItemClick = onTokenSelected, + ) + + override fun transform(prevState: SwapStateHolder): SwapStateHolder { + val accountList = tokensDataState.accountCurrencyList + val currentMarketsState = prevState.selectTokenState?.marketsState + return prevState.copy( + selectTokenState = SwapSelectTokenStateHolder( + availableTokens = persistentListOf(), + unavailableTokens = persistentListOf(), + tokensListData = if (isAccountsMode) { + val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() + val totalTokensCount = portfolioList.sumOf { it.tokens.size } + if (totalTokensCount > 0) { + TokenListUMData.AccountList( + tokensList = portfolioList, + totalTokensCount = totalTokensCount, + ) + } else { + TokenListUMData.EmptyList } - } - .toImmutableList(), - unavailableTokens = persistentListOf(), - tokensListData = TokenListUMData.EmptyList, - onSearchEntered = onSearchEntered, - onTokenSelected = onTokenSelected, - isBalanceHidden = isBalanceHiddenProvider(), - isAfterSearch = group.isAfterSearch, - ) - } + } else { + val tokensList = accountList.flatMap { (_, currencyList) -> + currencyList.asSequence().map { accountSwapCurrency -> + accountListItemConverter.createAvailableItemConverter() + .convert(accountSwapCurrency.cryptoCurrencyStatus) + }.map(TokensListItemUM::Token).toPersistentList() + }.toPersistentList() - private fun tokenWithBalanceToTokenToSelect( - cryptoCurrencySwapInfo: CryptoCurrencySwapInfo, - isAvailable: Boolean, - ): TokenToSelectState { - val cryptoCurrencyStatus = cryptoCurrencySwapInfo.currencyStatus - return TokenToSelectState.TokenToSelect( - id = cryptoCurrencyStatus.currency.id.value, - name = cryptoCurrencyStatus.currency.name, - symbol = cryptoCurrencyStatus.currency.symbol, - isAvailable = isAvailable, - tokenIcon = convertIcon(cryptoCurrencyStatus.currency, isAvailable), - addedTokenBalanceData = TokenBalanceData( - amount = formatCryptoAmount(cryptoCurrencyStatus), - amountEquivalent = formatFiatAmount(cryptoCurrencyStatus, appCurrencyProvider.invoke()), - isBalanceHidden = isBalanceHiddenProvider.invoke(), + if (tokensList.isNotEmpty()) { + TokenListUMData.TokenList( + tokensList = persistentListOf( + TokensListItemUM.GroupTitle( + id = "available_tokens_title", + text = resourceReference(R.string.exchange_tokens_available_tokens_header), + ), + ) + tokensList, + totalTokensCount = tokensList.size, + ) + } else { + TokenListUMData.EmptyList + } + }, + marketsState = currentMarketsState, + onSearchEntered = onSearchEntered, + onTokenSelected = onTokenSelected, + isBalanceHidden = isBalanceHidden, + isAfterSearch = tokensDataState.isAfterSearch, ), ) } - - private fun convertIcon(currency: CryptoCurrency, isAvailable: Boolean): CurrencyIconState { - return when (currency) { - is CryptoCurrency.Coin -> { - CurrencyIconState.CoinIcon( - url = currency.iconUrl, - fallbackResId = currency.networkIconResId, - isGrayscale = !isAvailable, - shouldShowCustomBadge = currency.isCustom, - ) - } - is CryptoCurrency.Token -> { - val isGrayscale = currency.network.isTestnet - val background = currency.tryGetBackgroundForTokenIcon(isGrayscale) - val tint = getTintForTokenIcon(background) - CurrencyIconState.TokenIcon( - url = currency.iconUrl, - isGrayscale = !isAvailable, - shouldShowCustomBadge = currency.isCustom, - topBadgeIconResId = currency.networkIconResId, - fallbackTint = tint, - fallbackBackground = background, - ) - } - } - } - - private fun formatCryptoAmount(cryptoCurrencyStatus: CryptoCurrencyStatus): String { - return cryptoCurrencyStatus.value.amount.format { - crypto(cryptoCurrencyStatus.currency) - } - } - - private fun formatFiatAmount(cryptoCurrencyStatus: CryptoCurrencyStatus, appCurrency: AppCurrency): String { - return cryptoCurrencyStatus.value.fiatAmount.format { - fiat( - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) - } - } } \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt deleted file mode 100644 index 1804474ea2..0000000000 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverterV2.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.tangem.feature.swap.converters - -import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup -import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.SwapStateHolder -import com.tangem.feature.swap.models.TokenListUMData -import com.tangem.feature.swap.presentation.R -import com.tangem.utils.Provider -import com.tangem.utils.transformer.Transformer -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.plus -import kotlinx.collections.immutable.toPersistentList - -internal class TokensDataConverterV2( - private val onSearchEntered: (String) -> Unit, - private val onTokenSelected: (String) -> Unit, - private val tokensDataState: CurrenciesGroup, - private val isBalanceHidden: Boolean, - private val isAccountsMode: Boolean, - appCurrencyProvider: Provider, -) : Transformer { - - private val accountListItemConverter = AccountTokenItemConverter( - appCurrency = appCurrencyProvider(), - unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), - onItemClick = onTokenSelected, - ) - - override fun transform(prevState: SwapStateHolder): SwapStateHolder { - val accountList = tokensDataState.accountCurrencyList - val currentMarketsState = prevState.selectTokenState?.marketsState - return prevState.copy( - selectTokenState = SwapSelectTokenStateHolder( - availableTokens = persistentListOf(), - unavailableTokens = persistentListOf(), - tokensListData = if (isAccountsMode) { - val portfolioList = accountListItemConverter.convertList(accountList).toPersistentList() - val totalTokensCount = portfolioList.sumOf { it.tokens.size } - if (totalTokensCount > 0) { - TokenListUMData.AccountList( - tokensList = portfolioList, - totalTokensCount = totalTokensCount, - ) - } else { - TokenListUMData.EmptyList - } - } else { - val tokensList = accountList.flatMap { (_, currencyList) -> - currencyList.asSequence().map { accountSwapCurrency -> - accountListItemConverter.createAvailableItemConverter() - .convert(accountSwapCurrency.cryptoCurrencyStatus) - }.map(TokensListItemUM::Token).toPersistentList() - }.toPersistentList() - - if (tokensList.isNotEmpty()) { - TokenListUMData.TokenList( - tokensList = persistentListOf( - TokensListItemUM.GroupTitle( - id = "available_tokens_title", - text = resourceReference(R.string.exchange_tokens_available_tokens_header), - ), - ) + tokensList, - totalTokensCount = tokensList.size, - ) - } else { - TokenListUMData.EmptyList - } - }, - marketsState = currentMarketsState, - onSearchEntered = onSearchEntered, - onTokenSelected = onTokenSelected, - isBalanceHidden = isBalanceHidden, - isAfterSearch = tokensDataState.isAfterSearch, - ), - ) - } -} \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt index b2ce189459..a0e992a13c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapModel.kt @@ -14,8 +14,6 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionState.InProgress.getApproveTypeOrNull -import com.tangem.features.approval.api.GiveApprovalComponent -import com.tangem.features.approval.api.GiveApprovalFeatureToggles import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.analytics.api.AnalyticsErrorHandler import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -36,7 +34,6 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -103,6 +100,8 @@ import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder import com.tangem.feature.swap.utils.formatToUIRepresentation +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalFeatureToggles import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager import com.tangem.features.send.v2.api.entity.FeeSelectorUM @@ -114,8 +113,6 @@ import com.tangem.utils.Provider import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero -import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.delay import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -156,7 +153,6 @@ internal class SwapModel @Inject constructor( router: AppRouter, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getTangemPayCurrencyStatusUseCase: GetTangemPayCurrencyStatusUseCase, private val tangemPayWithdrawUseCase: TangemPayWithdrawUseCase, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, @@ -244,12 +240,12 @@ internal class SwapModel @Inject constructor( private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null /** - * If accountsFeatureToggles is off OR user came from Tangem Pay -> fromAccountCurrencyStatus == null - * If accountsFeatureToggles is on AND user didn't come from Tangem Pay -> fromAccountCurrencyStatus != null + * If user came from Tangem Pay -> fromAccountCurrencyStatus == null + * If user didn't come from Tangem Pay -> fromAccountCurrencyStatus != null * * Remove when accounts are integrated into Tangem Pay */ - private val canUseFromAccountCurrencyStatus = accountsFeatureToggles.isFeatureEnabled && tangemPayInput == null + private val canUseFromAccountCurrencyStatus = tangemPayInput == null private val isUserResolvableError: (SwapState) -> Boolean = { swapState -> swapState is SwapState.SwapError && @@ -514,25 +510,25 @@ internal class SwapModel @Inject constructor( }.onSuccess { state -> updateTokensState(state) - val (selectedCurrency, selectedAccount) = if (accountsFeatureToggles.isFeatureEnabled) { - val selectedAccountCurrency = toAccountCurrencyStatus ?: swapInteractor.getInitialCurrencyToSwapV2( - initialCryptoCurrency = initialCurrencyFrom, - state = state, - isReverseFromTo = isReverseFromTo, - )?.let { accountSwapCurrency -> - AccountCryptoCurrencyStatus( - account = accountSwapCurrency.account, - status = accountSwapCurrency.cryptoCurrencyStatus, + val (selectedCurrency, selectedAccount) = run { + var selectedAccountCurrency = toAccountCurrencyStatus + + if (selectedAccountCurrency == null) { + val amountSwapCurrency = swapInteractor.getInitialCurrencyToSwap( + initialCryptoCurrency = initialCurrencyFrom, + state = state, + isReverseFromTo = isReverseFromTo, ) + + if (amountSwapCurrency != null) { + selectedAccountCurrency = AccountCryptoCurrencyStatus( + account = amountSwapCurrency.account, + status = amountSwapCurrency.cryptoCurrencyStatus, + ) + } } + selectedAccountCurrency?.status to selectedAccountCurrency?.account - } else { - val selectedCurrency = initialToStatus ?: swapInteractor.getInitialCurrencyToSwap( - initialCryptoCurrency = initialCurrencyFrom, - state = state, - isReverseFromTo = isReverseFromTo, - ) - selectedCurrency to null } applyInitialTokenChoice( @@ -695,19 +691,11 @@ internal class SwapModel @Inject constructor( private fun updateTokensState(tokenDataState: TokensDataStateExpress) { val tokensDataState = if (isOrderReversed) tokenDataState.fromGroup else tokenDataState.toGroup - uiState = if (accountsFeatureToggles.isFeatureEnabled) { - stateBuilder.addTokensToStateV2( - uiState = uiState, - tokensDataState = tokensDataState, - isAccountsMode = isAccountsMode, - ) - } else { - stateBuilder.addTokensToState( - uiState = uiState, - tokensDataState = tokensDataState, - fromToken = dataState.fromCryptoCurrency?.currency ?: initialCurrencyFrom, - ) - } + uiState = stateBuilder.addTokensToStateV2( + uiState = uiState, + tokensDataState = tokensDataState, + isAccountsMode = isAccountsMode, + ) latestMarketsState?.let(::applyMarketsState) } @@ -1366,11 +1354,7 @@ internal class SwapModel @Inject constructor( fromToken = foundToken fromAccount = foundAccount toToken = initialFromStatus - toAccount = if (accountsFeatureToggles.isFeatureEnabled) { - fromAccountCurrencyStatus?.account - } else { - null - } + toAccount = fromAccountCurrencyStatus?.account val newToken = fromToken.currency as? CryptoCurrency.Coin if (newToken != null) { @@ -1384,11 +1368,7 @@ internal class SwapModel @Inject constructor( } } else { fromToken = initialFromStatus - fromAccount = if (accountsFeatureToggles.isFeatureEnabled) { - fromAccountCurrencyStatus?.account - } else { - null - } + fromAccount = fromAccountCurrencyStatus?.account toToken = foundToken toAccount = foundAccount @@ -1446,26 +1426,16 @@ internal class SwapModel @Inject constructor( tokens: TokensDataStateExpress, id: String, ): Pair { - return if (accountsFeatureToggles.isFeatureEnabled) { - val accountCryptoCurrencyStatus = if (isOrderReversed) { - tokens.fromGroup - } else { - tokens.toGroup - }.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> - accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> - accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id - } - } - accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account + val accountCryptoCurrencyStatus = if (isOrderReversed) { + tokens.fromGroup } else { - if (isOrderReversed) { - tokens.fromGroup - } else { - tokens.toGroup - }.available.firstOrNull { swapAvailability -> - swapAvailability.currencyStatus.currency.id.value == id - }?.currencyStatus to null + tokens.toGroup + }.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> + accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> + accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id + } } + return accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -1476,79 +1446,44 @@ internal class SwapModel @Inject constructor( ) { Timber.d("Subscribe to ${coin.id} balance updates") - if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase( - userWalletId = userWalletId, - currency = coin, - ).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes - .onEach { (account, currencyStatus) -> - Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") + getAccountCurrencyStatusUseCase( + userWalletId = userWalletId, + currency = coin, + ).distinctUntilChanged { old, new -> old.status.value.amount == new.status.value.amount } // Check only balance changes + .onEach { (account, currencyStatus) -> + Timber.d("${coin.id} balance is ${currencyStatus.value.amount ?: "null"}") - if (isFromCurrency) { - dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = currencyStatus, - ).getOrNull() ?: currencyStatus, - ) - } - - uiState = when { - isFromCurrency && currencyStatus.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { - dataState = dataState.copy( - fromCryptoCurrency = currencyStatus, - fromAccount = account, - ) - stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus) - } - !isFromCurrency && currencyStatus.currency.id == dataState.toCryptoCurrency?.currency?.id -> { - dataState = dataState.copy( - toCryptoCurrency = currencyStatus, - toAccount = account, - ) - stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus) - } - else -> { - uiState - } - } - startLoadingQuotesFromLastState(isSilent = true) + if (isFromCurrency) { + dataState = dataState.copy( + feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( + userWalletId = userWalletId, + cryptoCurrencyStatus = currencyStatus, + ).getOrNull() ?: currencyStatus, + ) } - } else { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWallet( - userWalletId = userWalletId, - currencyId = coin.id, - isSingleWalletWithTokens = false, - ).mapNotNull { either -> (either as? Either.Right)?.value } - .distinctUntilChanged { old, new -> old.value.amount == new.value.amount } // Check only balance changes - .onEach { status -> - Timber.d("${coin.id} balance is ${status.value.amount ?: "null"}") - if (isFromCurrency) { + uiState = when { + isFromCurrency && currencyStatus.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { dataState = dataState.copy( - feePaidCryptoCurrency = getFeePaidCryptoCurrencyStatusSyncUseCase( - userWalletId = userWalletId, - cryptoCurrencyStatus = status, - ).getOrNull() ?: status, + fromCryptoCurrency = currencyStatus, + fromAccount = account, ) + stateBuilder.updateSendCurrencyBalance(uiState, currencyStatus) } - - uiState = when { - isFromCurrency && status.currency.id == dataState.fromCryptoCurrency?.currency?.id -> { - dataState = dataState.copy(fromCryptoCurrency = status) - stateBuilder.updateSendCurrencyBalance(uiState, status) - } - !isFromCurrency && status.currency.id == dataState.toCryptoCurrency?.currency?.id -> { - dataState = dataState.copy(toCryptoCurrency = status) - stateBuilder.updateReceiveCurrencyBalance(uiState, status) - } - else -> { - uiState - } + !isFromCurrency && currencyStatus.currency.id == dataState.toCryptoCurrency?.currency?.id -> { + dataState = dataState.copy( + toCryptoCurrency = currencyStatus, + toAccount = account, + ) + stateBuilder.updateReceiveCurrencyBalance(uiState, currencyStatus) + } + else -> { + uiState } - startLoadingQuotesFromLastState(isSilent = true) } - }.flowOn(dispatchers.main) + startLoadingQuotesFromLastState(isSilent = true) + } + .flowOn(dispatchers.main) .launchIn(modelScope) .saveIn(if (isFromCurrency) fromTokenBalanceJobHolder else toTokenBalanceJobHolder) } @@ -2027,18 +1962,12 @@ internal class SwapModel @Inject constructor( toToken.currency.id.value } - return if (accountsFeatureToggles.isFeatureEnabled) { - groupToFind.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) -> - currencyList.find { accountSwapCurrency -> - idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && - accountSwapCurrency.isAvailable - } - }?.providers - } else { - groupToFind.available.find { swapAvailability -> - idToFind == swapAvailability.currencyStatus.currency.id.value - }?.providers - } + return groupToFind.accountCurrencyList.firstNotNullOfOrNull { (_, currencyList) -> + currencyList.find { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable + } + }?.providers ?.filterForTangemPayWithdrawal() .orEmpty() } @@ -2082,16 +2011,10 @@ internal class SwapModel @Inject constructor( val group = if (isReverseFromTo) state.fromGroup else state.toGroup val idToFind = selectedCurrency.currency.id.value - return if (accountsFeatureToggles.isFeatureEnabled) { - group.accountCurrencyList.any { (_, currencyList) -> - currencyList.any { accountSwapCurrency -> - idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && - accountSwapCurrency.isAvailable - } - } - } else { - group.available.any { swapAvailability -> - idToFind == swapAvailability.currencyStatus.currency.id.value + return group.accountCurrencyList.any { (_, currencyList) -> + currencyList.any { accountSwapCurrency -> + idToFind == accountSwapCurrency.cryptoCurrencyStatus.currency.id.value && + accountSwapCurrency.isAvailable } } } @@ -2128,14 +2051,10 @@ internal class SwapModel @Inject constructor( val chosen = if (isOrderReversed) from else to - return if (accountsFeatureToggles.isFeatureEnabled) { - currenciesGroup.accountCurrencyList.flatMap { accountSwapAvailability -> - accountSwapAvailability.currencyList.map { accountSwapCurrency -> - accountSwapCurrency.cryptoCurrencyStatus - } + return currenciesGroup.accountCurrencyList.flatMap { accountSwapAvailability -> + accountSwapAvailability.currencyList.map { accountSwapCurrency -> + accountSwapCurrency.cryptoCurrencyStatus } - } else { - currenciesGroup.available.map { swapAvailability -> swapAvailability.currencyStatus } }.map { currencyStatus -> currencyStatus.currency }.contains(chosen.currency) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 3b0249e51b..3f8b76b556 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -9,6 +9,7 @@ import com.tangem.common.ui.bottomsheet.permission.state.* import com.tangem.common.ui.notifications.NotificationUM import com.tangem.common.ui.swapStoriesScreen.SwapStoriesFactory import com.tangem.common.ui.userwallet.ext.walletInterationIcon +import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.extensions.* @@ -20,13 +21,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.core.ui.HoldToConfirmButtonFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.promo.models.StoryContent import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNetwork import com.tangem.feature.swap.converters.TokensDataConverter -import com.tangem.feature.swap.converters.TokensDataConverterV2 import com.tangem.feature.swap.domain.models.ExpressDataError import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType @@ -62,7 +61,7 @@ internal class StateBuilder( private val appCurrencyProvider: Provider, private val isAccountsModeProvider: Provider, private val iGaslessFeeSupportedForNetwork: IsGaslessFeeSupportedForNetwork, - private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, + holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, ) { private val isHoldToConfirmEnabled: Boolean = @@ -70,13 +69,6 @@ internal class StateBuilder( private val iconStateConverter by lazy(::CryptoCurrencyToIconStateConverter) - private val tokensDataConverter = TokensDataConverter( - onSearchEntered = actions.onSearchEntered, - onTokenSelected = actions.onTokenSelected, - isBalanceHiddenProvider = isBalanceHiddenProvider, - appCurrencyProvider = appCurrencyProvider, - ) - private val notificationsFactory by lazy(LazyThreadSafetyMode.NONE) { SwapNotificationsFactory(actions, iGaslessFeeSupportedForNetwork) } @@ -677,28 +669,12 @@ internal class StateBuilder( ) } - fun addTokensToState( - uiState: SwapStateHolder, - fromToken: CryptoCurrency, - tokensDataState: CurrenciesGroup, - ): SwapStateHolder { - val currentMarketsState = uiState.selectTokenState?.marketsState - return uiState.copy( - selectTokenState = tokensDataConverter.convert( - value = CurrenciesGroupWithFromCurrency( - fromCurrency = fromToken, - group = tokensDataState, - ), - ).copy(marketsState = currentMarketsState), - ) - } - fun addTokensToStateV2( uiState: SwapStateHolder, tokensDataState: CurrenciesGroup, isAccountsMode: Boolean, ): SwapStateHolder { - return TokensDataConverterV2( + return TokensDataConverter( onSearchEntered = actions.onSearchEntered, onTokenSelected = actions.onTokenSelected, appCurrencyProvider = appCurrencyProvider, From 03691635a2dc7ce4928204a2a82db2a950ae25be Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 13:57:55 +0300 Subject: [PATCH 88/97] Updated on 2026-08-14 --- .../analytics/SendWithSwapAnalyticEvents.kt | 21 ++++++++++++++++ .../confirm/model/SendWithSwapConfirmModel.kt | 25 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt index 64e5650b5b..24a68d2945 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/analytics/SendWithSwapAnalyticEvents.kt @@ -40,6 +40,27 @@ internal sealed class SendWithSwapAnalyticEvents( }, ), AppsFlyerIncludedEvent + data class OnSendClick( + val providerName: String, + val feeType: AnalyticsParam.FeeType, + val fromToken: CryptoCurrency, + val toToken: CryptoCurrency, + val fromDerivationIndex: Int?, + val toDerivationIndex: Int?, + ) : SendWithSwapAnalyticEvents( + event = "Button - Send with Swap", + params = buildMap { + put(PROVIDER, providerName) + put(FEE_TYPE, if (feeType is AnalyticsParam.FeeType.Normal) "Market" else "Fast") + put(SEND_TOKEN, fromToken.symbol) + put(RECEIVE_TOKEN, toToken.symbol) + put(SEND_BLOCKCHAIN, fromToken.network.name) + put(RECEIVE_BLOCKCHAIN, toToken.network.name) + if (fromDerivationIndex != null) put(ACCOUNT_DERIVATION_FROM, fromDerivationIndex.toString()) + if (toDerivationIndex != null) put(ACCOUNT_DERIVATION_TO, toDerivationIndex.toString()) + }, + ), AppsFlyerIncludedEvent + data class NoticeCanNotSwapToken( val fromToken: CryptoCurrency, val toTokenSymbol: String, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt index 271decfaa7..99fcf5c1e4 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/sendviaswap/confirm/model/SendWithSwapConfirmModel.kt @@ -305,6 +305,7 @@ internal class SendWithSwapConfirmModel @Inject constructor( modelScope.launch { uiState.transformerUpdate(SendWithSwapConfirmSendingStateTransformer(true)) val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended + modelScope.launch(dispatchers.default) { sendClickAnalytics() } swapTransactionSender.sendTransaction( feeExtended = feeExtended, confirmData = confirmData, @@ -494,6 +495,30 @@ internal class SendWithSwapConfirmModel @Inject constructor( ) } + private suspend fun sendClickAnalytics() { + val selectedProvider = confirmData.quote?.provider ?: return + val fromCurrency = confirmData.fromCryptoCurrencyStatus?.currency ?: return + val toCurrency = confirmData.toCryptoCurrencyStatus?.currency ?: return + val feeSelectorUM = uiState.value.feeSelectorUM as? FeeSelectorUM.Content ?: return + val feeType = feeSelectorUM.toAnalyticType() + val fromDerivationIndex = confirmData.fromAccount?.derivationIndex?.value + val destination = destinationUM?.addressTextField?.actualAddress ?: return + val destinationAccount = getAccountCurrencyByAddressUseCase(destination) + .getOrNull()?.account + val toDerivationIndex = destinationAccount?.derivationIndex?.value + + analyticsEventHandler.send( + SendWithSwapAnalyticEvents.OnSendClick( + providerName = selectedProvider.name, + feeType = feeType, + fromToken = fromCurrency, + toToken = toCurrency, + fromDerivationIndex = fromDerivationIndex, + toDerivationIndex = toDerivationIndex, + ), + ) + } + private fun getSelectedFeeToken(): CryptoCurrency { val feeUMV2 = uiState.value.feeSelectorUM as? FeeSelectorUMRedesigned.Content val feeExtended = feeUMV2?.feeExtraInfo?.transactionFeeExtended From e2b489f0b949acb490d331f1eff8d6ec47d0fb9c Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 14:49:04 +0400 Subject: [PATCH 89/97] Updated on 2026-08-14 --- .../data/onramp/DefaultHotCryptoRepository.kt | 13 +- .../tangem/data/onramp/di/OnrampDataModule.kt | 6 - .../di/WalletConnectDataModule.kt | 3 - .../pair/DefaultWcPairUseCase.kt | 12 +- .../sessions/DefaultWcSessionsManager.kt | 7 +- .../walletconnect/DefaultWcPairUseCaseTest.kt | 5 +- .../onramp/impl/detekt-baseline-debug.xml | 5 - .../hottokens/DefaultHotCryptoComponent.kt | 54 +++--- .../onramp/hottokens/model/HotCryptoModel.kt | 90 ++------- .../SetNoAvailablePairsTransformer.kt | 50 ++++- .../SetNoAvailablePairsTransformerV2.kt | 66 ------- .../model/AvailableSwapPairsModel.kt | 173 ++---------------- .../swap/model/SwapSelectTokensModel.kt | 26 +-- .../SetNothingToFoundStateTransformer.kt | 26 +-- .../SetNothingToFoundStateTransformerV2.kt | 31 ---- .../tokenlist/model/OnrampTokenListModel.kt | 128 +------------ .../impl/detekt-baseline-debug.xml | 3 - .../connections/components/WcPairComponent.kt | 6 +- .../connections/model/WcConnectionsModel.kt | 23 +-- .../connections/model/WcPairModel.kt | 54 ++---- 20 files changed, 144 insertions(+), 637 deletions(-) delete mode 100644 features/onramp/impl/detekt-baseline-debug.xml delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt delete mode 100644 features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt index 6501990863..735a95f01e 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultHotCryptoRepository.kt @@ -17,8 +17,6 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.account.toUserTokensResponse import com.tangem.datasource.appcurrency.AppCurrencyResponseStore import com.tangem.datasource.exchangeservice.hotcrypto.HotCryptoResponseStore -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.extensions.canHandleBlockchain import com.tangem.domain.card.common.extensions.canHandleToken import com.tangem.domain.common.wallets.UserWalletsListRepository @@ -43,7 +41,6 @@ import timber.log.Timber * * @property excludedBlockchains excluded blockchains * @property hotCryptoResponseStore store of `HotCryptoResponse` - * @property userWalletsStore store of `UserWallet` * @property tangemTechApi tangem tech api * @property appCurrencyResponseStore store of current app currency * @property dispatchers dispatchers @@ -59,8 +56,6 @@ internal class DefaultHotCryptoRepository( private val userWalletsListRepository: UserWalletsListRepository, private val tangemTechApi: TangemTechApi, private val appCurrencyResponseStore: AppCurrencyResponseStore, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val userTokensResponseStore: UserTokensResponseStore, private val walletAccountsFetcher: WalletAccountsFetcher, private val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, @@ -106,12 +101,8 @@ internal class DefaultHotCryptoRepository( private fun getWalletsWithTokensFlow(): Flow>> { return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets -> val flows = userWallets.map { userWallet -> - if (accountsFeatureToggles.isFeatureEnabled) { - walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() } - } else { - userTokensResponseStore.get(userWalletId = userWallet.walletId) - } - .map { userWallet to it?.tokens.orEmpty() } + walletAccountsFetcher.get(userWalletId = userWallet.walletId).map { it.toUserTokensResponse() } + .map { userWallet to it.tokens } } combine(flows) { it.toMap() } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 156a72544e..ca1ca2f6fc 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -27,8 +27,6 @@ import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore import com.tangem.datasource.local.onramp.sepa.OnrampCurrentCountryByIPStore import com.tangem.datasource.local.onramp.sepa.OnrampSepaAvailabilityStore import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.onramp.repositories.* import com.tangem.domain.walletmanager.WalletManagersFacade @@ -110,8 +108,6 @@ internal object OnrampDataModule { appCurrencyResponseStore: AppCurrencyResponseStore, dispatchers: CoroutineDispatcherProvider, analyticsEventHandler: AnalyticsEventHandler, - userTokensResponseStore: UserTokensResponseStore, - accountsFeatureToggles: AccountsFeatureToggles, walletAccountsFetcher: WalletAccountsFetcher, ): HotCryptoRepository { return DefaultHotCryptoRepository( @@ -122,8 +118,6 @@ internal object OnrampDataModule { appCurrencyResponseStore = appCurrencyResponseStore, dispatchers = dispatchers, analyticsEventHandler = analyticsEventHandler, - userTokensResponseStore = userTokensResponseStore, - accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, ) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 65749b77cb..3ae3557cc0 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -20,7 +20,6 @@ import com.tangem.data.walletconnect.utils.WcNetworksConverter import com.tangem.data.walletconnect.utils.WcScope import com.tangem.datasource.di.SdkMoshi import com.tangem.datasource.local.walletconnect.WalletConnectStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.supplier.SingleAccountSupplier @@ -95,7 +94,6 @@ internal object WalletConnectDataModule { getWallets: GetWalletsUseCase, wcNetworksConverter: WcNetworksConverter, analytics: AnalyticsEventHandler, - accountsFeatureToggles: AccountsFeatureToggles, wcScope: WcScope, ): DefaultWcSessionsManager { return DefaultWcSessionsManager( @@ -104,7 +102,6 @@ internal object WalletConnectDataModule { getWallets = getWallets, wcNetworksConverter = wcNetworksConverter, analytics = analytics, - accountsFeatureToggles = accountsFeatureToggles, scope = wcScope, ) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index f07bf2bd9d..432700dc15 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -10,7 +10,6 @@ import com.reown.walletkit.client.Wallet import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.WC_TAG import com.tangem.data.walletconnect.utils.getDappOriginUrl -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.* @@ -35,7 +34,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( private val sdkDelegate: WcPairSdkDelegate, private val blockAidVerifier: BlockAidVerifier, private val analytics: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, @Assisted private val pairRequest: WcPairRequest, ) : WcPairUseCase { @@ -128,7 +126,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( pendingSessionForSave = pendingSessionForSave, sessionForApprove = sessionForApprove, sdkSessionProposal = sdkSessionProposal, - ).map { settledSession -> + ).map { _ -> analytics.send( WcAnalyticEvents.DAppConnected( sessionProposal = proposalState.dAppSession, @@ -198,11 +196,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( verifyContext: Wallet.Model.VerifyContext, ): Either = runCatching { val proposalNetwork = associateNetworksDelegate.associate(sessionProposal) - val proposalAccountNetwork = if (accountsFeatureToggles.isFeatureEnabled) { - associateNetworksDelegate.associateAccounts(sessionProposal) - } else { - null - } + val proposalAccountNetwork = associateNetworksDelegate.associateAccounts(sessionProposal) val verificationInfo = when { verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE verifyContext.isScam == true -> CheckDAppResult.UNSAFE @@ -211,7 +205,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( CheckDAppResult.FAILED_TO_VERIFY } } - val requestedNetworks = (proposalAccountNetwork ?: proposalNetwork) + val requestedNetworks = proposalAccountNetwork .values.map { it.available.plus(it.required) }.flatten().toSet() analytics.send( WcAnalyticEvents.PairRequested( diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 8f37e4e4a1..7967e2d59f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -8,7 +8,6 @@ import com.reown.walletkit.client.WalletKit import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.* import com.tangem.datasource.local.walletconnect.WalletConnectStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet @@ -34,7 +33,6 @@ internal class DefaultWcSessionsManager( private val dispatchers: CoroutineDispatcherProvider, private val wcNetworksConverter: WcNetworksConverter, private val analytics: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, private val scope: WcScope, ) : WcSessionsManager, WcSdkObserver { @@ -57,7 +55,6 @@ internal class DefaultWcSessionsManager( .flowOn(dispatchers.io) private suspend fun migrateToAccountSession(inStore: Set): Boolean { - if (!accountsFeatureToggles.isFeatureEnabled) return false if (oneTimeMigration.value) return false var someMigrated = false @@ -118,9 +115,7 @@ internal class DefaultWcSessionsManager( val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) } as? Account.CryptoPortfolio - if (accountsFeatureToggles.isFeatureEnabled && account == null) { - return@mapNotNull null - } + ?: return@mapNotNull null val networks = wcNetworksConverter.findWalletNetworks(wallet, account, sdkSession) val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: "" WcSession( diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 6313e3ddf4..9d81be58d2 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -14,7 +14,6 @@ import com.tangem.data.walletconnect.pair.CaipNamespaceDelegate import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.utils.WcSdkSessionConverter -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.blockaid.BlockAidVerifier import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletconnect.model.WcPairError @@ -37,7 +36,6 @@ internal class DefaultWcPairUseCaseTest { private val analytics: AnalyticsEventHandler = mockk(relaxed = true) private val sdkDelegate: WcPairSdkDelegate = mockk() private val blockAidVerifier: BlockAidVerifier = mockk() - private val accountsFeatureToggles = mockk() private val url = "testUrl" private val source = WcPairRequest.Source.QR @@ -117,14 +115,13 @@ internal class DefaultWcPairUseCaseTest { sdkDelegate = sdkDelegate, blockAidVerifier = blockAidVerifier, analytics = analytics, - accountsFeatureToggles = accountsFeatureToggles, pairRequest = WcPairRequest(userWalletId = UserWalletId(""), uri = url, source = source), ) @Before fun setup() { coEvery { associateNetworksDelegate.associate(sdkProposal) } returns mapOf() - coEvery { accountsFeatureToggles.isFeatureEnabled } returns false + coEvery { associateNetworksDelegate.associateAccounts(sdkProposal) } returns mapOf() coEvery { caipNamespaceDelegate.associate( sessionProposal = sdkProposal, diff --git a/features/onramp/impl/detekt-baseline-debug.xml b/features/onramp/impl/detekt-baseline-debug.xml deleted file mode 100644 index ecf2e0cce8..0000000000 --- a/features/onramp/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/DefaultHotCryptoComponent.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/DefaultHotCryptoComponent.kt index feff4ba69b..1112152d60 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/DefaultHotCryptoComponent.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/DefaultHotCryptoComponent.kt @@ -31,7 +31,6 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.features.account.PortfolioSelectorComponent import com.tangem.features.onramp.hottokens.model.HotCryptoModel import com.tangem.features.onramp.hottokens.portfolio.OnrampAddToPortfolioComponent @@ -48,38 +47,29 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( @Assisted context: AppComponentContext, @Assisted private val params: HotCryptoComponent.Params, private val onrampAddToPortfolioComponentFactory: OnrampAddToPortfolioComponent.Factory, - private val accountsFeatureToggles: AccountsFeatureToggles, portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, addTokenComponentFactory: OnrampAddTokenComponent.Factory, ) : HotCryptoComponent, AppComponentContext by context { private val model: HotCryptoModel = getOrCreateModel(params) - private val portfolioSelectorComponent: PortfolioSelectorComponent? by lazy { - if (accountsFeatureToggles.isFeatureEnabled) { - portfolioSelectorComponentFactory.create( - context = child("portfolioSelectorComponent"), - params = PortfolioSelectorComponent.Params( - portfolioFetcher = requireNotNull(model.portfolioFetcher), - controller = model.portfolioSelectorController, - ), - ) - } else { - null - } + private val portfolioSelectorComponent: PortfolioSelectorComponent by lazy { + portfolioSelectorComponentFactory.create( + context = child("portfolioSelectorComponent"), + params = PortfolioSelectorComponent.Params( + portfolioFetcher = model.portfolioFetcher, + controller = model.portfolioSelectorController, + ), + ) } - private val addTokenComponent: OnrampAddTokenComponent? by lazy { - if (accountsFeatureToggles.isFeatureEnabled) { - addTokenComponentFactory.create( - context = child("addTokenComponent"), - params = OnrampAddTokenComponent.Params( - callbacks = model, - tokenToAdd = model.hotCryptoToAddDataFlow, - ), - ) - } else { - null - } + private val addTokenComponent: OnrampAddTokenComponent by lazy { + addTokenComponentFactory.create( + context = child("addTokenComponent"), + params = OnrampAddTokenComponent.Params( + callbacks = model, + tokenToAdd = model.hotCryptoToAddDataFlow, + ), + ) } private val bottomSheetSlot = childSlot( @@ -106,9 +96,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( HotCrypto(state, modifier) bottomSheet.child?.instance?.BottomSheet() - if (accountsFeatureToggles.isFeatureEnabled) { - AddHotCryptoBottomSheet() - } + AddHotCryptoBottomSheet() } @Composable @@ -133,7 +121,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( content = TangemBottomSheetConfigContent.Empty, ), containerColor = TangemTheme.colors.background.tertiary, - title = { state -> + title = { _ -> AnimatedContent(targetState = contentStack.value) { stack -> BottomSheetTitle( stack = stack, @@ -142,7 +130,7 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( ) } }, - content = { state -> + content = { _ -> AnimatedContent(targetState = contentStack.value) { stack -> val paddingModifier = Modifier.padding( start = 16.dp, @@ -221,8 +209,8 @@ internal class DefaultHotCryptoComponent @AssistedInject constructor( } private fun contentChild(config: OnrampAddTokenRoute): ComposableContentComponent = when (config) { - OnrampAddTokenRoute.AddToken -> requireNotNull(addTokenComponent) - OnrampAddTokenRoute.PortfolioSelector -> requireNotNull(portfolioSelectorComponent) + OnrampAddTokenRoute.AddToken -> addTokenComponent + OnrampAddTokenRoute.PortfolioSelector -> portfolioSelectorComponent OnrampAddTokenRoute.Empty -> ComposableContentComponent.EMPTY } diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt index fadde70778..bf19ec5d37 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/model/HotCryptoModel.kt @@ -1,9 +1,6 @@ package com.tangem.features.onramp.hottokens.model -import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation -import com.arkivanov.decompose.router.slot.activate -import com.arkivanov.decompose.router.slot.dismiss import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.popToFirst import com.arkivanov.decompose.router.stack.pushNew @@ -13,7 +10,6 @@ import com.tangem.blockchainsdk.utils.toCoinId 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.token.state.TokenItemState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.resourceReference import com.tangem.data.common.currency.getCoinId @@ -21,17 +17,11 @@ import com.tangem.data.common.currency.getTokenId import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.common.currency.isCustomToken import com.tangem.data.common.network.NetworkFactory -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles -import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase -import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase -import com.tangem.domain.appcurrency.model.AppCurrency 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.wallet.UserWallet -import com.tangem.domain.onramp.GetHotCryptoUseCase import com.tangem.domain.onramp.model.HotCryptoCurrency -import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorController import com.tangem.features.onramp.hottokens.HotCryptoComponent @@ -51,7 +41,6 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -59,9 +48,6 @@ import javax.inject.Inject * Hot crypto model * * @param paramsContainer params container - * @param getHotCryptoUseCase use case for getting hot crypto - * @param getSelectedAppCurrencyUseCase use case for getting selected app currency - * @property getCryptoCurrencyStatusSyncUseCase use case for getting crypto currency status by id * @property dispatchers dispatchers * [REDACTED_AUTHOR] @@ -70,22 +56,17 @@ import javax.inject.Inject @ModelScoped internal class HotCryptoModel @Inject constructor( paramsContainer: ParamsContainer, - private val getHotCryptoUseCase: GetHotCryptoUseCase, private val callbackDelegate: HotCryptoModelCallbackDelegate, - private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, - private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase, override val dispatchers: CoroutineDispatcherProvider, private val hotCryptoPortfolioDataLoader: HotCryptoPortfolioDataLoader, - private val accountsFeatureToggles: AccountsFeatureToggles, - private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, val portfolioSelectorController: PortfolioSelectorController, private val networkFactory: NetworkFactory, - private val portfolioFetcherFactory: PortfolioFetcher.Factory, + portfolioFetcherFactory: PortfolioFetcher.Factory, ) : Model(), OnrampAddTokenComponent.Callbacks by callbackDelegate { val bottomSheetNavigation: SlotNavigation = SlotNavigation() - val portfolioFetcher: PortfolioFetcher? + val portfolioFetcher: PortfolioFetcher val bottomSheetNavigationV2 = StackNavigation() private val addHotCryptoJob = JobHolder() val hotCryptoToAddDataFlow: MutableSharedFlow = MutableSharedFlow( @@ -99,53 +80,30 @@ internal class HotCryptoModel @Inject constructor( private val params: HotCryptoComponent.Params = paramsContainer.require() init { - if (accountsFeatureToggles.isFeatureEnabled) { - portfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), - scope = modelScope, - ) - combineData() - } else { - portfolioFetcher = null - combineDataOld() - } + portfolioFetcher = portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), + scope = modelScope, + ) + combineData() } private fun combineData() { - combine( - flow = hotCryptoPortfolioDataLoader.loadPortfolioData(params.userWalletId), - flow2 = isAccountsModeEnabledUseCase.invoke(), - transform = { data, isAccountMode -> + hotCryptoPortfolioDataLoader.loadPortfolioData(params.userWalletId) + .map { data -> HotTokenItemStateConverter( appCurrency = data.appCurrency, - onItemClick = { tokenItemState, hotCryptoCurrency -> + onItemClick = { _, hotCryptoCurrency -> startAddTokenFlow(currency = hotCryptoCurrency, hotCryptoPortfolioData = data) }, ) .convertList(data.allHotCrypto) .map(TokensListItemUM::Token) - }, - ) + } .onEach { items -> state.update { HotCryptoUM(items = it.buildItems(items)) } } .flowOn(dispatchers.default) .launchIn(modelScope) } - private fun combineDataOld() { - combine( - flow = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }, - flow2 = getHotCryptoUseCase(params.userWalletId), - ) { appCurrency, currencies -> - HotTokenItemStateConverter(appCurrency = appCurrency, onItemClick = ::onTokenClick) - .convertList(currencies) - .map(TokensListItemUM::Token) - } - .onEach { items -> - state.update { HotCryptoUM(items = it.buildItems(items)) } - } - .launchIn(modelScope) - } - private fun HotCryptoUM.buildItems(items: List): ImmutableList = buildList { if (items.isNotEmpty()) { @@ -163,16 +121,6 @@ internal class HotCryptoModel @Inject constructor( ) } - private fun onTokenClick(tokenItemState: TokenItemState, currency: HotCryptoCurrency) { - bottomSheetNavigation.activate( - configuration = OnrampAddToPortfolioBSConfig.AddToPortfolio( - cryptoCurrency = currency.cryptoCurrency, - currencyIconState = tokenItemState.iconState, - onSuccessAdding = ::onSuccessAdding, - ), - ) - } - private fun startAddTokenFlow(currency: HotCryptoCurrency, hotCryptoPortfolioData: HotCryptoPortfolioData) { hotCryptoToAddDataFlow.resetReplayCache() fun closeNavigationFlow() = bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.Empty) @@ -200,7 +148,7 @@ internal class HotCryptoModel @Inject constructor( bottomSheetNavigationV2.replaceAll(OnrampAddTokenRoute.PortfolioSelector) val tokenToAddStateFlow = portfolioSelectorController - .selectedAccountWithData(requireNotNull(portfolioFetcher)) + .selectedAccountWithData(portfolioFetcher) .filterNotNull() .map { (_, selectedAccount) -> val cryptoCurrency = updateCryptoCurrency( @@ -246,20 +194,6 @@ internal class HotCryptoModel @Inject constructor( .saveIn(addHotCryptoJob) } - private fun onSuccessAdding(id: CryptoCurrency.ID) { - modelScope.launch { - getSingleCryptoCurrencyStatusUseCase.invokeMultiWalletSync( - userWalletId = params.userWalletId, - cryptoCurrencyId = id, - ) - .onRight { status -> - bottomSheetNavigation.dismiss() - params.onTokenClick(status) - } - .onLeft { Timber.d("Unable to get CryptoCurrencyStatus[$id]: $it") } - } - } - private fun setupPortfolioSelector(hotCrypto: HotCryptoCurrency, hotCryptoPortfolioData: HotCryptoPortfolioData) { portfolioSelectorController.selectAccount(null) portfolioSelectorController.isEnabled.value = isEnabled@{ _, accountStatus -> diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt index d46f06637e..08f93f0bd0 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformer.kt @@ -1,34 +1,64 @@ package com.tangem.features.onramp.swap.availablepairs.entity.transformers +import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter +import com.tangem.common.ui.account.TokensListPortfolioItemConverter 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 com.tangem.features.onramp.tokenlist.entity.utils.addHeader import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList -/** -[REDACTED_AUTHOR] - */ internal class SetNoAvailablePairsTransformer( private val appCurrency: AppCurrency, - private val unavailableStatuses: List, + private val accountList: Map>, private val isBalanceHidden: Boolean, - private val unavailableTokensHeaderReference: TextReference, + private val isAccountsMode: Boolean, + private val unavailableErrorText: TextReference, ) : TokenListUMTransformer { + private val unavailableConverter = OnrampTokenItemStateConverterFactory + .createUnavailableItemConverterV2(appCurrency = appCurrency, unavailableErrorText = unavailableErrorText) override fun transform(prevState: TokenListUM): TokenListUM { - val unavailableItems = OnrampTokenItemStateConverterFactory.createUnavailableItemConverter(appCurrency) - .convertList(unavailableStatuses) - .map(TokensListItemUM::Token) + val totalTokensCount = accountList.values.sumOf { it.size } return prevState.copy( availableItems = persistentListOf(), - unavailableItems = unavailableItems.addHeader(textReference = unavailableTokensHeaderReference), + unavailableItems = persistentListOf(), + tokensListData = if (isAccountsMode) { + TokenListUMData.AccountList( + tokensList = accountList.map { (account, cryptoCurrencies) -> + TokensListPortfolioItemConverter( + tokenItemUM = AccountCryptoPortfolioItemStateConverter( + appCurrency = appCurrency, + account = account, + onItemClick = null, + ).convert(TotalFiatBalance.Failed), + isExpanded = true, + isCollapsable = false, + tokens = unavailableConverter.convertList(cryptoCurrencies) + .map(TokensListItemUM::Token) + .toPersistentList(), + ).convert(Unit) + }.toPersistentList(), + totalTokensCount = totalTokensCount, + ) + } else { + TokenListUMData.TokenList( + tokensList = accountList.flatMap { (_, cryptoCurrencies) -> + unavailableConverter.convertList(cryptoCurrencies) + .map(TokensListItemUM::Token) + }.toPersistentList(), + totalTokensCount = totalTokensCount, + ) + }, isBalanceHidden = isBalanceHidden, warning = NotificationUM.Warning.SwapNoAvailablePair, ) 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 deleted file mode 100644 index eba2f260ee..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/swap/availablepairs/entity/transformers/SetNoAvailablePairsTransformerV2.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.features.onramp.swap.availablepairs.entity.transformers - -import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter -import com.tangem.common.ui.account.TokensListPortfolioItemConverter -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 { - val totalTokensCount = accountList.values.sumOf { it.size } - - return prevState.copy( - availableItems = persistentListOf(), - unavailableItems = persistentListOf(), - tokensListData = if (isAccountsMode) { - TokenListUMData.AccountList( - tokensList = accountList.map { (account, cryptoCurrencies) -> - TokensListPortfolioItemConverter( - tokenItemUM = AccountCryptoPortfolioItemStateConverter( - appCurrency = appCurrency, - account = account, - onItemClick = null, - ).convert(TotalFiatBalance.Failed), - isExpanded = true, - isCollapsable = false, - tokens = unavailableConverter.convertList(cryptoCurrencies) - .map(TokensListItemUM::Token) - .toPersistentList(), - ).convert(Unit) - }.toPersistentList(), - totalTokensCount = totalTokensCount, - ) - } else { - TokenListUMData.TokenList( - tokensList = accountList.flatMap { (_, cryptoCurrencies) -> - unavailableConverter.convertList(cryptoCurrencies) - .map(TokensListItemUM::Token) - }.toPersistentList(), - totalTokensCount = totalTokensCount, - ) - }, - 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 5457e2c0fe..f3b60a2dcd 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 @@ -13,10 +13,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference -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.status.usecase.GetAccountCurrencyStatusUseCase @@ -26,7 +23,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.card.common.extensions.hotWalletExcludedBlockchains import com.tangem.domain.core.lce.Lce -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 @@ -41,7 +37,6 @@ 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.tokens.GetTokenListUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.domain.GetAvailablePairsUseCase import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo @@ -51,9 +46,7 @@ import com.tangem.features.feed.components.market.details.portfolio.add.AddToPor import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent 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.availablepairs.market.SwapMarketsListBatchFlowManager import com.tangem.features.onramp.swap.availablepairs.market.state.SwapMarketState import com.tangem.features.onramp.swap.entity.AccountAvailabilityUM @@ -61,7 +54,9 @@ 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.* +import com.tangem.features.onramp.tokenlist.entity.transformer.SetLoadingAccountTokenListTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateAccountTokenListTransformer import com.tangem.features.onramp.tokenlist.entity.utils.OnrampTokenItemStateConverterFactory import com.tangem.features.onramp.utils.ClearSearchBarTransformer import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer @@ -87,7 +82,6 @@ internal class AvailableSwapPairsModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val analyticsEventHandler: AnalyticsEventHandler, - private val getTokenListUseCase: GetTokenListUseCase, private val tokenListUMController: TokenListUMController, private val searchManager: InputManager, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, @@ -95,7 +89,6 @@ internal class AvailableSwapPairsModel @Inject constructor( private val getAvailablePairsUseCase: GetAvailablePairsUseCase, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, private val getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, private val excludedBlockchains: ExcludedBlockchains, @@ -120,7 +113,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } private val addToPortfolioJobHolder = JobHolder() - private val tokenListFlow = getTokenListUseCaseFlow() private val accountListFlow = getAccountListUseCaseFlow() private val availablePairsByNetworkFlow = MutableStateFlow>(emptyMap()) @@ -157,11 +149,7 @@ internal class AvailableSwapPairsModel @Inject constructor( private val visibleDefaultMarketItemIds = MutableStateFlow>(emptyList()) init { - if (accountsFeatureToggles.isFeatureEnabled) { - subscribeOnUpdateStateV2() - } else { - subscribeOnUpdateState() - } + subscribeOnUpdateState() initializeSearchBarCallbacks() subscribeOnSelectedStatusChange() @@ -173,18 +161,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } } - private fun getTokenListUseCaseFlow(): SharedFlow> { - return getTokenListUseCase.launch(userWalletId = params.userWalletId) - .distinctUntilChanged() - .map { maybeTokenList -> - maybeTokenList.getOrElse( - ifLoading = { it ?: TokenList.Empty }, - ifError = { TokenList.Empty }, - ).flattenCurrencies() - } - .shareIn(scope = modelScope, started = SharingStarted.Eagerly, replay = 1) - } - private fun getAccountListUseCaseFlow(): SharedFlow> { return singleAccountStatusListSupplier(SingleAccountStatusListProducer.Params(params.userWalletId)) .distinctUntilChanged() @@ -214,42 +190,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } private fun subscribeOnUpdateState() { - combine( - flow = tokenListFlow, - flow2 = getAppCurrencyAndBalanceHidingFlow(), - flow3 = params.selectedStatus, - flow4 = searchManager.query, - flow5 = availablePairsByNetworkFlow - .map { it[params.selectedStatus.value?.toLeastTokenInfo()] } - .distinctUntilChanged(), - ) { currencies, appCurrencyAndBalanceHiding, selectedStatus, query, availablePairsState -> - availablePairsState?.fold( - ifLoading = { SetLoadingTokenItemsTransformer(currencies) }, - ifContent = { pairs -> - handleContentState( - appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding, - currencies = currencies, - selectedStatus = selectedStatus, - query = query, - availablePairs = pairs, - ) - }, - ifError = { throwable -> - handleErrorState( - cause = throwable, - networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), - currencies = currencies, - ) - }, - ) - ?: SetLoadingTokenItemsTransformer(currencies) - } - .onEach(tokenListUMController::update) - .flowOn(dispatchers.main) - .launchIn(modelScope) - } - - private fun subscribeOnUpdateStateV2() { combine( flow = getAccountsAndModeFlow(), flow2 = getAppCurrencyAndBalanceHidingFlow(), @@ -269,7 +209,7 @@ internal class AvailableSwapPairsModel @Inject constructor( ) }, ifContent = { pairs -> - handleContentStateV2( + handleContentState( appCurrencyAndBalanceHiding = appCurrencyAndBalanceHiding, accountList = accountList, selectedStatus = selectedStatus, @@ -279,7 +219,7 @@ internal class AvailableSwapPairsModel @Inject constructor( ) }, ifError = { throwable -> - handleErrorStateV2( + handleErrorState( cause = throwable, networkInfo = params.selectedStatus.value?.toLeastTokenInfo(), accountList = accountList, @@ -297,52 +237,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } private fun handleContentState( - appCurrencyAndBalanceHiding: Pair, - currencies: List, - selectedStatus: CryptoCurrencyStatus?, - query: String, - availablePairs: List, - ): TokenListUMTransformer { - val (appCurrency, isBalanceHidden) = appCurrencyAndBalanceHiding - - if (availablePairs.isEmpty()) { - return SetNoAvailablePairsTransformer( - appCurrency = appCurrency, - unavailableStatuses = currencies, - isBalanceHidden = isBalanceHidden, - unavailableTokensHeaderReference = resourceReference( - id = R.string.tokens_list_unavailable_to_swap_header, - wrappedList(selectedStatus?.currency?.name?.capitalize().orEmpty()), - ), - ) - } - - val filterByQueryTokenList = currencies - .filter { it.currency != selectedStatus?.currency } - .filterByQuery(query = query) - - return if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) { - SetNothingToFoundStateTransformer( - isBalanceHidden = isBalanceHidden, - emptySearchMessageReference = resourceReference( - id = R.string.action_buttons_swap_empty_search_message, - ), - ) - } else { - UpdateTokenItemsTransformer( - appCurrency = appCurrency, - onItemClick = ::onPortfolioTokenClick, - statuses = filterByQueryTokenList.filterByAvailability(availablePairs = availablePairs), - isBalanceHidden = isBalanceHidden, - unavailableTokensHeaderReference = resourceReference( - id = R.string.tokens_list_unavailable_to_swap_header, - wrappedList(selectedStatus?.currency?.name?.capitalize().orEmpty()), - ), - ) - } - } - - private fun handleContentStateV2( appCurrencyAndBalanceHiding: Pair, accountList: List, selectedStatus: CryptoCurrencyStatus?, @@ -371,7 +265,7 @@ internal class AvailableSwapPairsModel @Inject constructor( .filterValues { it.isNotEmpty() } if (availablePairs.isEmpty()) { - return SetNoAvailablePairsTransformerV2( + return SetNoAvailablePairsTransformer( appCurrency = appCurrency, accountList = filterByQueryAccountList, unavailableErrorText = resourceReference(R.string.tokens_list_unavailable_to_swap_source_header), @@ -381,7 +275,7 @@ internal class AvailableSwapPairsModel @Inject constructor( } return if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) { - SetNothingToFoundStateTransformerV2( + SetNothingToFoundStateTransformer( isBalanceHidden = isBalanceHidden, emptySearchMessageReference = resourceReference( id = R.string.action_buttons_swap_empty_search_message, @@ -400,23 +294,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } private fun handleErrorState( - cause: Throwable, - networkInfo: LeastTokenInfo?, - currencies: List, - ): SetErrorWarningTransformer { - return SetErrorWarningTransformer( - cause = cause, - onRefresh = { - modelScope.launch { - if (networkInfo != null) { - updateAvailablePairs(networkInfo, currencies) - } - } - }, - ) - } - - private fun handleErrorStateV2( cause: Throwable, networkInfo: LeastTokenInfo?, accountList: List, @@ -450,19 +327,14 @@ internal class AvailableSwapPairsModel @Inject constructor( val isAlreadyLoaded = availablePairsByNetworkFlow.value[networkInfo]?.isContent() == true if (isAlreadyLoaded) return@collectLatest - if (accountsFeatureToggles.isFeatureEnabled) { - val accountList = accountListFlow.firstOrNull() ?: return@collectLatest - updateAvailablePairs( - networkInfo = networkInfo, - statuses = accountList.filterCryptoPortfolio() - .flatMap { accountStatus -> - accountStatus.flattenCurrencies() - }.toSet().toList(), - ) - } else { - val statuses = tokenListFlow.firstOrNull() ?: return@collectLatest - updateAvailablePairs(networkInfo = networkInfo, statuses = statuses) - } + val accountList = accountListFlow.firstOrNull() ?: return@collectLatest + updateAvailablePairs( + networkInfo = networkInfo, + statuses = accountList.filterCryptoPortfolio() + .flatMap { accountStatus -> + accountStatus.flattenCurrencies() + }.toSet().toList(), + ) } } } @@ -540,19 +412,6 @@ internal class AvailableSwapPairsModel @Inject constructor( } } - private fun List.filterByAvailability( - availablePairs: List, - ): Map> { - return groupBy { status -> - val isAvailable = availablePairs.map(SwapPairLeast::to).contains(status.toLeastTokenInfo()) - - isAvailable && - status.value !is CryptoCurrencyStatus.MissedDerivation && - status.value !is CryptoCurrencyStatus.Unreachable && - !status.currency.isCustom - } - } - private fun Map>.filterByAvailability( availablePairs: List, ): List { 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 49a9b6d86d..dcf23013a0 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,6 @@ 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.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase @@ -37,7 +36,6 @@ internal class SwapSelectTokensModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val getAccountCurrencyStatusUseCase: GetAccountCurrencyStatusUseCase, - private val accountsFeatureToggles: AccountsFeatureToggles, ) : Model() { val state: StateFlow = controller.state @@ -77,14 +75,10 @@ internal class SwapSelectTokensModel @Inject constructor( selectedTokenItemState = selectedTokenItemState, onRemoveClick = ::onRemoveFromTokenClick, isAccountsMode = isAccountsMode, - account = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = params.userWalletId, - currency = status.currency, - ).getOrNull()?.account - } else { - null - }, + account = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = params.userWalletId, + currency = status.currency, + ).getOrNull()?.account, ), ) } @@ -108,14 +102,10 @@ internal class SwapSelectTokensModel @Inject constructor( transformer = SelectToTokenTransformer( selectedTokenItemState = selectedTokenItemState, isAccountsMode = isAccountsMode, - account = if (accountsFeatureToggles.isFeatureEnabled) { - getAccountCurrencyStatusUseCase.invokeSync( - userWalletId = params.userWalletId, - currency = status.currency, - ).getOrNull()?.account - } else { - null - }, + account = getAccountCurrencyStatusUseCase.invokeSync( + userWalletId = params.userWalletId, + currency = status.currency, + ).getOrNull()?.account, ), ) 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 1b388e9a47..ad495bf4ae 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 @@ -2,8 +2,6 @@ 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.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 @@ -16,22 +14,18 @@ internal class SetNothingToFoundStateTransformer( override fun transform(prevState: TokenListUM): TokenListUM { return prevState.copy( - availableItems = persistentListOf( - createGroupTitle( - textReference = resourceReference(id = R.string.exchange_tokens_available_tokens_header), - ), - TokensListItemUM.Text( - id = emptySearchMessageReference.hashCode(), - text = emptySearchMessageReference, - ), - ), + availableItems = persistentListOf(), unavailableItems = persistentListOf(), - tokensListData = TokenListUMData.EmptyList, + tokensListData = TokenListUMData.TokenList( + tokensList = persistentListOf( + TokensListItemUM.Text( + id = emptySearchMessageReference.hashCode(), + text = emptySearchMessageReference, + ), + ), + totalTokensCount = 0, + ), isBalanceHidden = isBalanceHidden, ) } - - private fun createGroupTitle(textReference: TextReference): TokensListItemUM.GroupTitle { - return TokensListItemUM.GroupTitle(id = textReference.hashCode(), text = textReference) - } } \ No newline at end of file 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 deleted file mode 100644 index 422e445dff..0000000000 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/tokenlist/entity/transformer/SetNothingToFoundStateTransformerV2.kt +++ /dev/null @@ -1,31 +0,0 @@ -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 - -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 = persistentListOf( - TokensListItemUM.Text( - id = emptySearchMessageReference.hashCode(), - text = emptySearchMessageReference, - ), - ), - totalTokensCount = 0, - ), - isBalanceHidden = isBalanceHidden, - ) - } -} \ 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 9d1a10ff99..ddc4c9751a 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 @@ -8,7 +8,6 @@ import com.tangem.core.ui.components.fields.InputManager import com.tangem.core.ui.components.token.state.TokenItemState import com.tangem.core.ui.extensions.TextReference 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 @@ -16,19 +15,14 @@ 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 -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 import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.domain.tokens.GetAssetRequirementsUseCase -import com.tangem.domain.tokens.GetTokenListUseCase -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 @@ -36,7 +30,9 @@ 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.* -import com.tangem.features.onramp.tokenlist.entity.transformer.* +import com.tangem.features.onramp.tokenlist.entity.transformer.SetLoadingAccountTokenListTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.SetNothingToFoundStateTransformer +import com.tangem.features.onramp.tokenlist.entity.transformer.UpdateAccountTokenListTransformer import com.tangem.features.onramp.utils.ClearSearchBarTransformer import com.tangem.features.onramp.utils.UpdateSearchBarActiveStateTransformer import com.tangem.features.onramp.utils.UpdateSearchBarCallbacksTransformer @@ -60,12 +56,10 @@ internal class OnrampTokenListModel @Inject constructor( private val searchManager: InputManager, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, - private val getTokenListUseCase: GetTokenListUseCase, private val getWalletsUseCase: GetWalletsUseCase, 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() { @@ -84,68 +78,10 @@ internal class OnrampTokenListModel @Inject constructor( onActiveChange = ::onSearchBarActiveChange, ), ) - if (accountsFeatureToggles.isFeatureEnabled) { - subscribeOnUpdateStateV2() - } else { - subscribeOnUpdateState() - } + subscribeOnUpdateState() } private fun subscribeOnUpdateState() { - combine( - flow = getTokenListUseCase.launch(userWalletId = params.userWalletId).distinctUntilChanged(), - flow2 = getSelectedAppCurrencyUseCase().map { it.getOrElse { AppCurrency.Default } }.distinctUntilChanged(), - flow3 = getBalanceHidingSettingsUseCase().map { it.isBalanceHidden }.distinctUntilChanged(), - flow4 = searchManager.query, - flow5 = hasRestrictionForSellFlow(), - ) { maybeTokenList, appCurrency, isBalanceHidden, query, hasRestrictionForSell -> - val currencies = maybeTokenList.getOrElse( - ifLoading = { it ?: TokenList.Empty }, - ifError = { TokenList.Empty }, - ) - .flattenCurrencies() - - val filterByQueryTokenList = currencies - .filterByQuery(query = query) - - if (query.isNotEmpty() && filterByQueryTokenList.isEmpty()) { - SetNothingToFoundStateTransformer( - isBalanceHidden = isBalanceHidden, - emptySearchMessageReference = getEmptySearchMessageReference(), - ) - } else { - val isInsufficientBalanceForSell = if (params.filterOperation == OnrampOperation.SELL) { - maybeTokenList.isInsufficientBalanceForSell() - } else { - false - } - - UpdateTokenItemsTransformer( - appCurrency = appCurrency, - onItemClick = ::onTokenClick, - statuses = filterByQueryTokenList.let { statuses -> - if (hasRestrictionForSell || isInsufficientBalanceForSell) { - mapOf(false to statuses) - } else { - statuses.filterByAvailability() - } - }, - isBalanceHidden = isBalanceHidden, - unavailableTokensHeaderReference = getUnavailableTokensHeaderReference(), - warning = when { - hasRestrictionForSell -> NotificationUM.Warning.SellingRegionalRestriction - isInsufficientBalanceForSell -> NotificationUM.Warning.InsufficientBalanceForSelling - else -> null - }, - ) - } - } - .onEach(::updateTokenListUM) - .flowOn(dispatchers.main) - .launchIn(modelScope) - } - - private fun subscribeOnUpdateStateV2() { combine( flow = singleAccountStatusListSupplier( SingleAccountStatusListProducer.Params(params.userWalletId), @@ -160,7 +96,7 @@ internal class OnrampTokenListModel @Inject constructor( if (query.isNotEmpty() && filterByQueryAccountList.isEmpty()) { updateTokenListUM( - SetNothingToFoundStateTransformerV2( + SetNothingToFoundStateTransformer( isBalanceHidden = isBalanceHidden, emptySearchMessageReference = getEmptySearchMessageReference(), ), @@ -211,16 +147,6 @@ internal class OnrampTokenListModel @Inject constructor( } } - private fun Lce.isInsufficientBalanceForSell(): Boolean { - return if (params.filterOperation == OnrampOperation.SELL) { - isContent { - (it.totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true - } - } else { - false - } - } - private fun AccountStatusList.isInsufficientBalanceForSell(): Boolean { return if (params.filterOperation == OnrampOperation.SELL) { (totalFiatBalance as? TotalFiatBalance.Loaded)?.amount?.isZero() == true @@ -268,13 +194,8 @@ internal class OnrampTokenListModel @Inject constructor( } private fun isFirstInitialization(prevState: TokenListUM, newState: TokenListUM): Boolean { - 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()) - } + return prevState.tokensListData == TokenListUMData.EmptyList && + newState.tokensListData != TokenListUMData.EmptyList } private fun onTokenClick(tokenItemState: TokenItemState, status: CryptoCurrencyStatus) { @@ -333,41 +254,6 @@ internal class OnrampTokenListModel @Inject constructor( } } - private suspend fun List.filterByAvailability(): Map> { - return coroutineScope { - map { status -> - async { - 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 - } - } - - status to (isOperationAvailable && isNotMissedDerivation && isNotLoading && isAvailable) - } - } - .awaitAll() - .groupBy(Pair::second) - .mapValues { it.value.map(Pair::first) } - } - } - private suspend fun AccountCryptoList.filterByAvailability(): List { return coroutineScope { map { (account, currencies) -> diff --git a/features/walletconnect/impl/detekt-baseline-debug.xml b/features/walletconnect/impl/detekt-baseline-debug.xml index 017f85e30e..2d5009d5c4 100644 --- a/features/walletconnect/impl/detekt-baseline-debug.xml +++ b/features/walletconnect/impl/detekt-baseline-debug.xml @@ -27,12 +27,10 @@ MultilineLambdaItParameter:WcSwitchNetworkModel.kt$WcSwitchNetworkModel${ if (it.isExistInWcSession) { router.pop() } else { showErrorDialog(HandleMethodError.RequiredNetwork(it.network.name)) } } NamedArguments:WcAddEthereumChainModalBottomSheetContent.kt$WcAddEthereumChainModalBottomSheetContent(state, {}, {}, {}) NamedArguments:WcAlertsFactory.kt$WcAlertsFactory$createMaliciousDAppAlert(alertType.description, alertType.onClick, alertType.iconType, alertType.iconBgType) - NamedArguments:WcPairModel.kt$WcPairModel$handlePairState( pairState, portfolios, selected, isAccountMode, ) NamedArguments:WcSendTransactionModel.kt$WcSendTransactionModel$buildUiState(securityCheck, useCase, signState, isApprovalMethod) NestedScopeFunctions:WcSendAndReceiveBlockAidUiConverter.kt$WcSendAndReceiveBlockAidUiConverter$let { spendAllowanceUMConverter.convert( WcSpendAllowanceUMConverter.Input( approvedAmount = it, onLearnMoreClick = value.onApproveLearnMoreClick, ), ) } NoNameShadowing:WcNavigationUtils.kt$model NullCheckOnMutableProperty:WcCommonTransactionComponentDelegate.kt$WcCommonTransactionComponentDelegate$if (contentStack != null) { val content by contentStack!!.subscribeAsState() BackHandler(onBack = ::onChildBack) content.active.instance.BottomSheet() } - NullableBooleanCheck:WcPairModel.kt$WcPairModel$isAccountMode ?: false NullableToStringCall:WcEstimatedWalletChangeUMConverter.kt$WcEstimatedWalletChangeUMConverter$${value.sign} ReusedModifierInstance:DefaultWalletConnectEntryComponent.kt$DefaultWalletConnectEntryComponent$Content(modifier = modifier) ReusedModifierInstance:WcAppInfoBS.kt$Box( modifier = modifier .padding(start = 48.dp) .border( width = 2.dp, color = TangemTheme.colors.background.action, shape = CircleShape, ) .padding(2.dp) .background(color = TangemTheme.colors.background.action) .size(20.dp) .clip(CircleShape) .background(color = TangemTheme.colors.icon.primary1.copy(alpha = 0.1F)), ) { Text( modifier = Modifier.align(Alignment.Center), text = "+$remainingCount", style = TangemTheme.typography.overline, color = TangemTheme.colors.text.secondary, ) } @@ -46,7 +44,6 @@ UnsafeCallOnNullableType:WcPairComponent.kt$WcPairComponent$model.portfolioFetcher!! UnsafeCallOnNullableType:WcSignTransactionComponent.kt$WcSignTransactionComponent$content!! UnsafeCallOnNullableType:WcTransactionRequestInfoComponent.kt$WcTransactionRequestInfoComponent$content!! - UseEmptyCounterpart:WcPairModel.kt$WcPairModel$setOf<Network>() UseOrEmpty:WcSpendAllowanceUMConverter.kt$WcSpendAllowanceUMConverter$value.approvedAmount.amount?.currencySymbol ?: "" UseOrEmpty:WcTransactionCheckErrorItem.kt$notification.text?.resolveReference() ?: "" diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index 39193ee405..220f294b49 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -15,11 +15,9 @@ import com.tangem.core.decompose.navigation.inner.InnerRouter import com.tangem.core.ui.components.bottomsheets.message.MessageBottomSheetUMV2 import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletconnect.model.WcPairRequest import com.tangem.features.account.PortfolioSelectorComponent -import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent.* -import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.* import com.tangem.features.walletconnect.connections.model.WcPairModel import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes.Alert @@ -115,7 +113,7 @@ internal class WcPairComponent( WcAppInfoRoutes.PortfolioSelector -> portfolioSelectorComponentFactory.create( context = appComponentContext, params = PortfolioSelectorComponent.Params( - portfolioFetcher = model.portfolioFetcher!!, + portfolioFetcher = model.portfolioFetcher, bsCallback = model.portfolioSelectorCallback, controller = model.selectorController, ), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt index 57298f9b5d..d82ef65634 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcConnectionsModel.kt @@ -18,7 +18,6 @@ import com.tangem.core.ui.message.DialogMessage import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.res.TangemTheme -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.qrscanning.models.QrResultSource @@ -55,7 +54,6 @@ internal class WcConnectionsModel @Inject constructor( private val wcDisconnectUseCase: WcDisconnectUseCase, private val multiAccountListSupplier: MultiAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - accountsFeatureToggles: AccountsFeatureToggles, private val wcPairService: WcPairService, override val dispatchers: CoroutineDispatcherProvider, analytics: AnalyticsEventHandler, @@ -70,11 +68,7 @@ internal class WcConnectionsModel @Inject constructor( init { analytics.send(WcAnalyticEvents.ScreenOpened()) listenQrUpdates() - if (accountsFeatureToggles.isFeatureEnabled) { - listenWcSessions() - } else { - listenWcSessionsOld() - } + listenWcSessions() } private fun listenQrUpdates() { @@ -98,21 +92,6 @@ internal class WcConnectionsModel @Inject constructor( .launchIn(modelScope) } - private fun listenWcSessionsOld() { - wcSessionsUseCase.invoke() - .conflate() - .distinctUntilChanged() - .onEach { sessionsMap -> - uiState.update( - WcSessionsTransformer( - sessionsMap = sessionsMap, - openAppInfoModal = ::openAppInfoModal, - ), - ) - } - .launchIn(modelScope) - } - private fun listenWcSessions() { combine( flow = wcSessionsUseCase.invoke().distinctUntilChanged(), diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index d686883940..d59a81f60d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -21,7 +21,6 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase @@ -75,7 +74,6 @@ internal class WcPairModel @Inject constructor( private val messageSender: UiMessageSender, override val dispatchers: CoroutineDispatcherProvider, private val analytics: AnalyticsEventHandler, - private val accountsFeatureToggles: AccountsFeatureToggles, val selectorController: PortfolioSelectorController, private val singleAccountListSupplier: SingleAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, @@ -95,7 +93,10 @@ internal class WcPairModel @Inject constructor( ) val stackNavigation = StackNavigation() - val portfolioFetcher: PortfolioFetcher? + val portfolioFetcher: PortfolioFetcher = portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), + scope = modelScope, + ) val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback { override val onDismiss: () -> Unit = { stackNavigation.pop() } override val onBack: () -> Unit = { stackNavigation.pop() } @@ -110,32 +111,23 @@ internal class WcPairModel @Inject constructor( ) private var proposalNetwork by Delegates.notNull() private var sessionProposal by Delegates.notNull() - private var additionallyEnabledNetworks = setOf() + private var additionallyEnabledNetworks = emptySet() private val dAppVerifiedStateConverter = WcDAppVerifiedStateConverter(onVerifiedClick = ::showVerifiedAlert) val appInfoUiState: StateFlow field = MutableStateFlow(createLoadingState()) init { - if (accountsFeatureToggles.isFeatureEnabled) { - portfolioFetcher = portfolioFetcherFactory.create( - mode = PortfolioFetcher.Mode.All(isOnlyMultiCurrency = true), - scope = modelScope, - ) - modelScope.launch { - val params = SingleAccountListProducer.Params(params.userWalletId) - val accountList = singleAccountListSupplier.getSyncOrNull(params) - if (accountList == null) { - router.pop() - return@launch - } - val firstAccount = accountList.accounts.first() - selectorController.selectAccount(firstAccount.accountId) - combineFlows(portfolioFetcher) + modelScope.launch { + val params = SingleAccountListProducer.Params(params.userWalletId) + val accountList = singleAccountListSupplier.getSyncOrNull(params) + if (accountList == null) { + router.pop() + return@launch } - } else { - portfolioFetcher = null - loadDAppInfo() + val firstAccount = accountList.accounts.first() + selectorController.selectAccount(firstAccount.accountId) + combineFlows(portfolioFetcher) } } @@ -151,22 +143,16 @@ internal class WcPairModel @Inject constructor( flow4 = isAccountsModeEnabledUseCase(), transform = { portfolios, selected, pairState, isAccountMode -> handlePairState( - pairState, - portfolios, - selected, - isAccountMode, + pairState = pairState, + portfolios = portfolios, + selected = selected, + isAccountMode = isAccountMode, ) }, ) .launchIn(modelScope) } - private fun loadDAppInfo() { - wcPairUseCase() - .onEach { pairState -> handlePairState(pairState) } - .launchIn(modelScope) - } - private suspend fun handlePairState( pairState: WcPairState, portfolios: PortfolioFetcher.Data? = null, @@ -193,7 +179,7 @@ internal class WcPairModel @Inject constructor( ) processError(pairState.error) } - is WcPairState.Loading -> appInfoUiState.update { createLoadingState(isAccountMode ?: false) } + is WcPairState.Loading -> appInfoUiState.update { createLoadingState(isAccountMode == true) } is WcPairState.Proposal -> handleProposalState( pairState = pairState, portfolios = portfolios, @@ -225,7 +211,7 @@ internal class WcPairModel @Inject constructor( } else { val portfolioSelectRow = tryToCreatePortfolioSelectRow(selected, portfolios) if (proposalAccountNetwork != null) { - selectorController.isEnabled.value = { wallet, account -> + selectorController.isEnabled.value = { _, account -> proposalAccountNetwork.contains(account.account.accountId) } } From 7b00754e24a4707cfa093d50ec790598e65c2f94 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 17:39:04 +0500 Subject: [PATCH 90/97] Updated on 2026-08-14 --- .../NorthernLightsBackground.kt | 25 ++++++++----------- .../tangem/core/ui/components/haze/HazeExt.kt | 10 ++++---- .../core/ui/ds/message/TangemMessageEffect.kt | 17 +++++++++---- .../core/ui/ds/row/header/TangemHeaderRow.kt | 14 +++++------ .../internal/TokenRowPriceChangeContent.kt | 14 ++++++----- .../feed/ui/feed/components/NewsSlider.kt | 2 +- .../page/background/NorthernLightsStory.kt | 3 +++ .../tokenActions/TokenActionsComponent.kt | 3 ++- .../presentation/wallet/ui/WalletScreen2.kt | 18 +++++++------ .../ui/components/common/WalletBalance.kt | 15 ++++++++++- .../ui/components/common/WalletContent.kt | 6 ++--- .../multicurrency/MultiCurrencyContent.kt | 1 + 12 files changed, 77 insertions(+), 51 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt index a59ce0f0e6..27b3583edc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/background/northernlights/NorthernLightsBackground.kt @@ -4,12 +4,7 @@ package com.tangem.core.ui.components.background.northernlights import android.os.Build import androidx.compose.animation.animateColor -import androidx.compose.animation.core.FastOutSlowInEasing -import androidx.compose.animation.core.RepeatMode -import androidx.compose.animation.core.StartOffset -import androidx.compose.animation.core.infiniteRepeatable -import androidx.compose.animation.core.keyframes -import androidx.compose.animation.core.rememberInfiniteTransition +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -21,7 +16,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.tangem.core.ui.components.background.shaderBackground import com.tangem.core.ui.res.LocalPowerSavingState -import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader /** @@ -29,10 +23,14 @@ import com.tangem.core.ui.shader.NorthernLightsMeshGradientShader * Uses a RuntimeShader on Android 13+ and falls back to a simpler implementation on older versions and in power saving mode. */ @Composable -fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion: Boolean = false) { +fun NorthernLightsBackground( + containerColor: Color, + modifier: Modifier = Modifier, + forceSimpleVersion: Boolean = false, +) { val isPowerSavingMode by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() if (!forceSimpleVersion && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && !isPowerSavingMode) { - NorthernLightsBackgroundWithShader(modifier) + NorthernLightsBackgroundWithShader(containerColor, modifier) } else { MovingColorfulBlubsBackground(modifier) } @@ -40,9 +38,8 @@ fun NorthernLightsBackground(modifier: Modifier = Modifier, forceSimpleVersion: @Suppress("LongMethod") @Composable -private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) { +private fun NorthernLightsBackgroundWithShader(containerColor: Color, modifier: Modifier = Modifier) { val transition = rememberInfiniteTransition(label = "FluidMeshGradientV2") - val backgroundColor = TangemTheme.colors2.surface.level1 // Each track cycles through 4 states (matching the screenshot frames): // deep/dark → saturated+bright → light/pastel → vibrant/vivid → back @@ -128,7 +125,7 @@ private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) { Color(0xFF1444AA), Color(0xFF4422BB), Color(0xFF331199), - backgroundColor, + containerColor, ), speed = 0.5f, scale = 4f, @@ -139,12 +136,12 @@ private fun NorthernLightsBackgroundWithShader(modifier: Modifier = Modifier) { colorsArray[1] = color2 colorsArray[2] = color3 colorsArray[3] = color4 - colorsArray[4] = backgroundColor + colorsArray[4] = containerColor shader.updateColors(colorsArray) Box( modifier = modifier - .background(backgroundColor) + .background(containerColor) .fillMaxSize() .shaderBackground(shader), ) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt index d34949ea34..b80af29a35 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/haze/HazeExt.kt @@ -32,15 +32,15 @@ internal fun ProvideHaze(content: @Composable () -> Unit) { */ @Composable fun Modifier.hazeEffectTangem( + state: HazeState = LocalHazeState.current, style: HazeStyle = HazeStyle.Unspecified, configure: HazeEffectScope.() -> Unit = {}, ): Modifier { val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState() - val hazeState = LocalHazeState.current - val isGlobalBlurEnabled = hazeState.blurEnabled && !powerSavingEnabled.value + val isGlobalBlurEnabled = state.blurEnabled && !powerSavingEnabled.value val rootBackground by LocalRootBackgroundColor.current - return hazeEffect(hazeState, style) { + return hazeEffect(state, style) { fallbackTint = HazeTint(rootBackground) if (isGlobalBlurEnabled) { configure() @@ -78,5 +78,5 @@ fun Modifier.hazeForegroundEffectTangem( * Applies a haze source to the [Modifier] using the current global haze state. */ @Composable -fun Modifier.hazeSourceTangem(zIndex: Float = 0f, key: Any? = null) = - this.hazeSource(LocalHazeState.current, zIndex, key) \ No newline at end of file +fun Modifier.hazeSourceTangem(state: HazeState = LocalHazeState.current, zIndex: Float = 0f, key: Any? = null) = + this.hazeSource(state, zIndex, key) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt index 96e24e5007..03b00e793a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt @@ -106,7 +106,10 @@ enum class TangemMessageEffect(val isAnimatable: Boolean) { Color(0x1AFFFFFF), ) } else { - persistentListOf() + persistentListOf( + Color(0xFFE1E1E1), + Color(0xFFE1E1E1), + ) } } } @@ -192,7 +195,10 @@ enum class TangemMessageEffect(val isAnimatable: Boolean) { Color(0x17E44848), ) None -> if (isInDarkTheme) { - persistentListOf() + persistentListOf( + Color(0x1AFFFFFF), + Color(0x1AFFFFFF), + ) } else { persistentListOf( Color(0x0d000000), @@ -238,9 +244,10 @@ internal fun Modifier.messageEffectBackground( val isInDarkTheme = LocalIsInDarkTheme.current val borderGradientColors = remember { messageEffect.getBorderGradient(isInDarkTheme) } val gradientColors = remember { messageEffect.getColorGradient(isInDarkTheme) } + val gradientTint = remember { messageEffect.getGradientTint(isInDarkTheme) } val angle by rememberAnimationAngle(messageEffect.isAnimatable) - val brush = Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme)) + val brush = remember { Brush.sweepGradient(messageEffect.getColorGradient(isInDarkTheme)) } val padding = 1.dp.toPx() return this @@ -254,14 +261,14 @@ internal fun Modifier.messageEffectBackground( border( width = 1.dp, brush = Brush.sweepGradient( - colors = messageEffect.getBorderGradient(isInDarkTheme), + colors = borderGradientColors, center = Offset.Infinite, ), shape = RoundedCornerShape(radius), ) } .hazeForegroundEffectTangem( - style = HazeStyle(tints = messageEffect.getGradientTint(isInDarkTheme)), + style = HazeStyle(tints = gradientTint), isBlurEnabled = true, ) { fallbackTint = HazeTint( diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt index f5af27f6c1..c635dff1a6 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/header/TangemHeaderRow.kt @@ -25,10 +25,7 @@ import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.ds.image.TangemIcon import com.tangem.core.ui.ds.image.TangemIconUM -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.clickableSingle -import com.tangem.core.ui.extensions.resolveAnnotatedReference -import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.test.TokenElementsTestTags @@ -40,12 +37,13 @@ import com.tangem.core.ui.test.TokenElementsTestTags * @param modifier Modifier for the composable */ @Composable -fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier) { +fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifier, isBalanceHidden: Boolean = false) { TangemHeaderRow( headTangemIconUM = headerRowUM.startIconUM, footerTangemIconRes = headerRowUM.endIconRes, title = headerRowUM.title, subtitle = headerRowUM.subtitle, + isBalanceHidden = isBalanceHidden, modifier = modifier, ) } @@ -63,6 +61,7 @@ fun TangemHeaderRow(headerRowUM: TangemHeaderRowUM, modifier: Modifier = Modifie @Composable fun TangemHeaderRow( modifier: Modifier = Modifier, + isBalanceHidden: Boolean = false, subtitle: TextReference? = null, onItemClick: (() -> Unit)? = null, @DrawableRes footerTangemIconRes: Int? = null, @@ -92,7 +91,7 @@ fun TangemHeaderRow( ) { val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } Text( - text = wrappedSubtitle.resolveAnnotatedReference(), + text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), style = TangemTheme.typography2.captionSemibold12, color = TangemTheme.colors2.text.neutral.secondary, maxLines = 1, @@ -131,6 +130,7 @@ fun TangemHeaderRow( fun TangemHeaderRow( title: TextReference, modifier: Modifier = Modifier, + isBalanceHidden: Boolean = false, subtitle: TextReference? = null, headTangemIconUM: TangemIconUM? = null, @DrawableRes footerTangemIconRes: Int? = null, @@ -172,7 +172,7 @@ fun TangemHeaderRow( ) { val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } Text( - text = wrappedSubtitle.resolveAnnotatedReference(), + text = wrappedSubtitle.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(), style = TangemTheme.typography2.captionSemibold12, color = TangemTheme.colors2.text.neutral.secondary, maxLines = 1, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt index ef635a0a8e..052c994ac7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/row/token/internal/TokenRowPriceChangeContent.kt @@ -24,6 +24,12 @@ internal fun RowScope.TokenRowPriceChangeContent( isFlickering: Boolean, isAvailable: Boolean = true, ) { + val color = when (priceChangeState.type) { + PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent + PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.tertiary + } + AnimatedContent( targetState = priceChangeState.type, label = "Update the price change's arrow", @@ -39,11 +45,7 @@ internal fun RowScope.TokenRowPriceChangeContent( }, ), ), - tint = when (animatedType) { - PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent - PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning - PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.secondary - }, + tint = color, contentDescription = null, modifier = Modifier.size(TangemTheme.dimens2.x3), ) @@ -61,7 +63,7 @@ internal fun RowScope.TokenRowPriceChangeContent( style = TangemTheme.typography2.captionSemibold12.applyBladeBrush( isEnabled = isFlickering, textColor = if (isAvailable) { - TangemTheme.colors2.text.neutral.secondary + color } else { TangemTheme.colors2.text.status.disabled }, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt index 4e20ea9c2f..1bc690a184 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/feed/components/NewsSlider.kt @@ -34,7 +34,7 @@ internal fun NewsSlider(newsSliderConfig: NewsSliderConfig) { .conditionalCompose( condition = isRedesignEnabled, modifier = { - hazeSourceTangem(-1f) + hazeSourceTangem(zIndex = -1f) }, ) .background(color = background), diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt index 3f96ca9d74..8f0fd1f280 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/background/NorthernLightsStory.kt @@ -1,4 +1,5 @@ @file:Suppress("MagicNumber", "LongMethod") + package com.tangem.feature.tester.presentation.storybook.page.background import androidx.compose.foundation.background @@ -15,6 +16,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.tangem.core.ui.components.background.northernlights.NorthernLightsBackground +import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory @Composable @@ -22,6 +24,7 @@ internal fun NorthernLightsStory(state: NorthernLightsStory, modifier: Modifier Box(modifier = modifier.fillMaxSize()) { NorthernLightsBackground( modifier = Modifier.fillMaxSize(), + containerColor = TangemTheme.colors2.surface.level1, forceSimpleVersion = state.variant == NorthernLightsStory.Variant.Simple, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt index e994cb9f17..aa0ae8e9d7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/tokenActions/TokenActionsComponent.kt @@ -13,6 +13,7 @@ import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.model.TokenActionButtonUM +import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach import dagger.assisted.Assisted import dagger.assisted.AssistedInject @@ -36,7 +37,7 @@ internal class TokenActionsComponent @AssistedInject constructor( ), ) { Column { - params.actions.forEach { action -> + params.actions.fastForEach { action -> if (action.isEnabled) { val rowColors = if (action.isWarning) { getWarningRowColors() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index c2c0b04c1a..119588e9f0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -54,10 +54,7 @@ import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior import com.tangem.core.ui.event.StateEvent import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.res.LocalMainBottomSheetColor -import com.tangem.core.ui.res.LocalWindowSize -import com.tangem.core.ui.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.res.* import com.tangem.feature.wallet.presentation.common.preview.WalletScreenPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.NOT_INITIALIZED_WALLET_INDEX import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM @@ -181,9 +178,16 @@ private fun WalletContent2( Box( modifier = Modifier .fillMaxSize() - .hazeSourceTangem(-1f), + .hazeSourceTangem(zIndex = -1f), ) { - NorthernLightsBackground(Modifier.matchParentSize()) + NorthernLightsBackground( + containerColor = if (LocalIsInDarkTheme.current) { + TangemTheme.colors2.surface.level1 + } else { + TangemTheme.colors2.surface.level2 + }, + modifier = Modifier.matchParentSize(), + ) WalletPagerIndicator( pagerState = walletsPagerState, @@ -201,7 +205,7 @@ private fun WalletContent2( state.wallets2[state.selectedWalletIndex] } - LaunchedEffect(walletsPagerState.currentPage) { + LaunchedEffect(walletsPagerState.currentPage, currentWallet.walletsBalanceUM) { if (walletsPagerState.currentPage == currentWalletIndex) { walletBalance = (currentWallet.walletsBalanceUM as? WalletBalanceUM.Content)?.balanceInAppBar } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt index 87d9cebc62..d5e4a5649a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt @@ -45,6 +45,7 @@ import com.tangem.feature.wallet.presentation.preview.WalletBalancePreview import com.tangem.feature.wallet.presentation.preview.WalletPreviewData import com.tangem.feature.wallet.presentation.wallet.state.model.WalletBalanceUM import com.tangem.feature.wallet.presentation.wallet.ui.components.fastForEach +import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.ImmutableList private const val MIN_SCALE = 0.75f @@ -135,7 +136,19 @@ private fun Balance(walletBalanceUM: WalletBalanceUM, isBalanceHidden: Boolean, ), ) } - is WalletBalanceUM.Error, + is WalletBalanceUM.Error -> { + Text( + text = StringsSigns.DASH_SIGN, + style = TangemTheme.typography2.titleRegular44, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = TangemTheme.typography2.bodySemibold15.fontSize, + maxFontSize = TangemTheme.typography2.titleRegular44.fontSize, + ), + ) + } is WalletBalanceUM.Loading, -> { TextShimmer( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt index 066a167e8d..3d542d67a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletContent.kt @@ -45,16 +45,14 @@ internal fun WalletListContent( overscrollEffect = rememberOverscrollEffect(), ) { notifications( - notifications = currentWallet.notifications.map { it.messageUM } - .toPersistentList(), + notifications = currentWallet.notifications.map { it.messageUM }.toPersistentList(), contentColor = containerColor, modifier = movableItemModifier, ) notificationsCarousel( containerColor = containerColor, modifier = movableItemModifier, - notifications = currentWallet.notifications.map { it.messageUM } - .toPersistentList(), + notifications = currentWallet.notificationsCarousel.map { it.messageUM }.toPersistentList(), ) tangemPay( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt index c3755c12bc..905ba692e0 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyContent.kt @@ -204,6 +204,7 @@ private fun LazyListScope.portfolioItem( ) is TangemHeaderRowUM -> TangemHeaderRow( headerRowUM = tokenRowUM, + isBalanceHidden = isBalanceHidden, modifier = itemModifier, ) } From 563d0834c46ed6bb6728ef9617a88b9b8befe221 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 3 Mar 2026 18:17:11 +0400 Subject: [PATCH 91/97] Updated on 2026-08-14 --- .../tap/di/domain/AccountDomainModule.kt | 3 - .../configs/feature_toggles_config.json | 4 - .../data/account/di/AccountDataModule.kt | 9 - ...ltiWalletCryptoCurrenciesProducerModule.kt | 6 +- .../DefaultAccountsFeatureToggles.kt | 12 - .../DefaultCardCryptoCurrencyFactory.kt | 64 +- .../data/common/currency/UserTokensSaver.kt | 39 +- .../tangem/data/common/di/DataCommonModule.kt | 9 - .../DefaultCardCryptoCurrencyFactoryTest.kt | 95 +-- .../common/currency/UserTokensSaverTest.kt | 9 - ...faultMultiWalletCryptoCurrenciesFetcher.kt | 129 ---- ...ultiWalletCryptoCurrenciesFetcherModule.kt | 45 +- .../tangem/data/tokens/di/TokensDataModule.kt | 3 - .../repository/DefaultCurrenciesRepository.kt | 65 +-- ...tMultiWalletCryptoCurrenciesFetcherTest.kt | 549 ------------------ .../featuretoggle/AccountsFeatureToggles.kt | 11 - .../usecase/IsAccountsModeEnabledUseCase.kt | 7 - .../IsAccountsModeEnabledUseCaseTest.kt | 57 +- .../com/tangem/domain/models/PortfolioId.kt | 8 +- 19 files changed, 99 insertions(+), 1025 deletions(-) delete mode 100644 data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt delete mode 100644 data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt delete mode 100644 data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt delete mode 100644 domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt index 569c69c6b8..1240259f77 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/AccountDomainModule.kt @@ -1,6 +1,5 @@ package com.tangem.tap.di.domain -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.fetcher.SingleAccountListFetcher import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier @@ -97,12 +96,10 @@ internal object AccountDomainModule { fun provideIsAccountsModeEnabledUseCase( userWalletsListRepository: UserWalletsListRepository, accountsCRUDRepository: AccountsCRUDRepository, - accountsFeatureToggles: AccountsFeatureToggles, ): IsAccountsModeEnabledUseCase { return IsAccountsModeEnabledUseCase( userWalletsListRepository = userWalletsListRepository, crudRepository = accountsCRUDRepository, - accountsFeatureToggles = accountsFeatureToggles, ) } diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index dc14b38ca6..73b02b2af8 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -24,10 +24,6 @@ "name": "HOT_WALLET_CREATION_RESTRICTION_ENABLED", "version": "5.32.0" }, - { - "name": "ACCOUNTS_FEATURE_ENABLED", - "version": "5.33.0" - }, { "name": "APP_REDESIGN_ENABLED", "version": "undefined" diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt index fc62ab079a..5af224e026 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/AccountDataModule.kt @@ -4,9 +4,7 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi -import com.tangem.core.configtoggle.feature.FeatureTogglesManager import com.tangem.data.account.converter.AccountConverterFactoryContainer -import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher import com.tangem.data.account.repository.AccountsExpandedDTO import com.tangem.data.account.repository.DefaultAccountsCRUDRepository @@ -25,7 +23,6 @@ import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.utils.MoshiDataStoreSerializer import com.tangem.datasource.utils.mapWithStringKeyTypes import com.tangem.datasource.utils.setTypes -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.repository.AccountsExpandedRepository import com.tangem.domain.account.tokens.MainAccountTokensMigration @@ -43,12 +40,6 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) internal object AccountDataModule { - @Provides - @Singleton - fun provideAccountFeatureToggle(featureTogglesManager: FeatureTogglesManager): AccountsFeatureToggles { - return DefaultAccountsFeatureToggles(featureTogglesManager = featureTogglesManager) - } - @Provides @Singleton fun provideAccountsCRUDRepository( diff --git a/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt b/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt index e283366b5b..deb207e821 100644 --- a/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt +++ b/data/account/src/main/kotlin/com/tangem/data/account/di/MultiWalletCryptoCurrenciesProducerModule.kt @@ -1,8 +1,6 @@ package com.tangem.data.account.di import com.tangem.data.account.producer.AccountListCryptoCurrenciesProducer -import com.tangem.data.account.producer.DefaultMultiWalletCryptoCurrenciesProducer -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import dagger.Module import dagger.Provides @@ -17,10 +15,8 @@ internal object MultiWalletCryptoCurrenciesProducerModule { @Singleton @Provides fun provideMultiWalletCryptoCurrenciesProducerFactory( - accountsFeatureToggles: AccountsFeatureToggles, - defaultImpl: DefaultMultiWalletCryptoCurrenciesProducer.Factory, accountsImpl: AccountListCryptoCurrenciesProducer.Factory, ): MultiWalletCryptoCurrenciesProducer.Factory { - return if (accountsFeatureToggles.isFeatureEnabled) accountsImpl else defaultImpl + return accountsImpl } } \ No newline at end of file diff --git a/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt b/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt deleted file mode 100644 index 0a6accf03f..0000000000 --- a/data/account/src/main/kotlin/com/tangem/data/account/featuretoggle/DefaultAccountsFeatureToggles.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.data.account.featuretoggle - -import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles - -internal class DefaultAccountsFeatureToggles( - private val featureTogglesManager: FeatureTogglesManager, -) : AccountsFeatureToggles { - - override val isFeatureEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(name = "ACCOUNTS_FEATURE_ENABLED") -} \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index a55c9c0da8..8dfe637ba5 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -5,8 +5,6 @@ import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.tokens.getDefaultWalletBlockchains -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncStrict @@ -23,17 +21,13 @@ import com.tangem.domain.models.wallet.isMultiCurrency * * @property demoConfig demo config * @property excludedBlockchains excluded blockchains - * @property userWalletsStore user wallets store - * @property userTokensResponseStore user tokens response store */ @Suppress("LongParameterList") internal class DefaultCardCryptoCurrencyFactory( private val demoConfig: DemoConfig, private val excludedBlockchains: ExcludedBlockchains, private val userWalletsListRepository: UserWalletsListRepository, - private val accountsFeatureToggles: AccountsFeatureToggles, private val walletAccountsFetcher: WalletAccountsFetcher, - private val userTokensResponseStore: UserTokensResponseStore, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ) : CardCryptoCurrencyFactory { @@ -140,34 +134,21 @@ internal class DefaultCardCryptoCurrencyFactory( userWallet: UserWallet, networks: Set, ): Map> { - val existingNetworkWithCurrencies = if (accountsFeatureToggles.isFeatureEnabled) { - val response = walletAccountsFetcher.getSaved(userWallet.walletId) - ?: return emptyMap() + val response = walletAccountsFetcher.getSaved(userWallet.walletId) + ?: return emptyMap() - response.accounts.flatMapTo(hashSetOf()) { accountDTO -> - val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() - ?: return@flatMapTo emptySet() - - responseCryptoCurrenciesFactory.createCurrencies( - tokens = accountDTO.tokens.orEmpty().filter { token -> - networks.any { - it.backendId == token.networkId && it.derivationPath.value == token.derivationPath - } - }, - userWallet = userWallet, - accountIndex = accountIndex, - ) - } - } else { - val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) - ?: return emptyMap() + val existingNetworkWithCurrencies = response.accounts.flatMapTo(hashSetOf()) { accountDTO -> + val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() + ?: return@flatMapTo emptySet() responseCryptoCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { token -> - networks.any { it.backendId == token.networkId && it.derivationPath.value == token.derivationPath } + tokens = accountDTO.tokens.orEmpty().filter { token -> + networks.any { + it.backendId == token.networkId && it.derivationPath.value == token.derivationPath + } }, userWallet = userWallet, - accountIndex = DerivationIndex.Main, + accountIndex = accountIndex, ) } .groupBy(CryptoCurrency::network) @@ -181,28 +162,17 @@ internal class DefaultCardCryptoCurrencyFactory( ): Map> { val networkIds = rawIds.map { it.toBlockchain().toNetworkId() } - return if (accountsFeatureToggles.isFeatureEnabled) { - val response = walletAccountsFetcher.getSaved(userWallet.walletId) - ?: return emptyMap() + val response = walletAccountsFetcher.getSaved(userWallet.walletId) + ?: return emptyMap() - response.accounts.flatMapTo(hashSetOf()) { accountDTO -> - val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() - ?: return@flatMapTo emptySet() - - responseCryptoCurrenciesFactory.createCurrencies( - tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds }, - userWallet = userWallet, - accountIndex = accountIndex, - ) - } - } else { - val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) - ?: return emptyMap() + return response.accounts.flatMapTo(hashSetOf()) { accountDTO -> + val accountIndex = DerivationIndex(accountDTO.derivationIndex).getOrNull() + ?: return@flatMapTo emptySet() responseCryptoCurrenciesFactory.createCurrencies( - tokens = response.tokens.filter { token -> token.networkId in networkIds }, + tokens = accountDTO.tokens.orEmpty().filter { token -> token.networkId in networkIds }, userWallet = userWallet, - accountIndex = DerivationIndex.Main, + accountIndex = accountIndex, ) } .groupBy { it.network.id.rawId } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt index cff0ae1d6d..a1ab6f180b 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensSaver.kt @@ -8,10 +8,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError 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.WalletType -import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncOrNull import com.tangem.domain.models.wallet.UserWallet @@ -30,8 +27,6 @@ class UserTokensSaver( private val dispatchers: CoroutineDispatcherProvider, private val addressesEnricher: UserTokensResponseAddressesEnricher, private val walletServerBinder: WalletServerBinder, - private val appsFlyerStore: AppsFlyerStore, - private val accountsFeatureToggles: AccountsFeatureToggles, private val pushTokensRetryerPool: RetryerPool, ) { private val userTokensBackwardCompatibility = UserTokensBackwardCompatibility() @@ -68,22 +63,9 @@ class UserTokensSaver( return@withContext } - if (accountsFeatureToggles.isFeatureEnabled) { - val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher) + val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher) - pushNew(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) - } else { - val conversionData = appsFlyerStore.get() - - val enrichedResponse = response.enrichIf(userWalletId = userWalletId, condition = useEnricher).copy( - walletName = userWallet.name.takeIf { it.isNotBlank() }, - walletType = WalletType.from(userWallet), - refcode = conversionData?.refcode, - campaign = conversionData?.campaign, - ) - - pushLegacy(userWalletId = userWalletId, response = enrichedResponse, onFailSend = onFailSend) - } + push(userWallet = userWallet, response = enrichedResponse, onFailSend = onFailSend) } suspend fun pushWithRetryer( @@ -103,14 +85,7 @@ class UserTokensSaver( ) } - private suspend fun pushLegacy(userWalletId: UserWalletId, response: UserTokensResponse, onFailSend: () -> Unit) { - safeApiCall( - call = { tangemTechApi.saveUserTokens(userId = userWalletId.stringValue, userTokens = response).bind() }, - onError = { onFailSend() }, - ) - } - - private suspend fun pushNew(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) { + private suspend fun push(userWallet: UserWallet, response: UserTokensResponse, onFailSend: () -> Unit) { safeApiCall( call = { val apiResponse = tangemTechApi.saveTokens( @@ -148,13 +123,7 @@ class UserTokensSaver( return this .enrichByAddress(userWalletId = userWalletId) - .let { response -> - if (accountsFeatureToggles.isFeatureEnabled) { - response.enrichByAccountId(userWalletId = userWalletId) - } else { - response - } - } + .enrichByAccountId(userWalletId = userWalletId) } private suspend fun UserTokensResponse.enrichByAddress(userWalletId: UserWalletId): UserTokensResponse { diff --git a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt index 9ce3a6b121..2d0f7de535 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/di/DataCommonModule.kt @@ -13,7 +13,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.walletmanager.WalletManagersFacade @@ -37,18 +36,14 @@ internal object DataCommonModule { fun provideCardCryptoCurrencyFactory( excludedBlockchains: ExcludedBlockchains, userWalletsListRepository: UserWalletsListRepository, - accountsFeatureToggles: AccountsFeatureToggles, walletAccountsFetcher: WalletAccountsFetcher, - userTokensResponseStore: UserTokensResponseStore, responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, ): CardCryptoCurrencyFactory { return DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig, excludedBlockchains = excludedBlockchains, userWalletsListRepository = userWalletsListRepository, - accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, - userTokensResponseStore = userTokensResponseStore, responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, ) } @@ -76,8 +71,6 @@ internal object DataCommonModule { dispatchers: CoroutineDispatcherProvider, addressesEnricher: UserTokensResponseAddressesEnricher, walletServerBinder: WalletServerBinder, - appsFlyerStore: AppsFlyerStore, - accountsFeatureToggles: AccountsFeatureToggles, ): UserTokensSaver { return UserTokensSaver( tangemTechApi = tangemTechApi, @@ -85,12 +78,10 @@ internal object DataCommonModule { userTokensResponseStore = userTokensResponseStore, dispatchers = dispatchers, addressesEnricher = addressesEnricher, - accountsFeatureToggles = accountsFeatureToggles, pushTokensRetryerPool = RetryerPool( coroutineScope = CoroutineScope(SupervisorJob() + dispatchers.default), ), walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, ) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt index eb90a1efb0..749eadff8b 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactoryTest.kt @@ -11,8 +11,8 @@ import com.tangem.common.test.domain.wallet.MockUserWalletFactory import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.network.NetworkFactory import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles +import com.tangem.datasource.api.tangemTech.models.account.GetWalletAccountsResponse +import com.tangem.datasource.api.tangemTech.models.account.WalletAccountDTO import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.configs.GenericCardConfig import com.tangem.domain.common.wallets.UserWalletsListRepository @@ -38,20 +38,16 @@ import org.junit.jupiter.params.ParameterizedTest internal class DefaultCardCryptoCurrencyFactoryTest { private val userWalletsListRepository: UserWalletsListRepository = mockk() - private val userTokensResponseStore: UserTokensResponseStore = mockk() private val excludedBlockchains = ExcludedBlockchains() - private val accountsFeatureToggles = mockk() private val walletAccountsFetcher = mockk() private val factory = DefaultCardCryptoCurrencyFactory( demoConfig = DemoConfig, excludedBlockchains = excludedBlockchains, userWalletsListRepository = userWalletsListRepository, - userTokensResponseStore = userTokensResponseStore, responseCryptoCurrenciesFactory = ResponseCryptoCurrenciesFactory( networkFactory = NetworkFactory(excludedBlockchains = excludedBlockchains), ), - accountsFeatureToggles = accountsFeatureToggles, walletAccountsFetcher = walletAccountsFetcher, ) @@ -64,7 +60,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { @BeforeEach fun init() { - clearMocks(userWalletsListRepository, userTokensResponseStore, accountsFeatureToggles, walletAccountsFetcher, iconUri) + clearMocks(userWalletsListRepository, walletAccountsFetcher, iconUri) mockkStatic(Uri::class) every { Uri.parse(any()) } returns iconUri @@ -80,12 +76,11 @@ internal class DefaultCardCryptoCurrencyFactoryTest { // Arrange val userWallet = createMultiWallet() val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - val userTokensResponse = model.userTokensResponse + val accountsResponse = model.accountsResponse val network = ethereum.network - every { accountsFeatureToggles.isFeatureEnabled } returns false every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse + coEvery { walletAccountsFetcher.getSaved(userWallet.walletId) } returns accountsResponse // Act val actual = factory.create(userWalletId = userWallet.walletId, network = network) @@ -97,25 +92,25 @@ internal class DefaultCardCryptoCurrencyFactoryTest { coVerifyOrder { userWalletsListRepository.userWallets - userTokensResponseStore.getSyncOrNull(userWalletId = userWallet.walletId) + walletAccountsFetcher.getSaved(userWalletId = userWallet.walletId) } } private fun provideTestModels() = listOf( - CreateTestModel.MultiWallet(userTokensResponse = null, expected = emptyList()), - CreateTestModel.MultiWallet(userTokensResponse = createUserTokensResponse(), expected = emptyList()), + CreateTestModel.MultiWallet(accountsResponse = null, expected = emptyList()), + CreateTestModel.MultiWallet(accountsResponse = createAccountsResponse(), expected = emptyList()), CreateTestModel.MultiWallet( - userTokensResponse = createUserTokensResponse(currencies = listOf(ethereum)), + accountsResponse = createAccountsResponse(currencies = listOf(ethereum)), expected = listOf(ethereum), ), CreateTestModel.MultiWallet( - userTokensResponse = createUserTokensResponse(listOf(element = bitcoin)), + accountsResponse = createAccountsResponse(listOf(element = bitcoin)), expected = emptyList(), ), ) - private fun createUserTokensResponse(currencies: List = emptyList()): UserTokensResponse { - return userTokensResponseFactory.createUserTokensResponse( + private fun createAccountsResponse(currencies: List = emptyList()): GetWalletAccountsResponse { + return createWalletAccountsResponse( currencies = currencies, isGroupedByNetwork = false, isSortedByBalance = false, @@ -150,7 +145,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { } coVerify(inverse = true) { - userTokensResponseStore.getSyncOrNull(userWalletId = any()) + walletAccountsFetcher.getSaved(userWalletId = any()) } } @@ -195,7 +190,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { } coVerify(inverse = true) { - userTokensResponseStore.getSyncOrNull(userWalletId = any()) + walletAccountsFetcher.getSaved(userWalletId = any()) } } @@ -218,7 +213,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { val expected: List data class MultiWallet( - val userTokensResponse: UserTokensResponse?, + val accountsResponse: GetWalletAccountsResponse?, override val expected: List, ) : CreateTestModel @@ -245,10 +240,9 @@ internal class DefaultCardCryptoCurrencyFactoryTest { // Arrange val userWallet = model.multiWallet val networks = setOf(ethereum.network, bitcoin.network) - val userTokensResponse = model.userTokensResponse + val accountsResponse = model.accountsResponse - every { accountsFeatureToggles.isFeatureEnabled } returns false - coEvery { userTokensResponseStore.getSyncOrNull(userWallet.walletId) } returns userTokensResponse + coEvery { walletAccountsFetcher.getSaved(userWallet.walletId) } returns accountsResponse // Act val actual = runCatching { @@ -273,19 +267,19 @@ internal class DefaultCardCryptoCurrencyFactoryTest { private fun provideTestModels() = listOf( CreateCurrenciesForMultiWalletModel( multiWallet = createMultiWallet(), - userTokensResponse = null, + accountsResponse = null, expected = Result.success(emptyMap()), ), CreateCurrenciesForMultiWalletModel( multiWallet = createMultiWallet(), - userTokensResponse = createUserTokensResponse(), + accountsResponse = createWalletAccountsResponse(emptyList(), false, false), expected = Result.success( setOf(ethereum.network, bitcoin.network).associateWith { emptyList() }, ), ), CreateCurrenciesForMultiWalletModel( multiWallet = createMultiWallet(), - userTokensResponse = createUserTokensResponse(currencies = listOf(bitcoin, ethereum)), + accountsResponse = createWalletAccountsResponse(currencies = listOf(bitcoin, ethereum), false, false), expected = mapOf( bitcoin.network to listOf(bitcoin), ethereum.network to listOf(ethereum), @@ -293,12 +287,12 @@ internal class DefaultCardCryptoCurrencyFactoryTest { ), CreateCurrenciesForMultiWalletModel( multiWallet = createSingleWallet(), - userTokensResponse = null, + accountsResponse = null, expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")), ), CreateCurrenciesForMultiWalletModel( multiWallet = MockUserWalletFactory.createSingleWalletWithToken(), - userTokensResponse = null, + accountsResponse = null, expected = Result.failure(IllegalArgumentException("It isn't multi-currency wallet")), ), ) @@ -306,7 +300,7 @@ internal class DefaultCardCryptoCurrencyFactoryTest { data class CreateCurrenciesForMultiWalletModel( val multiWallet: UserWallet, - val userTokensResponse: UserTokensResponse?, + val accountsResponse: GetWalletAccountsResponse?, val expected: Result>>, ) @@ -546,11 +540,44 @@ internal class DefaultCardCryptoCurrencyFactoryTest { )!! } - private fun createUserTokensResponse(currencies: List = emptyList()): UserTokensResponse { - return userTokensResponseFactory.createUserTokensResponse( - currencies = currencies, - isGroupedByNetwork = false, - isSortedByBalance = false, + private fun createWalletAccountsResponse( + currencies: List, + isGroupedByNetwork: Boolean, + isSortedByBalance: Boolean, + ): GetWalletAccountsResponse { + val tokens = currencies.map { currency -> + userTokensResponseFactory.createResponseToken(currency = currency, accountId = null) + } + + return GetWalletAccountsResponse( + wallet = GetWalletAccountsResponse.Wallet( + version = 0, + group = if (isGroupedByNetwork) { + UserTokensResponse.GroupType.NETWORK + } else { + UserTokensResponse.GroupType.NONE + }, + sort = if (isSortedByBalance) { + UserTokensResponse.SortType.BALANCE + } else { + UserTokensResponse.SortType.MANUAL + }, + totalAccounts = 1, + totalArchivedAccounts = 0, + ), + accounts = listOf( + WalletAccountDTO( + id = "account_0", + name = "Main", + derivationIndex = 0, + icon = "🏠", + iconColor = "#000000", + tokens = tokens, + totalTokens = tokens.size, + totalNetworks = currencies.map { it.network }.distinct().size, + ), + ), + unassignedTokens = emptyList(), ) } diff --git a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt index 16bb9deefe..4cc3f5aa9f 100644 --- a/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt +++ b/data/common/src/test/kotlin/com/tangem/data/common/currency/UserTokensSaverTest.kt @@ -6,9 +6,7 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.api.tangemTech.models.WalletType -import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -27,11 +25,7 @@ class UserTokensSaverTest { private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxed = true) private val enricher: UserTokensResponseAddressesEnricher = mockk() - private val accountsFeatureToggles = mockk { - every { this@mockk.isFeatureEnabled } returns true - } private val walletServerBinder: WalletServerBinder = mockk() - private val appsFlyerStore: AppsFlyerStore = mockk() private val userTokensSaver: UserTokensSaver = UserTokensSaver( tangemTechApi = tangemTechApi, @@ -40,8 +34,6 @@ class UserTokensSaverTest { dispatchers = TestingCoroutineDispatcherProvider(), addressesEnricher = enricher, walletServerBinder = walletServerBinder, - appsFlyerStore = appsFlyerStore, - accountsFeatureToggles = accountsFeatureToggles, pushTokensRetryerPool = mockk(), ) @@ -121,7 +113,6 @@ class UserTokensSaverTest { val userWalletsFlow = MutableStateFlow(listOf(userWallet)) - every { accountsFeatureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns userWalletsFlow coEvery { enricher(userWalletId, response) } returns enrichedResponse coEvery { tangemTechApi.saveTokens(any(), any()) } returns ApiResponse.Error(error) as ApiResponse diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt deleted file mode 100644 index 4b563eb468..0000000000 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcher.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.tangem.data.tokens - -import arrow.core.Either -import com.tangem.data.common.api.safeApiCall -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory -import com.tangem.data.common.currency.UserTokensSaver -import com.tangem.data.tokens.utils.CustomTokensMerger -import com.tangem.datasource.api.common.response.ApiResponseError -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.common.wallets.getSyncStrict -import com.tangem.domain.core.utils.catchOn -import com.tangem.domain.demo.models.DemoConfig -import com.tangem.domain.express.ExpressServiceFetcher -import com.tangem.domain.express.models.ExpressAsset -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.models.wallet.isMultiCurrency -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher.Params -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.withContext -import timber.log.Timber - -/** - * Default implementation of [MultiWalletCryptoCurrenciesFetcher] - * - * @property tangemTechApi Tangem Tech API - * @property userTokensResponseStore store of [UserTokensResponse] - * @property userTokensSaver user tokens saver - * @property cardCryptoCurrencyFactory factory for creating crypto currencies for specified card - * @property expressServiceFetcher express service loader - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -@Suppress("LongParameterList") -internal class DefaultMultiWalletCryptoCurrenciesFetcher( - private val demoConfig: DemoConfig, - private val userWalletsListRepository: UserWalletsListRepository, - private val tangemTechApi: TangemTechApi, - private val customTokensMerger: CustomTokensMerger, - private val userTokensResponseStore: UserTokensResponseStore, - private val userTokensSaver: UserTokensSaver, - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - private val expressServiceFetcher: ExpressServiceFetcher, - private val dispatchers: CoroutineDispatcherProvider, -) : MultiWalletCryptoCurrenciesFetcher { - - private val userTokensResponseFactory = UserTokensResponseFactory() - - override suspend fun invoke(params: Params) = Either.catchOn(dispatchers.default) { - val userWallet = userWalletsListRepository.getSyncStrict(id = params.userWalletId) - - if (!userWallet.isMultiCurrency) error("${this::class.simpleName} supports only multi-currency wallet") - - val response = if (userWallet is UserWallet.Cold && userWallet.isDemoWalletWithoutSavedTokens()) { - createDefaultUserTokensResponse(userWallet = userWallet) - } else { - safeApiCall( - call = { - withContext(dispatchers.io) { - tangemTechApi.getUserTokens(userId = userWallet.walletId.stringValue).bind() - } - }, - onError = { - handleFetchTokensError(error = it, userWallet = userWallet) - }, - ) - } - - val compatibleUserTokensResponse = response - .let { it.copy(tokens = it.tokens.distinct()) } - .let { customTokensMerger.mergeIfPresented(userWalletId = userWallet.walletId, response = it) } - - userTokensSaver.store(userWalletId = userWallet.walletId, response = compatibleUserTokensResponse) - - fetchExpressAssetsByNetworkIds(userWallet = userWallet, userTokens = compatibleUserTokensResponse) - } - - private suspend fun UserWallet.Cold.isDemoWalletWithoutSavedTokens(): Boolean { - val isDemoCard = demoConfig.isDemoCardId(cardId = cardId) - - return if (isDemoCard) { - val response = userTokensResponseStore.getSyncOrNull(userWalletId = walletId) - - response == null - } else { - false - } - } - - private suspend fun handleFetchTokensError(error: ApiResponseError, userWallet: UserWallet): UserTokensResponse { - val userWalletId = userWallet.walletId - - val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - ?: createDefaultUserTokensResponse(userWallet = userWallet) - - if (error is ApiResponseError.HttpException && error.code == ApiResponseError.HttpException.Code.NOT_FOUND) { - Timber.w(error, "Requested currencies could not be found in the remote store for: $userWalletId") - - userTokensSaver.push(userWalletId, response) - } - - return response - } - - private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) { - val tokens = userTokens.tokens.mapTo(hashSetOf()) { token -> - ExpressAsset.ID( - networkId = token.networkId, - contractAddress = token.contractAddress, - ) - } - - expressServiceFetcher.fetch(userWallet = userWallet, assetIds = tokens) - } - - private fun createDefaultUserTokensResponse(userWallet: UserWallet): UserTokensResponse { - return userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(userWallet), - isGroupedByNetwork = false, - isSortedByBalance = false, - accountId = null, - ) - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt index e3e6f70446..68065f148b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/MultiWalletCryptoCurrenciesFetcherModule.kt @@ -1,16 +1,8 @@ package com.tangem.data.tokens.di import com.tangem.data.common.account.WalletAccountsFetcher -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.tokens.AccountListCryptoCurrenciesFetcher -import com.tangem.data.tokens.DefaultMultiWalletCryptoCurrenciesFetcher -import com.tangem.data.tokens.utils.CustomTokensMerger -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -27,39 +19,16 @@ internal class MultiWalletCryptoCurrenciesFetcherModule { @Singleton @Provides fun provideMultiWalletCryptoCurrenciesFetcher( - accountsFeatureToggles: AccountsFeatureToggles, - tangemTechApi: TangemTechApi, userWalletsListRepository: UserWalletsListRepository, - userTokensResponseStore: UserTokensResponseStore, - userTokensSaver: UserTokensSaver, - cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - expressServiceFetcher: ExpressServiceFetcher, walletAccountsFetcher: WalletAccountsFetcher, + expressServiceFetcher: ExpressServiceFetcher, dispatchers: CoroutineDispatcherProvider, ): MultiWalletCryptoCurrenciesFetcher { - return if (accountsFeatureToggles.isFeatureEnabled) { - AccountListCryptoCurrenciesFetcher( - userWalletsListRepository = userWalletsListRepository, - walletAccountsFetcher = walletAccountsFetcher, - expressServiceFetcher = expressServiceFetcher, - dispatchers = dispatchers, - ) - } else { - DefaultMultiWalletCryptoCurrenciesFetcher( - demoConfig = DemoConfig, - userWalletsListRepository = userWalletsListRepository, - tangemTechApi = tangemTechApi, - customTokensMerger = CustomTokensMerger( - tangemTechApi = tangemTechApi, - userTokensSaver = userTokensSaver, - dispatchers = dispatchers, - ), - userTokensResponseStore = userTokensResponseStore, - userTokensSaver = userTokensSaver, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, - expressServiceFetcher = expressServiceFetcher, - dispatchers = dispatchers, - ) - } + return AccountListCryptoCurrenciesFetcher( + userWalletsListRepository = userWalletsListRepository, + walletAccountsFetcher = walletAccountsFetcher, + expressServiceFetcher = expressServiceFetcher, + dispatchers = dispatchers, + ) } } \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 652ffeaa92..26626eb998 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -13,7 +13,6 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.token.TokenReceiveWarningActionStore import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier @@ -48,7 +47,6 @@ internal object TokensDataModule { tokensSaver: UserTokensSaver, responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - accountsFeatureToggles: AccountsFeatureToggles, ): CurrenciesRepository { return DefaultCurrenciesRepository( tangemTechApi = tangemTechApi, @@ -63,7 +61,6 @@ internal object TokensDataModule { userTokensSaver = tokensSaver, responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - accountsFeatureToggles = accountsFeatureToggles, ) } diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 3e5eadeda3..16b2415e7e 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -3,7 +3,9 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison -import com.tangem.blockchainsdk.utils.* +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.* @@ -13,7 +15,6 @@ import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository @@ -51,7 +52,6 @@ internal class DefaultCurrenciesRepository( private val userTokensSaver: UserTokensSaver, private val userTokensResponseStore: UserTokensResponseStore, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, - private val accountsFeatureToggles: AccountsFeatureToggles, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { @@ -187,19 +187,7 @@ internal class DefaultCurrenciesRepository( networkId: Network.ID, derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin { - return if (accountsFeatureToggles.isFeatureEnabled) { - getNetworkCoinNew(userWalletId, networkId, derivationPath) - } else { - getNetworkCoinLegacy(userWalletId, networkId, derivationPath) - } - } - - private suspend fun getNetworkCoinNew( - userWalletId: UserWalletId, - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin = withContext(dispatchers.default) { - multiWalletCryptoCurrenciesSupplier.getSyncOrNull( + return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), ) .orEmpty() @@ -211,44 +199,6 @@ internal class DefaultCurrenciesRepository( ?: error("Unable to find coin for network ID: $networkId") } - private suspend fun getNetworkCoinLegacy( - userWalletId: UserWalletId, - networkId: Network.ID, - derivationPath: Network.DerivationPath, - ): CryptoCurrency.Coin { - return withContext(dispatchers.io) { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - ensureIsCorrectUserWallet(userWallet = userWallet, isMultiCurrencyWalletExpected = true) - - fetchTokensIfCacheExpired(userWallet = userWallet, refresh = false) - - val storedTokens = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWalletId), - lazyMessage = { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - }, - ) - val blockchain = networkId.toBlockchain() - val blockchainNetworkId = blockchain.toNetworkId() - val coinId = blockchain.toCoinId() - - val storedCoin = storedTokens.tokens - .find { token -> - token.networkId == blockchainNetworkId && - compareIdWithMigrations(token, coinId) && - token.derivationPath == derivationPath.value - } ?: error("Coin in this network $networkId not found") - - val coin = responseCryptoCurrenciesFactory.createCurrency( - responseToken = storedCoin, - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - - coin as? CryptoCurrency.Coin ?: error("Unable to create currency") - } - } - override fun isTokensGrouped(userWalletId: UserWalletId): Flow { return channelFlow { val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) @@ -470,13 +420,6 @@ internal class DefaultCurrenciesRepository( ) } - private fun compareIdWithMigrations(token: UserTokensResponse.Token, coinId: String): Boolean { - return when { - token.id == OLD_POLYGON_NAME -> NEW_POLYGON_NAME == coinId - else -> token.id == coinId - } - } - private suspend fun fetchTokens(userWallet: UserWallet) { val userWalletId = userWallet.walletId diff --git a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt b/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt deleted file mode 100644 index 1b649ebbe0..0000000000 --- a/data/tokens/src/test/kotlin/com/tangem/data/tokens/DefaultMultiWalletCryptoCurrenciesFetcherTest.kt +++ /dev/null @@ -1,549 +0,0 @@ -package com.tangem.data.tokens - -import arrow.core.left -import arrow.core.right -import com.tangem.blockchain.common.Blockchain -import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory -import com.tangem.data.common.currency.CardCryptoCurrencyFactory -import com.tangem.data.common.currency.UserTokensResponseFactory -import com.tangem.data.common.currency.UserTokensSaver -import com.tangem.data.tokens.utils.CustomTokensMerger -import com.tangem.datasource.api.common.response.ApiResponse -import com.tangem.datasource.api.common.response.ApiResponseError -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -import com.tangem.datasource.local.token.UserTokensResponseStore -import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.demo.models.DemoConfig -import com.tangem.domain.express.ExpressServiceFetcher -import com.tangem.domain.express.models.ExpressAsset -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.tokens.MultiWalletCryptoCurrenciesFetcher -import com.tangem.test.core.assertEither -import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider -import io.mockk.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.test.runTest -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.TestInstance - -/** -[REDACTED_AUTHOR] - */ -@TestInstance(TestInstance.Lifecycle.PER_CLASS) -internal class DefaultMultiWalletCryptoCurrenciesFetcherTest { - - private val cryptoCurrencyFactory = MockCryptoCurrencyFactory() - private val userTokensResponseFactory = UserTokensResponseFactory() - - private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) - private val tangemTechApi: TangemTechApi = mockk() - private val customTokensMerger: CustomTokensMerger = mockk() - private val userTokensResponseStore: UserTokensResponseStore = mockk(relaxUnitFun = true) - private val userTokensSaver: UserTokensSaver = mockk(relaxUnitFun = true) - private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() - private val expressServiceFetcher: ExpressServiceFetcher = mockk(relaxUnitFun = true) - - private val fetcher = DefaultMultiWalletCryptoCurrenciesFetcher( - demoConfig = DemoConfig, - userWalletsListRepository = userWalletsListRepository, - tangemTechApi = tangemTechApi, - customTokensMerger = customTokensMerger, - userTokensResponseStore = userTokensResponseStore, - userTokensSaver = userTokensSaver, - cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, - expressServiceFetcher = expressServiceFetcher, - dispatchers = TestingCoroutineDispatcherProvider(), - ) - - @BeforeEach - fun resetMocks() { - clearMocks( - userWalletsListRepository, - tangemTechApi, - userTokensResponseStore, - userTokensSaver, - cardCryptoCurrencyFactory, - expressServiceFetcher, - ) - } - - @Test - fun `fetch failure if UserWallet ISN'T MULTI-CURRENCY wallet`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns false - } - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - - // Act - val actual = fetcher(params) - - // Assert - val expected = IllegalStateException( - "${DefaultMultiWalletCryptoCurrenciesFetcher::class.simpleName} supports only multi-currency wallet", - ).left() - assertEither(actual, expected) - - verifyOrder { userWalletsListRepository.userWallets } - coVerify(inverse = true) { - userTokensResponseStore.getSyncOrNull(any()) - } - } - - @Test - fun `fetch successfully if CARD IS DEMO and STORED TOKENS ARE EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "AC01000000041225" - } - - val defaultCoins = listOf( - cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), - cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), - ) - - val userTokensResponse = UserTokensResponse( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - tokens = listOf( - userTokensResponseFactory.createResponseToken(defaultCoins.first()), - userTokensResponseFactory.createResponseToken(defaultCoins.last()), - ), - ) - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns null - every { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - } returns defaultCoins - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - } returns userTokensResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = userTokensResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId()) - } - } - - @Test - fun `fetch successfully if CARD IS DEMO and STORED TOKENS AREN'T EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "AC01000000041225" - } - - val apiResponse = ApiResponse.Success( - data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), - ) - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) } returns defaultResponse - coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) - } returns apiResponse.data - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = defaultResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - userTokensResponseStore.getSyncOrNull(userWalletId = params.userWalletId) - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) - userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId()) - } - - coVerify(inverse = true) { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - } - } - - @Test - fun `fetch successfully if CARD ISN'T DEMO`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - val apiResponse = ApiResponse.Success( - data = defaultResponse.copy(group = UserTokensResponse.GroupType.TOKEN), - ) - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) - } returns apiResponse.data - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = apiResponse.data.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = apiResponse.data) - userTokensSaver.store(userWalletId = params.userWalletId, response = apiResponse.data) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId()) - } - - coVerify(inverse = true) { - userTokensResponseStore.getSyncOrNull(userWalletId = any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - } - } - - @Test - fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - @Suppress("UNCHECKED_CAST") - val apiResponse = ApiResponse.Error( - cause = ApiResponseError.TimeoutException(), - ) as ApiResponse - - val defaultCoins = listOf( - cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), - cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), - ) - - val userTokensResponse = UserTokensResponse( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - tokens = listOf( - userTokensResponseFactory.createResponseToken(defaultCoins.first()), - userTokensResponseFactory.createResponseToken(defaultCoins.last()), - ), - ) - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null - coEvery { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - } returns defaultCoins - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - } returns userTokensResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = userTokensResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId()) - } - - coVerify(inverse = true) { - userTokensSaver.push(userWalletId = any(), response = any()) - } - } - - @Test - fun `fetch successfully if API request RETURNS TIMEOUT EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - @Suppress("UNCHECKED_CAST") - val apiResponse = ApiResponse.Error( - cause = ApiResponseError.TimeoutException(), - ) as ApiResponse - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) - } returns defaultResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = defaultResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId()) - } - - coVerify(inverse = true) { - userTokensSaver.push(userWalletId = any(), response = any()) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - } - } - - @Test - fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS ARE EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - @Suppress("UNCHECKED_CAST") - val apiResponse = ApiResponse.Error( - cause = ApiResponseError.HttpException( - code = ApiResponseError.HttpException.Code.NOT_FOUND, - message = null, - errorBody = null, - ), - ) as ApiResponse - - val defaultCoins = listOf( - cryptoCurrencyFactory.createCoin(Blockchain.Bitcoin), - cryptoCurrencyFactory.createCoin(Blockchain.Ethereum), - ) - - val userTokensResponse = UserTokensResponse( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - tokens = listOf( - userTokensResponseFactory.createResponseToken(defaultCoins.first()), - userTokensResponseFactory.createResponseToken(defaultCoins.last()), - ), - ) - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns null - coEvery { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - } returns defaultCoins - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - } returns userTokensResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = userTokensResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(mockUserWallet) - userTokensSaver.push(userWalletId = params.userWalletId, response = userTokensResponse) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = userTokensResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = userTokensResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = userTokensResponse.toAssetId()) - } - } - - @Test - fun `fetch successfully if API request RETURNS NOT FOUND EXCEPTION and STORED TOKENS AREN'T EMPTY`() = runTest { - // Arrange - val params = MultiWalletCryptoCurrenciesFetcher.Params(userWalletId = userWalletId) - - val mockUserWallet = mockk { - every { walletId } returns userWalletId - every { isMultiCurrency } returns true - every { cardId } returns "cardID" - } - - @Suppress("UNCHECKED_CAST") - val apiResponse = ApiResponse.Error( - cause = ApiResponseError.HttpException( - code = ApiResponseError.HttpException.Code.NOT_FOUND, - message = null, - errorBody = null, - ), - ) as ApiResponse - - val userWalletsFlow = MutableStateFlow(listOf(mockUserWallet)) - - every { userWalletsListRepository.userWallets } returns userWalletsFlow - coEvery { tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) } returns apiResponse - coEvery { userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) } returns defaultResponse - coEvery { - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) - } returns defaultResponse - - coEvery { - expressServiceFetcher.fetch( - userWallet = mockUserWallet, - assetIds = defaultResponse.toAssetId(), - ) - } returns Unit.right() - - // Act - val actual = fetcher(params) - - // Assert - val expected = Unit.right() - assertEither(actual, expected) - - coVerifyOrder { - userWalletsListRepository.userWallets - tangemTechApi.getUserTokens(userId = params.userWalletId.stringValue) - userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - userTokensSaver.push(userWalletId = params.userWalletId, response = defaultResponse) - customTokensMerger.mergeIfPresented(userWalletId = params.userWalletId, response = defaultResponse) - userTokensSaver.store(userWalletId = params.userWalletId, response = defaultResponse) - expressServiceFetcher.fetch(userWallet = mockUserWallet, assetIds = defaultResponse.toAssetId()) - } - - coVerify(inverse = true) { - cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet(any()) - } - } - - private companion object { - val userWalletId = UserWalletId("011") - - val defaultResponse = UserTokensResponse( - group = UserTokensResponse.GroupType.NONE, - sort = UserTokensResponse.SortType.MANUAL, - tokens = listOf( - UserTokensResponse.Token( - id = null, - networkId = "bitcoin", - derivationPath = null, - name = "Bitcoin", - symbol = "BTC", - decimals = 8, - contractAddress = null, - addresses = listOf(), - ), - ), - ) - - fun UserTokensResponse.toAssetId(): Set { - return tokens.mapTo(hashSetOf()) { token -> - ExpressAsset.ID( - networkId = token.networkId, - contractAddress = token.contractAddress, - ) - } - } - } -} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt b/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt deleted file mode 100644 index 289b2a02e6..0000000000 --- a/domain/account/src/main/java/com/tangem/domain/account/featuretoggle/AccountsFeatureToggles.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.tangem.domain.account.featuretoggle - -/** - * Accounts feature toggle - * -[REDACTED_AUTHOR] - */ -interface AccountsFeatureToggles { - - val isFeatureEnabled: Boolean -} \ No newline at end of file diff --git a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt index 2a0bfba4d0..94e6e051af 100644 --- a/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt +++ b/domain/account/src/main/java/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCase.kt @@ -2,7 +2,6 @@ package com.tangem.domain.account.usecase import arrow.core.Option import arrow.core.getOrElse -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.loadAndGet @@ -18,20 +17,16 @@ import kotlinx.coroutines.flow.* * * @property crudRepository repository to perform CRUD operations on accounts. * @property userWalletsListRepository repository to get the list of user wallets. - * @property accountsFeatureToggles feature toggles for accounts. * [REDACTED_AUTHOR] */ class IsAccountsModeEnabledUseCase( private val crudRepository: AccountsCRUDRepository, private val userWalletsListRepository: UserWalletsListRepository, - private val accountsFeatureToggles: AccountsFeatureToggles, ) { @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke(): Flow { - if (!accountsFeatureToggles.isFeatureEnabled) return flowOf(value = false) - return userWalletsListRepository.loadAndGet() .flatMapLatest { userWallets -> val totalAccountsCountList = getTotalAccountsCountList(userWallets) @@ -43,8 +38,6 @@ class IsAccountsModeEnabledUseCase( } suspend fun invokeSync(): Boolean { - if (!accountsFeatureToggles.isFeatureEnabled) return false - return userWalletsListRepository.userWallets.value.orEmpty() .map { userWallet -> // If the wallet does not support multiple currencies, we consider its account count as 0 diff --git a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt index 750b29926b..894d4ec39d 100644 --- a/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt +++ b/domain/account/src/test/kotlin/com/tangem/domain/account/usecase/IsAccountsModeEnabledUseCaseTest.kt @@ -3,7 +3,6 @@ package com.tangem.domain.account.usecase import arrow.core.none import arrow.core.some import com.google.common.truth.Truth -import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet @@ -12,7 +11,6 @@ import com.tangem.domain.models.wallet.isMultiCurrency import io.mockk.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.AfterEach @@ -26,47 +24,26 @@ class IsAccountsModeEnabledUseCaseTest { private val accountsCRUDRepository: AccountsCRUDRepository = mockk() private val userWalletsListRepository: UserWalletsListRepository = mockk(relaxUnitFun = true) - private val featureToggles: AccountsFeatureToggles = mockk() private val useCase = IsAccountsModeEnabledUseCase( crudRepository = accountsCRUDRepository, userWalletsListRepository = userWalletsListRepository, - accountsFeatureToggles = featureToggles, ) @AfterEach fun tearDown() { - clearMocks(userWalletsListRepository, accountsCRUDRepository, featureToggles) + clearMocks(userWalletsListRepository, accountsCRUDRepository) } @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class Invoke { - @Test - fun `returns false when feature is disabled`() = runTest { - // Arrange - every { featureToggles.isFeatureEnabled } returns false - - // Act - val actual = useCase.invoke().firstOrNull() - - // Assert - Truth.assertThat(actual).isFalse() - - verify(exactly = 1) { featureToggles.isFeatureEnabled } - coVerify(inverse = true) { - userWalletsListRepository.load() - userWalletsListRepository.userWallets - } - } - @Test fun `returns false when loadAndGet emits one wallet with isMultiCurrency false`() = runTest { // Arrange val wallet = createUserWallet(isMultiCurrency = false) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) // Act @@ -76,7 +53,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.load() userWalletsListRepository.userWallets } @@ -89,7 +65,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(2.some()) @@ -100,7 +75,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isTrue() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.load() userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) @@ -112,7 +86,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) } returns flowOf(none()) @@ -123,7 +96,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.load() userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet.walletId) @@ -136,7 +108,6 @@ class IsAccountsModeEnabledUseCaseTest { val wallet1 = createUserWallet(isMultiCurrency = false) val wallet2 = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets } returns MutableStateFlow(listOf(wallet1, wallet2)) every { accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) } returns flowOf(2.some()) @@ -147,7 +118,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isTrue() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.load() userWalletsListRepository.userWallets accountsCRUDRepository.getTotalActiveAccountsCount(wallet2.walletId) @@ -161,25 +131,9 @@ class IsAccountsModeEnabledUseCaseTest { @TestInstance(TestInstance.Lifecycle.PER_CLASS) inner class InvokeSync { - @Test - fun `returns false when feature is disabled`() = runTest { - // Arrange - every { featureToggles.isFeatureEnabled } returns false - - // Act - val actual = useCase.invokeSync() - - // Assert - Truth.assertThat(actual).isFalse() - - verify(exactly = 1) { featureToggles.isFeatureEnabled } - verify(inverse = true) { userWalletsListRepository.userWallets.value } - } - @Test fun `returns false when getUserWalletsSync returns empty list`() = runTest { // Arrange - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns emptyList() // Act @@ -189,7 +143,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() verifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value } @@ -201,7 +154,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = false) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns listOf(wallet) // Act @@ -211,7 +163,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() verifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value } @@ -223,7 +174,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns listOf(wallet) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns 2.some() @@ -234,7 +184,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isTrue() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } @@ -245,7 +194,6 @@ class IsAccountsModeEnabledUseCaseTest { // Arrange val wallet = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns listOf(wallet) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } returns none() @@ -256,7 +204,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isFalse() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet.walletId) } @@ -268,7 +215,6 @@ class IsAccountsModeEnabledUseCaseTest { val wallet1 = createUserWallet(isMultiCurrency = false) val wallet2 = createUserWallet(isMultiCurrency = true) - every { featureToggles.isFeatureEnabled } returns true every { userWalletsListRepository.userWallets.value } returns listOf(wallet1, wallet2) coEvery { accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId) } returns 2.some() @@ -279,7 +225,6 @@ class IsAccountsModeEnabledUseCaseTest { Truth.assertThat(actual).isTrue() coVerifyOrder { - featureToggles.isFeatureEnabled userWalletsListRepository.userWallets.value accountsCRUDRepository.getTotalActiveAccountsCountSync(wallet2.walletId) } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/PortfolioId.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/PortfolioId.kt index 07b9e3d022..a140670520 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/PortfolioId.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/PortfolioId.kt @@ -7,11 +7,11 @@ import kotlinx.serialization.Serializable /** * Temporary wrapper over ID to support a gradual migration between two modes: * - * - [Wallet] — legacy flow, wallet design; used when the [AccountsFeatureToggles] is disabled. - * - [Account] — new flow, wallet/account design; used when the [AccountsFeatureToggles] is enabled. + * - [Wallet] — legacy flow, wallet design. + * - [Account] — new flow, wallet/account design. * - * ⚠️ When an [Account] you must verify the current app mode with [IsAccountsModeEnabledUseCase] - * and then use wallet/account design + * ⚠️ When using an [Account], you must verify the current app mode with [IsAccountsModeEnabledUseCase], + * then use the wallet/account design. * * Intended to be removed after the full migration to the new mode. */ From 26a6da1b8527675db3aff21cf61d1e6c391467e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Mar 2026 11:58:10 +0300 Subject: [PATCH 92/97] Updated on 2026-08-14 --- .../store/DefaultNetworksStatusesStore.kt | 68 +++++++++-- .../store/StoreAdaptiveThrottleTest.kt | 109 ++++++++++++++++++ .../DefaultSingleAccountStatusListProducer.kt | 23 +--- test/core/build.gradle.kts | 1 + 4 files changed, 174 insertions(+), 27 deletions(-) create mode 100644 data/networks/src/test/java/com/tangem/data/networks/store/StoreAdaptiveThrottleTest.kt diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index 1131c43dfc..7961e06c77 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -13,13 +13,8 @@ import com.tangem.domain.models.network.NetworkStatus import com.tangem.domain.models.wallet.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import timber.log.Timber import java.io.File @@ -67,6 +62,8 @@ internal class DefaultNetworksStatusesStore( override fun get(userWalletId: UserWalletId): Flow> { return runtimeStore.get().mapNotNull { it[userWalletId.stringValue] } + .adaptiveThrottle() + .conflate() } override suspend fun getSyncOrNull(userWalletId: UserWalletId, network: Network): SimpleNetworkStatus? { @@ -180,4 +177,61 @@ internal class DefaultNetworksStatusesStore( } } } +} + +@Suppress("MagicNumber") +internal fun Flow>.adaptiveThrottle(): Flow> = channelFlow { + var accumulator: Set? = null + var lastEmitTime = 0L + + // params that control maximum emissions that can be throttled + var densityLevel = 0 + val maxDensity = 10 + + // params that control maximum delay and growth of delay between emissions + var lastDelay = 0L + val maxDelay = 1500L + val growthFactor = 250L + + fun resetThrottling() { + lastDelay = 0L + densityLevel = 0 + } + + this@adaptiveThrottle.collectLatest { newSet -> + val previousSet: Collection? = accumulator + accumulator = newSet + + when { + // first value, just emit + previousSet == null -> resetThrottling() + // changed size, just emit + previousSet.size != newSet.size -> resetThrottling() + + // apply adaptive throttling + else -> { + val networksCount = newSet.size + // more networks - more throttling + val cooldownThreshold = when { + networksCount in 10..25 -> 300L + networksCount > 25 -> 500L + // 0..9 networks + else -> 100L + } + + val now = System.currentTimeMillis() + val timeSinceLastEmit = now - lastEmitTime + if (timeSinceLastEmit < cooldownThreshold && densityLevel < maxDensity) { + lastDelay = (lastDelay + growthFactor).coerceAtMost(maximumValue = maxDelay) + densityLevel += 1 + delay(lastDelay) + } else { + resetThrottling() + } + } + } + + lastEmitTime = System.currentTimeMillis() + channel.send(newSet) + } } \ No newline at end of file diff --git a/data/networks/src/test/java/com/tangem/data/networks/store/StoreAdaptiveThrottleTest.kt b/data/networks/src/test/java/com/tangem/data/networks/store/StoreAdaptiveThrottleTest.kt new file mode 100644 index 0000000000..719df24b15 --- /dev/null +++ b/data/networks/src/test/java/com/tangem/data/networks/store/StoreAdaptiveThrottleTest.kt @@ -0,0 +1,109 @@ +package com.tangem.data.networks.store + +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +internal class StoreAdaptiveThrottleTest { + + @Test + fun `first value is emitted immediately`() = runTest { + val flow = flowOf(setOf(1, 2, 3)).adaptiveThrottle() + + flow.test { + val item = awaitItem() + assertThat(item).isEqualTo(setOf(1, 2, 3)) + awaitComplete() + } + } + + @Test + fun `size change bypasses throttling`() = runTest { + val upstream = MutableSharedFlow>() + + upstream.adaptiveThrottle().test { + + upstream.emit(setOf(1, 2)) + assertThat(awaitItem()).isEqualTo(setOf(1, 2)) + + upstream.emit(setOf(1, 2, 3)) + assertThat(awaitItem()).isEqualTo(setOf(1, 2, 3)) + + upstream.emit(setOf(1)) + assertThat(awaitItem()).isEqualTo(setOf(1)) + } + } + + @Test + fun `same size events trigger throttling delay`() = runTest { + val upstream = MutableSharedFlow>() + + upstream.adaptiveThrottle().test { + + upstream.emit(setOf(1, 2)) + awaitItem() + + upstream.emit(setOf(3, 4)) + + // delay should happen + expectNoEvents() + advanceTimeBy(250) + + val item = awaitItem() + assertThat(item).isEqualTo(setOf(3, 4)) + } + } + + @Test + fun `rapid events result in only latest emission due to collectLatest`() = runTest { + val upstream = MutableSharedFlow>() + + upstream.adaptiveThrottle().test { + + upstream.emit(setOf(1, 2)) + awaitItem() + + launch { + upstream.emit(setOf(3, 4)) + upstream.emit(setOf(5, 6)) + upstream.emit(setOf(7, 8)) + } + + // delay should happen + expectNoEvents() + advanceTimeBy(250) + + val item = awaitItem() + assertThat(item).isEqualTo(setOf(7, 8)) + } + } + + @Test + fun `throttling resets when cooldown window passed`() = runTest { + val upstream = MutableSharedFlow>() + + upstream.adaptiveThrottle().test { + + upstream.emit(setOf(1, 2)) + awaitItem() + + upstream.emit(setOf(3, 4)) + expectNoEvents() + advanceTimeBy(250) + awaitItem() + + // wait long enough to reset throttling + advanceTimeBy(2000) + + upstream.emit(setOf(5, 6)) + + val item = awaitItem() + assertThat(item).isEqualTo(setOf(5, 6)) + } + } +} \ No newline at end of file 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 be5c3f927d..5250d458de 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 @@ -164,7 +164,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo flattenCurrency: MutableSharedFlow>, ): Flow> { val walletId = userWallet.walletId - val networkStatusFlow: SharedFlow> = networkStatusFlow(walletId, flattenCurrency) + val networkStatusFlow: SharedFlow> = networkStatusFlow(walletId) .shareIn(this, started = SharingStarted.Eagerly, replay = 1) val stakingBalanceFlow: SharedFlow>> = stakingFlow(userWallet) .shareIn(this, started = SharingStarted.Eagerly, replay = 1) @@ -213,27 +213,10 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo } } - private fun networkStatusFlow( - walletId: UserWalletId, - flattenCurrency: MutableSharedFlow>, - ): Flow> = channelFlow { - val currencyCount = flattenCurrency - .map { map -> map.size } - .stateIn(this, SharingStarted.Eagerly, 0) - + private fun networkStatusFlow(walletId: UserWalletId): Flow> = networkStatusSupplier(MultiNetworkStatusProducer.Params(walletId)) - // todo accounts high frequency, investigate better debounce - .debounce { - val count = currencyCount.value - @Suppress("MagicNumber") when { - count in 10..25 -> 50L - count > 25 -> 100L - else -> 0 - } - } .mapLatest { statuses -> statuses.associateBy { status -> status.network.id } } - .distinctUntilChanged().collect { result -> channel.send(result) } - } + .distinctUntilChanged() private fun stakingFlow(wallet: UserWallet): Flow>> = if (!wallet.isMultiCurrency) { diff --git a/test/core/build.gradle.kts b/test/core/build.gradle.kts index d34ffdd2aa..ad834bcedb 100644 --- a/test/core/build.gradle.kts +++ b/test/core/build.gradle.kts @@ -10,4 +10,5 @@ dependencies { api(deps.test.junit5) api(deps.test.mockk) api(deps.test.truth) + api(deps.test.turbine) } \ No newline at end of file From 428c0a56b0f1ff3f69bd4ddfe77037fe6f55f244 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Mar 2026 10:17:34 +0400 Subject: [PATCH 93/97] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../EnvironmentConfigGenerator.kt | 60 ++++++++++--- .../EnvironmentConfigGeneratorTest.kt | 88 +++++++++++++++++++ 3 files changed, 137 insertions(+), 13 deletions(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index e4fac168bd..b693d601d9 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit e4fac168bd941fe90b0f081dfa70ea1b4148b49f +Subproject commit b693d601d9dc3de27f0536574bce59a6aa5e2e89 diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt index 5c6d9e6e79..93e83b4e87 100644 --- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt +++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGenerator.kt @@ -4,6 +4,7 @@ import com.squareup.kotlinpoet.* import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import kotlinx.serialization.json.* import java.io.File +import java.util.Locale /** * Generator for environment configuration Kotlin object from JSON file. @@ -68,12 +69,13 @@ object EnvironmentConfigGenerator { * Adds a property to the TypeSpec based on the JSON value type */ private fun addPropertyFromJsonValue(builder: TypeSpec.Builder, name: String, value: JsonElement) { + val propertyName = name.toValidIdentifier() when (value) { is JsonPrimitive -> { when { value.isString -> { val stringValue = value.content - val propertySpec = PropertySpec.builder(name, STRING) + val propertySpec = PropertySpec.builder(propertyName, STRING) .addModifiers(KModifier.CONST) .initializer("%S", stringValue) @@ -81,7 +83,7 @@ object EnvironmentConfigGenerator { } value.booleanOrNull != null -> { builder.addProperty( - PropertySpec.builder(name, BOOLEAN) + PropertySpec.builder(propertyName, BOOLEAN) .addModifiers(KModifier.CONST) .initializer("%L", value.boolean) .build() @@ -89,7 +91,7 @@ object EnvironmentConfigGenerator { } value.longOrNull != null -> { builder.addProperty( - PropertySpec.builder(name, LONG) + PropertySpec.builder(propertyName, LONG) .addModifiers(KModifier.CONST) .initializer("%L", value.long) .build() @@ -97,7 +99,7 @@ object EnvironmentConfigGenerator { } value.doubleOrNull != null -> { builder.addProperty( - PropertySpec.builder(name, DOUBLE) + PropertySpec.builder(propertyName, DOUBLE) .addModifiers(KModifier.CONST) .initializer("%L", value.double) .build() @@ -106,7 +108,7 @@ object EnvironmentConfigGenerator { else -> { // Null value builder.addProperty( - PropertySpec.builder(name, STRING.copy(nullable = true)) + PropertySpec.builder(propertyName, STRING.copy(nullable = true)) .initializer("null") .build() ) @@ -117,7 +119,7 @@ object EnvironmentConfigGenerator { val listType = LIST.parameterizedBy(STRING) val values = value.map { it.jsonPrimitive.content } builder.addProperty( - PropertySpec.builder(name, listType) + PropertySpec.builder(propertyName, listType) .initializer( CodeBlock.builder() .add("listOf(\n") @@ -147,14 +149,48 @@ object EnvironmentConfigGenerator { } /** - * Converts a string to PascalCase, handling dashes and underscores. - * Examples: "cosmos-hub" -> "CosmosHub", "polygon-zkevm" -> "PolygonZkevm" + * Converts a string to PascalCase for use as a class/object name. + * - If the string contains separators (dots, dashes, underscores), splits and joins in PascalCase + * - If no separators, just capitalizes the first letter to preserve original casing (e.g., "AppsFlyer" stays "AppsFlyer") */ private fun String.toPascalCase(): String { - return this.split("-", "_") + val hasSeparators = contains('.') || contains('-') || contains('_') + return if (hasSeparators) { + this.split("-", "_", ".") + .filter { it.isNotEmpty() } + .joinToString("") { part -> + part.lowercase(Locale.ROOT).replaceFirstChar { it.uppercase(Locale.ROOT) } + } + } else { + this.replaceFirstChar { it.uppercase(Locale.ROOT) } + } + } + + /** + * Converts a string to a valid Kotlin property identifier. + * - If the string contains dots, converts to camelCase (dots cannot be escaped by KotlinPoet) + * - Otherwise, ensures the first letter is lowercase (Kotlin property naming convention) + */ + private fun String.toValidIdentifier(): String { + return if (contains('.')) { + toCamelCase() + } else { + this.replaceFirstChar { it.lowercase(Locale.ROOT) } + } + } + + /** + * Converts a string to camelCase, handling dashes, underscores, and dots. + * Normalizes each segment to lowercase first for consistent results. + * Examples: "cosmos-hub" -> "cosmosHub", "customer.io" -> "customerIo", "CUSTOMER.IO" -> "customerIo" + */ + private fun String.toCamelCase(): String { + val parts = this.split("-", "_", ".") .filter { it.isNotEmpty() } - .joinToString("") { part -> - part.replaceFirstChar { it.uppercase() } - } + return parts.mapIndexed { index, part -> + val normalized = part.lowercase(Locale.ROOT) + if (index == 0) normalized + else normalized.replaceFirstChar { it.uppercase(Locale.ROOT) } + }.joinToString("") } } diff --git a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt index bfcccb71a8..dae3182b9d 100644 --- a/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt +++ b/plugins/configuration/src/test/kotlin/com/tangem/plugin/configuration/configurations/EnvironmentConfigGeneratorTest.kt @@ -210,6 +210,32 @@ class EnvironmentConfigGeneratorTest { assertThat(generatedCode).contains("""const val deepValue: String = "deep"""") } + @Test + fun `generate preserves camelCase object names without separators`() { + // Arrange - object names like "AppsFlyer" should stay as "AppsFlyer", not become "Appsflyer" + val json = """ + { + "AppsFlyer": { + "DevKey": "key123" + }, + "GetBlockAccessTokens": { + "ethereum": { + "jsonRpc": "token" + } + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert - object names preserved, property names have first letter lowercased + assertThat(generatedCode).contains("object AppsFlyer {") + assertThat(generatedCode).contains("""const val devKey: String = "key123"""") + assertThat(generatedCode).contains("object GetBlockAccessTokens {") + assertThat(generatedCode).contains("object Ethereum {") + } + @Test fun `generate converts dash-separated names to PascalCase`() { // Arrange @@ -451,6 +477,68 @@ class EnvironmentConfigGeneratorTest { assertThat(generatedCode).contains("object QuickNodeSolanaCredentials {") } + @Test + fun `generate converts dot-separated object names to PascalCase`() { + // Arrange - testing the customer.io case that caused the original build failure + val json = """ + { + "customer.io": { + "TrackSiteID": "site-id-123", + "TrackApiKey": "api-key-456" + } + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert + assertThat(generatedCode).contains("object CustomerIo {") + // Property names have first letter lowercased (Kotlin convention) + assertThat(generatedCode).contains("""const val trackSiteID: String = "site-id-123"""") + assertThat(generatedCode).contains("""const val trackApiKey: String = "api-key-456"""") + } + + @Test + fun `generate converts dot-separated property names to camelCase`() { + // Arrange - testing property names with dots (not nested objects) + val json = """ + { + "api.key": "test-key", + "service.url": "https://example.com" + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert - dots in property names are converted to camelCase + assertThat(generatedCode).contains("""const val apiKey: String = "test-key"""") + assertThat(generatedCode).contains("""const val serviceUrl: String = "https://example.com"""") + } + + @Test + fun `generate preserves valid property names without transformation`() { + // Arrange - valid Kotlin identifiers should not be transformed + val json = """ + { + "apiKey": "key1", + "moonPayApiKey": "moon-pay-key", + "isEnabled": true, + "maxRetryCount": 5 + } + """.trimIndent() + + // Act + val generatedCode = generateAndReadOutput(json) + + // Assert - original names preserved exactly + assertThat(generatedCode).contains("""const val apiKey: String = "key1"""") + assertThat(generatedCode).contains("""const val moonPayApiKey: String = "moon-pay-key"""") + assertThat(generatedCode).contains("const val isEnabled: Boolean = true") + assertThat(generatedCode).contains("const val maxRetryCount: Long = 5") + } + private fun generateAndReadOutput(jsonContent: String): String { val inputFile = File(tempDir, "config.json").apply { writeText(jsonContent) From 5ac3ff2272619d006583bea031b19fee82cbd137 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Mar 2026 10:45:45 +0100 Subject: [PATCH 94/97] Updated on 2026-08-14 --- .../state/transformers/EarnFilterSelectedStateTransformer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt index 3258d8f251..5e8c4ef1c5 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/earn/state/transformers/EarnFilterSelectedStateTransformer.kt @@ -16,7 +16,7 @@ internal class EarnFilterSelectedStateTransformer( earnFilterUM = prevState.earnFilterUM.copy( selectedTypeFilter = filterType, selectedNetworkFilter = filterNetwork, - isNetworkFilterEnabled = earnNetworks.isRight(), + isNetworkFilterEnabled = earnNetworks.isRight { networks -> networks.isNotEmpty() }, isTypeFilterEnabled = true, ), ) From 90abe449d69853cec8e9ecb6bd8b513a55dffc3e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Mar 2026 15:19:16 +0300 Subject: [PATCH 95/97] Updated on 2026-08-14 --- .../tap/di/domain/WalletsDomainModule.kt | 6 + .../converter/WalletIconUMConverter.kt | 55 + .../com/tangem/core/ui/ds/image/DeviceIcon.kt | 202 +++ .../tangem/core/ui/ds/image/DeviceIconUM.kt | 24 + .../ui/ds/image/WalletIconVectorBuilders.kt | 1131 +++++++++++++++++ .../com/tangem/core/ui/res/TangemColors2.kt | 4 + .../tangem/core/ui/res/TangemThemeRedesign.kt | 2 + .../ui/src/main/res/drawable/ic_shield_24.xml | 5 + .../domain/models/wallet/UserWalletIcon.kt | 21 + .../wallets/usecase/GetWalletIconUseCase.kt | 242 ++++ .../wallet/child/wallet/model/WalletModel.kt | 7 + .../preview/WalletBalancePreview.kt | 4 + .../wallet/domain/WalletImageResolver.kt | 1 + .../wallet/state/model/WalletBalanceUM.kt | 7 + .../transformers/AddWalletTransformer.kt | 3 + .../InitializeWalletsTransformer.kt | 6 + .../ReinitializeNewWalletTransformer.kt | 3 + .../ReinitializeWalletTransformer.kt | 3 + .../SetTokenListErrorTransformer.kt | 1 + .../transformers/UnlockWalletTransformer.kt | 3 + .../MultiWalletBalanceUMTransformer.kt | 3 + .../state/utils/WalletLoadingStateFactory.kt | 5 + .../ui/components/common/WalletBalance.kt | 14 +- 23 files changed, 1741 insertions(+), 11 deletions(-) create mode 100644 common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt create mode 100644 core/ui/src/main/res/drawable/ic_shield_24.xml create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWalletIcon.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletIconUseCase.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 5e55ebdd54..c63293cda2 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 @@ -66,6 +66,12 @@ internal object WalletsDomainModule { return GetUserWalletUseCase(userWalletsListRepository = userWalletsListRepository) } + @Provides + @Singleton + fun provideGetWalletIconUseCase(walletsRepository: WalletsRepository): GetWalletIconUseCase { + return GetWalletIconUseCase(walletsRepository = walletsRepository) + } + @Provides @Singleton fun providesGetSelectedWalletSyncUseCase( diff --git a/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt new file mode 100644 index 0000000000..0a7afcbb8d --- /dev/null +++ b/common/ui/src/main/java/com/tangem/common/ui/userwallet/converter/WalletIconUMConverter.kt @@ -0,0 +1,55 @@ +package com.tangem.common.ui.userwallet.converter + +import androidx.compose.ui.graphics.Color +import androidx.core.graphics.toColorInt +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.domain.models.wallet.UserWalletIcon +import com.tangem.utils.converter.Converter +import javax.inject.Inject + +/** + * Converter for mapping [UserWalletIcon] to [DeviceIconUM], + * which is used for displaying the wallet icon in the UI. + */ +class WalletIconUMConverter @Inject constructor() : Converter { + + override fun convert(value: UserWalletIcon): DeviceIconUM = with(value) { + fun String.parseHexColor(): Color = try { + Color(this.toColorInt()) + } catch (_: IllegalArgumentException) { + Color.Unspecified + } + + return when (this) { + UserWalletIcon.Hot -> DeviceIconUM.Mobile + is UserWalletIcon.Stub -> + DeviceIconUM.Stub(cardsCount = this.cardsCount) + is UserWalletIcon.Default -> if (isRing) { + DeviceIconUM.Ring( + mainColor = Color.Unspecified, + cardColor = Color.Unspecified, + secondCardColor = if (cardsCount > 2) Color.Unspecified else null, + ) + } else { + DeviceIconUM.Card( + mainColor = Color.Unspecified, + secondColor = if (cardsCount > 1) Color.Unspecified else null, + thirdColor = if (cardsCount > 2) Color.Unspecified else null, + ) + } + is UserWalletIcon.Colored -> if (isRing) { + DeviceIconUM.Ring( + mainColor = mainColor.parseHexColor(), + cardColor = secondColor?.parseHexColor(), + secondCardColor = thirdColor?.parseHexColor(), + ) + } else { + DeviceIconUM.Card( + mainColor = mainColor.parseHexColor(), + secondColor = secondColor?.parseHexColor(), + thirdColor = thirdColor?.parseHexColor(), + ) + } + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt new file mode 100644 index 0000000000..6d6d6f046d --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIcon.kt @@ -0,0 +1,202 @@ +package com.tangem.core.ui.ds.image + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.takeOrElse +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign + +/** + * Composable function for displaying a wallet icon based on the provided [DeviceIconUM] state. + * + * The icon can represent different types of devices, such as cards, rings, stubs, or mobile wallet, + * with customizable colors and styles. + * + * @param state The state of the device icon, which determines its appearance. + * @param modifier Optional [Modifier] for styling the composable. + */ +@Composable +fun TangemDeviceIcon(state: DeviceIconUM, modifier: Modifier = Modifier) { + when (state) { + is DeviceIconUM.Card -> DeviceIcon( + modifier = modifier, + isRing = false, + mainColor = state.mainColor, + secondColor = state.secondColor, + thirdColor = state.thirdColor, + tColor = null, + ) + is DeviceIconUM.Ring -> DeviceIcon( + modifier = modifier, + isRing = true, + mainColor = state.mainColor, + secondColor = state.cardColor, + thirdColor = state.secondCardColor, + tColor = null, + ) + is DeviceIconUM.Stub -> DeviceIcon( + modifier = modifier, + isRing = false, + mainColor = Color.Unspecified, + secondColor = Color.Unspecified.takeIf { state.cardsCount > 1 }, + thirdColor = Color.Unspecified.takeIf { state.cardsCount > 2 }, + tColor = TangemTheme.colors2.graphic.neutral.secondary, + ) + DeviceIconUM.Mobile -> Icon( + modifier = modifier, + imageVector = ImageVector.vectorResource(R.drawable.ic_shield_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.status.attention, + ) + } +} + +@Composable +private fun DeviceIcon( + isRing: Boolean, + mainColor: Color, + secondColor: Color?, + thirdColor: Color?, + tColor: Color?, + modifier: Modifier = Modifier, +) { + val main = mainColor.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant } + val second = secondColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant } + val third = thirdColor?.takeOrElse { TangemTheme.colors2.graphic.neutral.tertiaryConstant } + val borderColor = TangemTheme.colors2.border.walletIcon + + val imageVector = remember(isRing, main, second, third, borderColor, tColor) { + when { + isRing && second != null && third != null -> WalletIconVectorBuilders.buildRingWithCard2( + mainColor = main, + cardColor = second, + secondCardColor = third, + borderColor = borderColor, + ) + !isRing && second != null && third != null -> WalletIconVectorBuilders.buildCard3( + mainColor = main, + secondColor = second, + thirdColor = third, + tColor = tColor, + borderColor = borderColor, + ) + isRing && second != null -> WalletIconVectorBuilders.buildRingWithCard( + mainColor = main, + cardColor = second, + borderColor = borderColor, + ) + !isRing && second != null -> WalletIconVectorBuilders.buildCard2( + mainColor = main, + secondColor = second, + tColor = tColor, + borderColor = borderColor, + ) + isRing -> WalletIconVectorBuilders.buildRing( + mainColor = main, + borderColor = borderColor, + ) + else -> WalletIconVectorBuilders.buildCard( + mainColor = main, + borderColor = borderColor, + tColor = tColor, + ) + } + } + + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = modifier, + tint = Color.Unspecified, + ) +} + +// region Preview + +private val previewCardBlue + get() = Color(0xFF1C5FBF) +private val previewCardGold + get() = Color(0xFFD4A017) +private val previewCardPurple + get() = Color(0xFF7B2FBE) +private val previewRingGreen + get() = Color(0xFF2ECC71) + +private val previewStates: List> + get() = listOf( + "Card 1" to DeviceIconUM.Card( + mainColor = previewCardBlue, + secondColor = null, + ), + "Card 2" to DeviceIconUM.Card( + mainColor = previewCardBlue, + secondColor = previewCardGold, + ), + "Card 3" to DeviceIconUM.Card( + mainColor = previewCardBlue, + secondColor = previewCardGold, + thirdColor = previewCardPurple, + ), + "Ring" to DeviceIconUM.Ring( + mainColor = previewRingGreen, + ), + "Ring + Card" to DeviceIconUM.Ring( + mainColor = previewRingGreen, + cardColor = previewCardBlue, + ), + "Ring + 2 Cards" to DeviceIconUM.Ring( + mainColor = previewRingGreen, + cardColor = previewCardBlue, + secondCardColor = previewCardGold, + ), + "Stub 1" to DeviceIconUM.Stub(cardsCount = 1), + "Stub 2" to DeviceIconUM.Stub(cardsCount = 2), + "Stub 3" to DeviceIconUM.Stub(cardsCount = 3), + "Mobile" to DeviceIconUM.Mobile, + ) + +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemDeviceIcon_Preview() { + TangemThemePreviewRedesign { + Column( + modifier = Modifier + .background(TangemTheme.colors.background.primary) + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + previewStates.forEach { (label, state) -> + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + TangemDeviceIcon( + modifier = Modifier.size(40.dp), + state = state, + ) + Text( + text = label, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + } + } + } + } +} + +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt new file mode 100644 index 0000000000..0007a0f1a8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/DeviceIconUM.kt @@ -0,0 +1,24 @@ +package com.tangem.core.ui.ds.image + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +sealed interface DeviceIconUM { + + data class Card( + val mainColor: Color, + val secondColor: Color?, + val thirdColor: Color? = null, + ) : DeviceIconUM + + data class Ring( + val mainColor: Color = Color.Unspecified, + val cardColor: Color? = null, + val secondCardColor: Color? = null, + ) : DeviceIconUM + + data class Stub(val cardsCount: Int) : DeviceIconUM + + data object Mobile : DeviceIconUM +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt new file mode 100644 index 0000000000..993fdf132f --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/image/WalletIconVectorBuilders.kt @@ -0,0 +1,1131 @@ +@file:Suppress("MagicNumber", "LargeClass", "LongMethod", "NamedArguments") +package com.tangem.core.ui.ds.image + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathFillType +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.PathData +import androidx.compose.ui.graphics.vector.group +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp + +/** + * Builders for creating [ImageVector] instances for wallet icons. + * These builders are used to generate the vector graphics for the wallet icons + * + * Generated based on SVG paths. + */ +internal object WalletIconVectorBuilders { + + fun buildRing(mainColor: Color, borderColor: Color): ImageVector = ImageVector.Builder( + name = "Ring", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = SolidColor(mainColor), + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(10.333f, 3f) + curveTo(7.94f, 3f, 6f, 7.029f, 6f, 12f) + curveTo(6f, 16.971f, 7.94f, 21f, 10.333f, 21f) + horizontalLineTo(14.667f) + curveTo(17.06f, 21f, 19f, 16.971f, 19f, 12f) + curveTo(19f, 7.029f, 17.06f, 3f, 14.667f, 3f) + horizontalLineTo(10.333f) + close() + moveTo(10.574f, 3.9f) + curveTo(16.403f, 3.9f, 16.498f, 20.1f, 10.574f, 20.1f) + curveTo(10.541f, 20.1f, 10.539f, 20.052f, 10.571f, 20.045f) + curveTo(11.037f, 19.937f, 11.478f, 19.675f, 11.883f, 19.285f) + curveTo(11.961f, 19.21f, 11.96f, 19.087f, 11.889f, 19.005f) + curveTo(10.671f, 17.602f, 9.852f, 14.99f, 9.852f, 12f) + curveTo(9.852f, 9.009f, 10.671f, 6.397f, 11.89f, 4.994f) + curveTo(11.961f, 4.913f, 11.961f, 4.789f, 11.883f, 4.714f) + curveTo(11.479f, 4.324f, 11.037f, 4.062f, 10.571f, 3.955f) + curveTo(10.539f, 3.948f, 10.541f, 3.9f, 10.574f, 3.9f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(14.667f, 3.5f) + horizontalLineTo(11.398f) + curveTo(12.705f, 3.818f, 13.671f, 4.88f, 14.316f, 6.213f) + curveTo(15.094f, 7.818f, 15.475f, 9.923f, 15.481f, 11.998f) + curveTo(15.488f, 14.073f, 15.118f, 16.179f, 14.343f, 17.786f) + curveTo(13.698f, 19.122f, 12.728f, 20.183f, 11.407f, 20.5f) + horizontalLineTo(14.667f) + curveTo(15.562f, 20.5f, 16.518f, 19.73f, 17.28f, 18.147f) + curveTo(18.025f, 16.6f, 18.5f, 14.427f, 18.5f, 12f) + curveTo(18.5f, 9.573f, 18.025f, 7.4f, 17.28f, 5.853f) + curveTo(16.518f, 4.27f, 15.562f, 3.5f, 14.667f, 3.5f) + close() + moveTo(10.264f, 3.503f) + curveTo(9.389f, 3.542f, 8.462f, 4.311f, 7.72f, 5.853f) + curveTo(6.975f, 7.4f, 6.5f, 9.573f, 6.5f, 12f) + curveTo(6.5f, 14.427f, 6.975f, 16.6f, 7.72f, 18.147f) + curveTo(8.462f, 19.688f, 9.388f, 20.457f, 10.263f, 20.496f) + curveTo(10.238f, 20.478f, 10.212f, 20.458f, 10.19f, 20.435f) + curveTo(10.094f, 20.332f, 10.054f, 20.208f, 10.049f, 20.101f) + curveTo(10.038f, 19.888f, 10.168f, 19.625f, 10.459f, 19.558f) + curveTo(10.748f, 19.491f, 11.04f, 19.341f, 11.329f, 19.106f) + curveTo(10.111f, 17.543f, 9.352f, 14.913f, 9.352f, 12f) + curveTo(9.352f, 9.086f, 10.111f, 6.455f, 11.33f, 4.892f) + curveTo(11.041f, 4.657f, 10.748f, 4.509f, 10.459f, 4.442f) + curveTo(10.171f, 4.376f, 10.038f, 4.115f, 10.049f, 3.898f) + curveTo(10.055f, 3.79f, 10.096f, 3.666f, 10.192f, 3.563f) + curveTo(10.214f, 3.54f, 10.239f, 3.521f, 10.264f, 3.503f) + close() + } + }.build() + + fun buildRingWithCard(mainColor: Color, cardColor: Color, borderColor: Color): ImageVector = ImageVector.Builder( + name = "RingWithCard", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = SolidColor(mainColor), + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(4.667f, 10f) + curveTo(3.194f, 10f, 2f, 12.462f, 2f, 15.5f) + curveTo(2f, 18.538f, 3.194f, 21f, 4.667f, 21f) + horizontalLineTo(7.333f) + curveTo(8.806f, 21f, 10f, 18.538f, 10f, 15.5f) + curveTo(10f, 12.462f, 8.806f, 10f, 7.333f, 10f) + horizontalLineTo(4.667f) + close() + moveTo(4.815f, 10.55f) + curveTo(8.402f, 10.55f, 8.46f, 20.45f, 4.815f, 20.45f) + curveTo(4.795f, 20.45f, 4.793f, 20.421f, 4.813f, 20.416f) + curveTo(5.099f, 20.351f, 5.371f, 20.19f, 5.62f, 19.952f) + curveTo(5.668f, 19.906f, 5.668f, 19.83f, 5.624f, 19.781f) + curveTo(4.874f, 18.923f, 4.37f, 17.327f, 4.37f, 15.5f) + curveTo(4.37f, 13.672f, 4.875f, 12.076f, 5.624f, 11.219f) + curveTo(5.668f, 11.169f, 5.668f, 11.093f, 5.62f, 11.047f) + curveTo(5.371f, 10.809f, 5.1f, 10.649f, 4.813f, 10.584f) + curveTo(4.793f, 10.579f, 4.795f, 10.55f, 4.815f, 10.55f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(7.333f, 10.5f) + horizontalLineTo(6.192f) + curveTo(6.657f, 10.835f, 7.021f, 11.328f, 7.29f, 11.878f) + curveTo(7.785f, 12.893f, 8.023f, 14.21f, 8.027f, 15.498f) + curveTo(8.031f, 16.786f, 7.801f, 18.106f, 7.307f, 19.122f) + curveTo(7.038f, 19.674f, 6.674f, 20.166f, 6.207f, 20.5f) + horizontalLineTo(7.333f) + curveTo(7.77f, 20.5f, 8.31f, 20.119f, 8.77f, 19.171f) + curveTo(9.212f, 18.257f, 9.5f, 16.96f, 9.5f, 15.5f) + curveTo(9.5f, 14.04f, 9.212f, 12.743f, 8.77f, 11.829f) + curveTo(8.31f, 10.881f, 7.77f, 10.5f, 7.333f, 10.5f) + close() + moveTo(4.302f, 10.584f) + curveTo(3.95f, 10.742f, 3.568f, 11.132f, 3.23f, 11.829f) + curveTo(2.788f, 12.743f, 2.5f, 14.04f, 2.5f, 15.5f) + curveTo(2.5f, 16.96f, 2.788f, 18.257f, 3.23f, 19.171f) + curveTo(3.568f, 19.868f, 3.95f, 20.257f, 4.302f, 20.415f) + curveTo(4.31f, 20.216f, 4.436f, 19.99f, 4.701f, 19.929f) + curveTo(4.8f, 19.906f, 4.902f, 19.863f, 5.008f, 19.798f) + curveTo(4.295f, 18.785f, 3.87f, 17.209f, 3.87f, 15.5f) + curveTo(3.87f, 13.791f, 4.294f, 12.215f, 5.007f, 11.201f) + curveTo(4.901f, 11.137f, 4.799f, 11.094f, 4.701f, 11.071f) + curveTo(4.438f, 11.011f, 4.31f, 10.785f, 4.302f, 10.584f) + close() + } + path(fill = SolidColor(cardColor)) { + moveTo(16.2f, 4.998f) + curveTo(17.88f, 4.998f, 18.721f, 4.997f, 19.362f, 5.324f) + curveTo(19.927f, 5.612f, 20.385f, 6.071f, 20.673f, 6.636f) + curveTo(21f, 7.277f, 21f, 8.118f, 21f, 9.798f) + verticalLineTo(14.197f) + curveTo(21f, 15.877f, 21f, 16.718f, 20.673f, 17.359f) + curveTo(20.385f, 17.924f, 19.927f, 18.383f, 19.362f, 18.671f) + curveTo(18.721f, 18.998f, 17.88f, 18.998f, 16.2f, 18.998f) + horizontalLineTo(10.509f) + curveTo(10.851f, 17.983f, 11.044f, 16.778f, 11.044f, 15.5f) + curveTo(11.044f, 13.809f, 10.707f, 12.245f, 10.132f, 11.081f) + curveTo(9.58f, 9.963f, 8.688f, 9f, 7.488f, 9f) + horizontalLineTo(4.6f) + curveTo(3.984f, 9f, 3.449f, 9.254f, 3f, 9.652f) + curveTo(3f, 8.068f, 3.01f, 7.259f, 3.327f, 6.636f) + curveTo(3.615f, 6.071f, 4.073f, 5.612f, 4.638f, 5.324f) + curveTo(5.279f, 4.997f, 6.12f, 4.998f, 7.8f, 4.998f) + horizontalLineTo(16.2f) + close() + } + group( + clipPathData = PathData { + moveTo(16.2f, 4.998f) + curveTo(17.88f, 4.998f, 18.721f, 4.997f, 19.362f, 5.324f) + curveTo(19.927f, 5.612f, 20.385f, 6.071f, 20.673f, 6.636f) + curveTo(21f, 7.277f, 21f, 8.118f, 21f, 9.798f) + verticalLineTo(14.197f) + curveTo(21f, 15.877f, 21f, 16.718f, 20.673f, 17.359f) + curveTo(20.385f, 17.924f, 19.927f, 18.383f, 19.362f, 18.671f) + curveTo(18.721f, 18.998f, 17.88f, 18.998f, 16.2f, 18.998f) + horizontalLineTo(10.509f) + curveTo(10.851f, 17.983f, 11.044f, 16.778f, 11.044f, 15.5f) + curveTo(11.044f, 13.809f, 10.707f, 12.245f, 10.132f, 11.081f) + curveTo(9.58f, 9.963f, 8.688f, 9f, 7.488f, 9f) + horizontalLineTo(4.6f) + curveTo(3.984f, 9f, 3.449f, 9.254f, 3f, 9.652f) + curveTo(3f, 8.068f, 3.01f, 7.259f, 3.327f, 6.636f) + curveTo(3.615f, 6.071f, 4.073f, 5.612f, 4.638f, 5.324f) + curveTo(5.279f, 4.997f, 6.12f, 4.998f, 7.8f, 4.998f) + horizontalLineTo(16.2f) + close() + }, + ) { + path( + fill = SolidColor(borderColor), + ) { + moveTo(16.2f, 4.998f) + verticalLineTo(3.998f) + verticalLineTo(4.998f) + close() + moveTo(19.362f, 5.324f) + lineTo(19.816f, 4.433f) + lineTo(19.816f, 4.433f) + lineTo(19.362f, 5.324f) + close() + moveTo(20.673f, 6.636f) + lineTo(21.564f, 6.182f) + lineTo(21.564f, 6.182f) + lineTo(20.673f, 6.636f) + close() + moveTo(20.673f, 17.359f) + lineTo(21.564f, 17.813f) + lineTo(21.564f, 17.813f) + lineTo(20.673f, 17.359f) + close() + moveTo(19.362f, 18.671f) + lineTo(19.816f, 19.562f) + lineTo(19.816f, 19.562f) + lineTo(19.362f, 18.671f) + close() + moveTo(16.2f, 18.998f) + verticalLineTo(19.998f) + verticalLineTo(18.998f) + close() + moveTo(10.509f, 18.998f) + lineTo(9.561f, 18.679f) + lineTo(9.116f, 19.998f) + horizontalLineTo(10.509f) + verticalLineTo(18.998f) + close() + moveTo(10.132f, 11.081f) + lineTo(11.028f, 10.638f) + lineTo(11.028f, 10.638f) + lineTo(10.132f, 11.081f) + close() + moveTo(7.488f, 9f) + lineTo(7.488f, 8f) + horizontalLineTo(7.488f) + verticalLineTo(9f) + close() + moveTo(4.6f, 9f) + verticalLineTo(8f) + horizontalLineTo(4.6f) + lineTo(4.6f, 9f) + close() + moveTo(3f, 9.652f) + lineTo(2f, 9.652f) + lineTo(2f, 11.875f) + lineTo(3.663f, 10.401f) + lineTo(3f, 9.652f) + close() + moveTo(3.327f, 6.636f) + lineTo(2.436f, 6.182f) + lineTo(2.436f, 6.182f) + lineTo(3.327f, 6.636f) + close() + moveTo(4.638f, 5.324f) + lineTo(4.184f, 4.433f) + lineTo(4.184f, 4.433f) + lineTo(4.638f, 5.324f) + close() + moveTo(7.8f, 4.998f) + verticalLineTo(3.998f) + verticalLineTo(4.998f) + close() + moveTo(16.2f, 4.998f) + verticalLineTo(5.998f) + curveTo(17.057f, 5.998f, 17.639f, 5.999f, 18.09f, 6.035f) + curveTo(18.528f, 6.071f, 18.752f, 6.136f, 18.908f, 6.215f) + lineTo(19.362f, 5.324f) + lineTo(19.816f, 4.433f) + curveTo(19.331f, 4.186f, 18.814f, 4.087f, 18.252f, 4.042f) + curveTo(17.701f, 3.997f, 17.024f, 3.998f, 16.2f, 3.998f) + verticalLineTo(4.998f) + close() + moveTo(19.362f, 5.324f) + lineTo(18.908f, 6.215f) + curveTo(19.284f, 6.407f, 19.59f, 6.713f, 19.782f, 7.09f) + lineTo(20.673f, 6.636f) + lineTo(21.564f, 6.182f) + curveTo(21.181f, 5.43f, 20.569f, 4.817f, 19.816f, 4.433f) + lineTo(19.362f, 5.324f) + close() + moveTo(20.673f, 6.636f) + lineTo(19.782f, 7.09f) + curveTo(19.861f, 7.246f, 19.927f, 7.47f, 19.962f, 7.909f) + curveTo(19.999f, 8.359f, 20f, 8.941f, 20f, 9.798f) + horizontalLineTo(21f) + horizontalLineTo(22f) + curveTo(22f, 8.974f, 22.001f, 8.296f, 21.956f, 7.746f) + curveTo(21.91f, 7.184f, 21.811f, 6.667f, 21.564f, 6.182f) + lineTo(20.673f, 6.636f) + close() + moveTo(21f, 9.798f) + horizontalLineTo(20f) + verticalLineTo(14.197f) + horizontalLineTo(21f) + horizontalLineTo(22f) + verticalLineTo(9.798f) + horizontalLineTo(21f) + close() + moveTo(21f, 14.197f) + horizontalLineTo(20f) + curveTo(20f, 15.054f, 19.999f, 15.636f, 19.962f, 16.086f) + curveTo(19.927f, 16.525f, 19.861f, 16.749f, 19.782f, 16.905f) + lineTo(20.673f, 17.359f) + lineTo(21.564f, 17.813f) + curveTo(21.811f, 17.328f, 21.91f, 16.811f, 21.956f, 16.249f) + curveTo(22.001f, 15.699f, 22f, 15.021f, 22f, 14.197f) + horizontalLineTo(21f) + close() + moveTo(20.673f, 17.359f) + lineTo(19.782f, 16.905f) + curveTo(19.59f, 17.282f, 19.284f, 17.588f, 18.908f, 17.78f) + lineTo(19.362f, 18.671f) + lineTo(19.816f, 19.562f) + curveTo(20.569f, 19.178f, 21.181f, 18.565f, 21.564f, 17.813f) + lineTo(20.673f, 17.359f) + close() + moveTo(19.362f, 18.671f) + lineTo(18.908f, 17.78f) + curveTo(18.752f, 17.86f, 18.528f, 17.925f, 18.089f, 17.96f) + curveTo(17.639f, 17.997f, 17.057f, 17.998f, 16.2f, 17.998f) + verticalLineTo(18.998f) + verticalLineTo(19.998f) + curveTo(17.024f, 19.998f, 17.702f, 19.999f, 18.252f, 19.954f) + curveTo(18.814f, 19.908f, 19.331f, 19.809f, 19.816f, 19.562f) + lineTo(19.362f, 18.671f) + close() + moveTo(16.2f, 18.998f) + verticalLineTo(17.998f) + horizontalLineTo(10.509f) + verticalLineTo(18.998f) + verticalLineTo(19.998f) + horizontalLineTo(16.2f) + verticalLineTo(18.998f) + close() + moveTo(10.509f, 18.998f) + lineTo(11.456f, 19.317f) + curveTo(11.837f, 18.188f, 12.044f, 16.874f, 12.044f, 15.5f) + horizontalLineTo(11.044f) + horizontalLineTo(10.044f) + curveTo(10.044f, 16.682f, 9.865f, 17.778f, 9.561f, 18.679f) + lineTo(10.509f, 18.998f) + close() + moveTo(11.044f, 15.5f) + horizontalLineTo(12.044f) + curveTo(12.044f, 13.689f, 11.685f, 11.968f, 11.028f, 10.638f) + lineTo(10.132f, 11.081f) + lineTo(9.235f, 11.524f) + curveTo(9.729f, 12.523f, 10.044f, 13.929f, 10.044f, 15.5f) + horizontalLineTo(11.044f) + close() + moveTo(10.132f, 11.081f) + lineTo(11.028f, 10.638f) + curveTo(10.428f, 9.423f, 9.28f, 8f, 7.488f, 8f) + lineTo(7.488f, 9f) + lineTo(7.488f, 10f) + curveTo(8.095f, 10f, 8.731f, 10.503f, 9.235f, 11.524f) + lineTo(10.132f, 11.081f) + close() + moveTo(7.488f, 9f) + verticalLineTo(8f) + horizontalLineTo(4.6f) + verticalLineTo(9f) + verticalLineTo(10f) + horizontalLineTo(7.488f) + verticalLineTo(9f) + close() + moveTo(4.6f, 9f) + lineTo(4.6f, 8f) + curveTo(3.687f, 8f, 2.925f, 8.382f, 2.337f, 8.904f) + lineTo(3f, 9.652f) + lineTo(3.663f, 10.401f) + curveTo(3.973f, 10.126f, 4.281f, 10f, 4.6f, 10f) + lineTo(4.6f, 9f) + close() + moveTo(3f, 9.652f) + lineTo(4f, 9.653f) + curveTo(4f, 8.847f, 4.003f, 8.297f, 4.041f, 7.871f) + curveTo(4.077f, 7.457f, 4.141f, 7.241f, 4.218f, 7.09f) + lineTo(3.327f, 6.636f) + lineTo(2.436f, 6.182f) + curveTo(2.196f, 6.653f, 2.096f, 7.153f, 2.049f, 7.696f) + curveTo(2.002f, 8.227f, 2f, 8.874f, 2f, 9.652f) + lineTo(3f, 9.652f) + close() + moveTo(3.327f, 6.636f) + lineTo(4.218f, 7.09f) + curveTo(4.41f, 6.713f, 4.716f, 6.407f, 5.092f, 6.215f) + lineTo(4.638f, 5.324f) + lineTo(4.184f, 4.433f) + curveTo(3.43f, 4.817f, 2.819f, 5.43f, 2.436f, 6.182f) + lineTo(3.327f, 6.636f) + close() + moveTo(4.638f, 5.324f) + lineTo(5.092f, 6.215f) + curveTo(5.248f, 6.136f, 5.472f, 6.071f, 5.91f, 6.035f) + curveTo(6.361f, 5.999f, 6.943f, 5.998f, 7.8f, 5.998f) + verticalLineTo(4.998f) + verticalLineTo(3.998f) + curveTo(6.977f, 3.998f, 6.299f, 3.997f, 5.748f, 4.042f) + curveTo(5.186f, 4.087f, 4.669f, 4.186f, 4.184f, 4.433f) + lineTo(4.638f, 5.324f) + close() + moveTo(7.8f, 4.998f) + verticalLineTo(5.998f) + horizontalLineTo(16.2f) + verticalLineTo(4.998f) + verticalLineTo(3.998f) + horizontalLineTo(7.8f) + verticalLineTo(4.998f) + close() + } + } + }.build() + + fun buildRingWithCard2( + mainColor: Color, + cardColor: Color, + secondCardColor: Color, + borderColor: Color, + ): ImageVector = ImageVector.Builder( + name = "RingTwoCards", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = SolidColor(mainColor), + pathFillType = PathFillType.EvenOdd, + ) { + moveTo(3.667f, 10f) + curveTo(2.194f, 10f, 1f, 12.462f, 1f, 15.5f) + curveTo(1f, 18.538f, 2.194f, 21f, 3.667f, 21f) + horizontalLineTo(6.333f) + curveTo(7.806f, 21f, 9f, 18.538f, 9f, 15.5f) + curveTo(9f, 12.462f, 7.806f, 10f, 6.333f, 10f) + horizontalLineTo(3.667f) + close() + moveTo(3.815f, 10.55f) + curveTo(7.402f, 10.55f, 7.46f, 20.45f, 3.815f, 20.45f) + curveTo(3.795f, 20.45f, 3.793f, 20.421f, 3.813f, 20.416f) + curveTo(4.099f, 20.351f, 4.371f, 20.19f, 4.62f, 19.952f) + curveTo(4.668f, 19.906f, 4.668f, 19.83f, 4.624f, 19.781f) + curveTo(3.874f, 18.923f, 3.37f, 17.327f, 3.37f, 15.5f) + curveTo(3.37f, 13.672f, 3.875f, 12.076f, 4.624f, 11.219f) + curveTo(4.668f, 11.169f, 4.668f, 11.093f, 4.62f, 11.047f) + curveTo(4.371f, 10.809f, 4.1f, 10.649f, 3.813f, 10.584f) + curveTo(3.793f, 10.579f, 3.795f, 10.55f, 3.815f, 10.55f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(6.333f, 10.5f) + horizontalLineTo(5.192f) + curveTo(5.657f, 10.835f, 6.021f, 11.328f, 6.29f, 11.878f) + curveTo(6.785f, 12.893f, 7.023f, 14.21f, 7.027f, 15.498f) + curveTo(7.031f, 16.786f, 6.801f, 18.106f, 6.307f, 19.122f) + curveTo(6.038f, 19.674f, 5.674f, 20.166f, 5.207f, 20.5f) + horizontalLineTo(6.333f) + curveTo(6.77f, 20.5f, 7.31f, 20.119f, 7.77f, 19.171f) + curveTo(8.212f, 18.257f, 8.5f, 16.96f, 8.5f, 15.5f) + curveTo(8.5f, 14.04f, 8.212f, 12.743f, 7.77f, 11.829f) + curveTo(7.31f, 10.881f, 6.77f, 10.5f, 6.333f, 10.5f) + close() + moveTo(3.302f, 10.584f) + curveTo(2.95f, 10.742f, 2.568f, 11.132f, 2.23f, 11.829f) + curveTo(1.788f, 12.743f, 1.5f, 14.04f, 1.5f, 15.5f) + curveTo(1.5f, 16.96f, 1.788f, 18.257f, 2.23f, 19.171f) + curveTo(2.568f, 19.868f, 2.95f, 20.257f, 3.302f, 20.415f) + curveTo(3.31f, 20.216f, 3.436f, 19.99f, 3.701f, 19.929f) + curveTo(3.8f, 19.906f, 3.902f, 19.863f, 4.008f, 19.798f) + curveTo(3.295f, 18.785f, 2.87f, 17.209f, 2.87f, 15.5f) + curveTo(2.87f, 13.791f, 3.294f, 12.215f, 4.007f, 11.201f) + curveTo(3.901f, 11.137f, 3.799f, 11.094f, 3.701f, 11.071f) + curveTo(3.438f, 11.011f, 3.31f, 10.785f, 3.302f, 10.584f) + close() + } + path(fill = SolidColor(cardColor)) { + moveTo(15.2f, 4.998f) + curveTo(16.88f, 4.998f, 17.721f, 4.997f, 18.362f, 5.324f) + curveTo(18.927f, 5.612f, 19.385f, 6.071f, 19.673f, 6.636f) + curveTo(20f, 7.277f, 20f, 8.118f, 20f, 9.798f) + verticalLineTo(14.197f) + curveTo(20f, 15.877f, 20f, 16.718f, 19.673f, 17.359f) + curveTo(19.385f, 17.924f, 18.927f, 18.383f, 18.362f, 18.671f) + curveTo(17.721f, 18.998f, 16.88f, 18.998f, 15.2f, 18.998f) + horizontalLineTo(9.509f) + curveTo(9.851f, 17.983f, 10.044f, 16.778f, 10.044f, 15.5f) + curveTo(10.044f, 13.809f, 9.707f, 12.245f, 9.132f, 11.081f) + curveTo(8.58f, 9.963f, 7.688f, 9f, 6.488f, 9f) + horizontalLineTo(3.6f) + curveTo(2.984f, 9f, 2.449f, 9.254f, 2f, 9.652f) + curveTo(2f, 8.068f, 2.01f, 7.259f, 2.327f, 6.636f) + curveTo(2.615f, 6.071f, 3.073f, 5.612f, 3.638f, 5.324f) + curveTo(4.279f, 4.997f, 5.12f, 4.998f, 6.8f, 4.998f) + horizontalLineTo(15.2f) + close() + } + group( + clipPathData = PathData { + moveTo(15.2f, 4.998f) + curveTo(16.88f, 4.998f, 17.721f, 4.997f, 18.362f, 5.324f) + curveTo(18.927f, 5.612f, 19.385f, 6.071f, 19.673f, 6.636f) + curveTo(20f, 7.277f, 20f, 8.118f, 20f, 9.798f) + verticalLineTo(14.197f) + curveTo(20f, 15.877f, 20f, 16.718f, 19.673f, 17.359f) + curveTo(19.385f, 17.924f, 18.927f, 18.383f, 18.362f, 18.671f) + curveTo(17.721f, 18.998f, 16.88f, 18.998f, 15.2f, 18.998f) + horizontalLineTo(9.509f) + curveTo(9.851f, 17.983f, 10.044f, 16.778f, 10.044f, 15.5f) + curveTo(10.044f, 13.809f, 9.707f, 12.245f, 9.132f, 11.081f) + curveTo(8.58f, 9.963f, 7.688f, 9f, 6.488f, 9f) + horizontalLineTo(3.6f) + curveTo(2.984f, 9f, 2.449f, 9.254f, 2f, 9.652f) + curveTo(2f, 8.068f, 2.01f, 7.259f, 2.327f, 6.636f) + curveTo(2.615f, 6.071f, 3.073f, 5.612f, 3.638f, 5.324f) + curveTo(4.279f, 4.997f, 5.12f, 4.998f, 6.8f, 4.998f) + horizontalLineTo(15.2f) + close() + }, + ) { + path( + fill = SolidColor(secondCardColor), + fillAlpha = 0.1f, + ) { + moveTo(15.2f, 4.998f) + verticalLineTo(3.998f) + verticalLineTo(4.998f) + close() + moveTo(18.362f, 5.324f) + lineTo(18.816f, 4.433f) + lineTo(18.816f, 4.433f) + lineTo(18.362f, 5.324f) + close() + moveTo(19.673f, 6.636f) + lineTo(20.564f, 6.182f) + lineTo(20.564f, 6.182f) + lineTo(19.673f, 6.636f) + close() + moveTo(19.673f, 17.359f) + lineTo(20.564f, 17.813f) + lineTo(20.564f, 17.813f) + lineTo(19.673f, 17.359f) + close() + moveTo(18.362f, 18.671f) + lineTo(18.816f, 19.562f) + lineTo(18.816f, 19.562f) + lineTo(18.362f, 18.671f) + close() + moveTo(15.2f, 18.998f) + verticalLineTo(19.998f) + verticalLineTo(18.998f) + close() + moveTo(9.509f, 18.998f) + lineTo(8.561f, 18.679f) + lineTo(8.116f, 19.998f) + horizontalLineTo(9.509f) + verticalLineTo(18.998f) + close() + moveTo(9.132f, 11.081f) + lineTo(10.028f, 10.638f) + lineTo(10.028f, 10.638f) + lineTo(9.132f, 11.081f) + close() + moveTo(6.488f, 9f) + lineTo(6.488f, 8f) + horizontalLineTo(6.488f) + verticalLineTo(9f) + close() + moveTo(3.6f, 9f) + verticalLineTo(8f) + horizontalLineTo(3.6f) + lineTo(3.6f, 9f) + close() + moveTo(2f, 9.652f) + lineTo(1f, 9.652f) + lineTo(1f, 11.875f) + lineTo(2.663f, 10.401f) + lineTo(2f, 9.652f) + close() + moveTo(2.327f, 6.636f) + lineTo(1.436f, 6.182f) + lineTo(1.436f, 6.182f) + lineTo(2.327f, 6.636f) + close() + moveTo(3.638f, 5.324f) + lineTo(3.184f, 4.433f) + lineTo(3.184f, 4.433f) + lineTo(3.638f, 5.324f) + close() + moveTo(6.8f, 4.998f) + verticalLineTo(3.998f) + verticalLineTo(4.998f) + close() + moveTo(15.2f, 4.998f) + verticalLineTo(5.998f) + curveTo(16.057f, 5.998f, 16.639f, 5.999f, 17.09f, 6.035f) + curveTo(17.528f, 6.071f, 17.752f, 6.136f, 17.908f, 6.215f) + lineTo(18.362f, 5.324f) + lineTo(18.816f, 4.433f) + curveTo(18.331f, 4.186f, 17.814f, 4.087f, 17.252f, 4.042f) + curveTo(16.701f, 3.997f, 16.024f, 3.998f, 15.2f, 3.998f) + verticalLineTo(4.998f) + close() + moveTo(18.362f, 5.324f) + lineTo(17.908f, 6.215f) + curveTo(18.284f, 6.407f, 18.59f, 6.713f, 18.782f, 7.09f) + lineTo(19.673f, 6.636f) + lineTo(20.564f, 6.182f) + curveTo(20.181f, 5.43f, 19.569f, 4.817f, 18.816f, 4.433f) + lineTo(18.362f, 5.324f) + close() + moveTo(19.673f, 6.636f) + lineTo(18.782f, 7.09f) + curveTo(18.861f, 7.246f, 18.927f, 7.47f, 18.962f, 7.909f) + curveTo(18.999f, 8.359f, 19f, 8.941f, 19f, 9.798f) + horizontalLineTo(20f) + horizontalLineTo(21f) + curveTo(21f, 8.974f, 21.001f, 8.296f, 20.956f, 7.746f) + curveTo(20.91f, 7.184f, 20.811f, 6.667f, 20.564f, 6.182f) + lineTo(19.673f, 6.636f) + close() + moveTo(20f, 9.798f) + horizontalLineTo(19f) + verticalLineTo(14.197f) + horizontalLineTo(20f) + horizontalLineTo(21f) + verticalLineTo(9.798f) + horizontalLineTo(20f) + close() + moveTo(20f, 14.197f) + horizontalLineTo(19f) + curveTo(19f, 15.054f, 18.999f, 15.636f, 18.962f, 16.086f) + curveTo(18.927f, 16.525f, 18.861f, 16.749f, 18.782f, 16.905f) + lineTo(19.673f, 17.359f) + lineTo(20.564f, 17.813f) + curveTo(20.811f, 17.328f, 20.91f, 16.811f, 20.956f, 16.249f) + curveTo(21.001f, 15.699f, 21f, 15.021f, 21f, 14.197f) + horizontalLineTo(20f) + close() + moveTo(19.673f, 17.359f) + lineTo(18.782f, 16.905f) + curveTo(18.59f, 17.282f, 18.284f, 17.588f, 17.908f, 17.78f) + lineTo(18.362f, 18.671f) + lineTo(18.816f, 19.562f) + curveTo(19.569f, 19.178f, 20.181f, 18.565f, 20.564f, 17.813f) + lineTo(19.673f, 17.359f) + close() + moveTo(18.362f, 18.671f) + lineTo(17.908f, 17.78f) + curveTo(17.752f, 17.86f, 17.528f, 17.925f, 17.089f, 17.96f) + curveTo(16.639f, 17.997f, 16.057f, 17.998f, 15.2f, 17.998f) + verticalLineTo(18.998f) + verticalLineTo(19.998f) + curveTo(16.024f, 19.998f, 16.702f, 19.999f, 17.252f, 19.954f) + curveTo(17.814f, 19.908f, 18.331f, 19.809f, 18.816f, 19.562f) + lineTo(18.362f, 18.671f) + close() + moveTo(15.2f, 18.998f) + verticalLineTo(17.998f) + horizontalLineTo(9.509f) + verticalLineTo(18.998f) + verticalLineTo(19.998f) + horizontalLineTo(15.2f) + verticalLineTo(18.998f) + close() + moveTo(9.509f, 18.998f) + lineTo(10.456f, 19.317f) + curveTo(10.837f, 18.188f, 11.044f, 16.874f, 11.044f, 15.5f) + horizontalLineTo(10.044f) + horizontalLineTo(9.044f) + curveTo(9.044f, 16.682f, 8.865f, 17.778f, 8.561f, 18.679f) + lineTo(9.509f, 18.998f) + close() + moveTo(10.044f, 15.5f) + horizontalLineTo(11.044f) + curveTo(11.044f, 13.689f, 10.685f, 11.968f, 10.028f, 10.638f) + lineTo(9.132f, 11.081f) + lineTo(8.235f, 11.524f) + curveTo(8.729f, 12.523f, 9.044f, 13.929f, 9.044f, 15.5f) + horizontalLineTo(10.044f) + close() + moveTo(9.132f, 11.081f) + lineTo(10.028f, 10.638f) + curveTo(9.428f, 9.423f, 8.28f, 8f, 6.488f, 8f) + lineTo(6.488f, 9f) + lineTo(6.488f, 10f) + curveTo(7.095f, 10f, 7.731f, 10.503f, 8.235f, 11.524f) + lineTo(9.132f, 11.081f) + close() + moveTo(6.488f, 9f) + verticalLineTo(8f) + horizontalLineTo(3.6f) + verticalLineTo(9f) + verticalLineTo(10f) + horizontalLineTo(6.488f) + verticalLineTo(9f) + close() + moveTo(3.6f, 9f) + lineTo(3.6f, 8f) + curveTo(2.687f, 8f, 1.925f, 8.382f, 1.337f, 8.904f) + lineTo(2f, 9.652f) + lineTo(2.663f, 10.401f) + curveTo(2.973f, 10.126f, 3.281f, 10f, 3.6f, 10f) + lineTo(3.6f, 9f) + close() + moveTo(2f, 9.652f) + lineTo(3f, 9.653f) + curveTo(3f, 8.847f, 3.003f, 8.297f, 3.041f, 7.871f) + curveTo(3.077f, 7.457f, 3.141f, 7.241f, 3.218f, 7.09f) + lineTo(2.327f, 6.636f) + lineTo(1.436f, 6.182f) + curveTo(1.196f, 6.653f, 1.096f, 7.153f, 1.048f, 7.696f) + curveTo(1.002f, 8.227f, 1f, 8.874f, 1f, 9.652f) + lineTo(2f, 9.652f) + close() + moveTo(2.327f, 6.636f) + lineTo(3.218f, 7.09f) + curveTo(3.41f, 6.713f, 3.716f, 6.407f, 4.092f, 6.215f) + lineTo(3.638f, 5.324f) + lineTo(3.184f, 4.433f) + curveTo(2.43f, 4.817f, 1.819f, 5.43f, 1.436f, 6.182f) + lineTo(2.327f, 6.636f) + close() + moveTo(3.638f, 5.324f) + lineTo(4.092f, 6.215f) + curveTo(4.248f, 6.136f, 4.472f, 6.071f, 4.91f, 6.035f) + curveTo(5.361f, 5.999f, 5.943f, 5.998f, 6.8f, 5.998f) + verticalLineTo(4.998f) + verticalLineTo(3.998f) + curveTo(5.977f, 3.998f, 5.299f, 3.997f, 4.748f, 4.042f) + curveTo(4.186f, 4.087f, 3.669f, 4.186f, 3.184f, 4.433f) + lineTo(3.638f, 5.324f) + close() + moveTo(6.8f, 4.998f) + verticalLineTo(5.998f) + horizontalLineTo(15.2f) + verticalLineTo(4.998f) + verticalLineTo(3.998f) + horizontalLineTo(6.8f) + verticalLineTo(4.998f) + close() + } + } + path(fill = SolidColor(secondCardColor)) { + moveTo(18.035f, 5f) + curveTo(19.772f, 5f, 20.642f, 5f, 21.306f, 5.327f) + curveTo(21.889f, 5.615f, 22.364f, 6.073f, 22.662f, 6.638f) + curveTo(23f, 7.279f, 23f, 8.12f, 23f, 9.8f) + verticalLineTo(14.2f) + curveTo(23f, 15.88f, 23f, 16.721f, 22.662f, 17.362f) + curveTo(22.364f, 17.927f, 21.889f, 18.385f, 21.306f, 18.673f) + curveTo(20.642f, 19f, 19.772f, 19f, 18.035f, 19f) + horizontalLineTo(17f) + curveTo(18.738f, 19f, 19.607f, 19f, 20.271f, 18.673f) + curveTo(20.855f, 18.385f, 21.33f, 17.927f, 21.627f, 17.362f) + curveTo(21.965f, 16.721f, 21.965f, 15.88f, 21.965f, 14.2f) + verticalLineTo(9.8f) + curveTo(21.965f, 8.12f, 21.965f, 7.279f, 21.627f, 6.638f) + curveTo(21.33f, 6.073f, 20.855f, 5.615f, 20.271f, 5.327f) + curveTo(19.607f, 5f, 18.738f, 5f, 17f, 5f) + horizontalLineTo(18.035f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(22.331f, 7.153f) + curveTo(22.39f, 7.342f, 22.434f, 7.57f, 22.459f, 7.871f) + curveTo(22.499f, 8.345f, 22.5f, 8.951f, 22.5f, 9.8f) + verticalLineTo(14.2f) + lineTo(22.495f, 15.305f) + curveTo(22.49f, 15.621f, 22.479f, 15.892f, 22.459f, 16.129f) + curveTo(22.434f, 16.429f, 22.39f, 16.657f, 22.331f, 16.846f) + curveTo(22.373f, 16.648f, 22.402f, 16.438f, 22.421f, 16.213f) + curveTo(22.465f, 15.687f, 22.466f, 15.032f, 22.466f, 14.2f) + verticalLineTo(9.8f) + lineTo(22.46f, 8.679f) + curveTo(22.455f, 8.345f, 22.443f, 8.05f, 22.421f, 7.787f) + curveTo(22.402f, 7.561f, 22.373f, 7.351f, 22.331f, 7.153f) + close() + } + }.build() + + fun buildCard(mainColor: Color, borderColor: Color, tColor: Color?): ImageVector = ImageVector.Builder( + name = "KeyCard", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(mainColor)) { + moveTo(3f, 9.8f) + curveTo(3f, 8.12f, 3f, 7.28f, 3.327f, 6.638f) + curveTo(3.615f, 6.074f, 4.074f, 5.615f, 4.638f, 5.327f) + curveTo(5.28f, 5f, 6.12f, 5f, 7.8f, 5f) + horizontalLineTo(16.2f) + curveTo(17.88f, 5f, 18.72f, 5f, 19.362f, 5.327f) + curveTo(19.927f, 5.615f, 20.385f, 6.074f, 20.673f, 6.638f) + curveTo(21f, 7.28f, 21f, 8.12f, 21f, 9.8f) + verticalLineTo(14.2f) + curveTo(21f, 15.88f, 21f, 16.72f, 20.673f, 17.362f) + curveTo(20.385f, 17.927f, 19.927f, 18.385f, 19.362f, 18.673f) + curveTo(18.72f, 19f, 17.88f, 19f, 16.2f, 19f) + horizontalLineTo(7.8f) + curveTo(6.12f, 19f, 5.28f, 19f, 4.638f, 18.673f) + curveTo(4.074f, 18.385f, 3.615f, 17.927f, 3.327f, 17.362f) + curveTo(3f, 16.72f, 3f, 15.88f, 3f, 14.2f) + verticalLineTo(9.8f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(7.8f, 5.5f) + horizontalLineTo(16.2f) + curveTo(17.048f, 5.5f, 17.655f, 5.5f, 18.13f, 5.539f) + curveTo(18.599f, 5.577f, 18.896f, 5.651f, 19.135f, 5.772f) + curveTo(19.605f, 6.012f, 19.988f, 6.395f, 20.228f, 6.865f) + curveTo(20.349f, 7.104f, 20.423f, 7.401f, 20.461f, 7.87f) + curveTo(20.5f, 8.345f, 20.5f, 8.952f, 20.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(20.5f, 15.048f, 20.5f, 15.655f, 20.461f, 16.13f) + curveTo(20.423f, 16.599f, 20.349f, 16.896f, 20.228f, 17.135f) + curveTo(19.988f, 17.605f, 19.605f, 17.988f, 19.135f, 18.228f) + curveTo(18.896f, 18.349f, 18.599f, 18.423f, 18.13f, 18.461f) + curveTo(17.655f, 18.5f, 17.048f, 18.5f, 16.2f, 18.5f) + horizontalLineTo(7.8f) + curveTo(6.952f, 18.5f, 6.345f, 18.5f, 5.87f, 18.461f) + curveTo(5.401f, 18.423f, 5.104f, 18.349f, 4.865f, 18.228f) + curveTo(4.395f, 17.988f, 4.012f, 17.605f, 3.772f, 17.135f) + curveTo(3.651f, 16.896f, 3.577f, 16.599f, 3.539f, 16.13f) + curveTo(3.5f, 15.655f, 3.5f, 15.048f, 3.5f, 14.2f) + verticalLineTo(9.8f) + curveTo(3.5f, 8.952f, 3.5f, 8.345f, 3.539f, 7.87f) + curveTo(3.577f, 7.401f, 3.651f, 7.104f, 3.772f, 6.865f) + curveTo(4.012f, 6.395f, 4.395f, 6.012f, 4.865f, 5.772f) + curveTo(5.104f, 5.651f, 5.401f, 5.577f, 5.87f, 5.539f) + curveTo(6.345f, 5.5f, 6.952f, 5.5f, 7.8f, 5.5f) + close() + } + if (tColor != null) { + path(fill = SolidColor(tColor)) { + moveTo(11.082f, 16f) + verticalLineTo(9.635f) + horizontalLineTo(9f) + verticalLineTo(8f) + horizontalLineTo(15f) + verticalLineTo(9.635f) + horizontalLineTo(12.913f) + verticalLineTo(16f) + horizontalLineTo(11.082f) + close() + } + } + }.build() + + fun buildCard2(mainColor: Color, secondColor: Color, borderColor: Color, tColor: Color?): ImageVector = + ImageVector.Builder( + name = "KeyCard2", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(mainColor)) { + moveTo(2f, 9.8f) + curveTo(2f, 8.12f, 2f, 7.28f, 2.327f, 6.638f) + curveTo(2.615f, 6.074f, 3.074f, 5.615f, 3.638f, 5.327f) + curveTo(4.28f, 5f, 5.12f, 5f, 6.8f, 5f) + horizontalLineTo(15.2f) + curveTo(16.88f, 5f, 17.72f, 5f, 18.362f, 5.327f) + curveTo(18.927f, 5.615f, 19.385f, 6.074f, 19.673f, 6.638f) + curveTo(20f, 7.28f, 20f, 8.12f, 20f, 9.8f) + verticalLineTo(14.2f) + curveTo(20f, 15.88f, 20f, 16.72f, 19.673f, 17.362f) + curveTo(19.385f, 17.927f, 18.927f, 18.385f, 18.362f, 18.673f) + curveTo(17.72f, 19f, 16.88f, 19f, 15.2f, 19f) + horizontalLineTo(6.8f) + curveTo(5.12f, 19f, 4.28f, 19f, 3.638f, 18.673f) + curveTo(3.074f, 18.385f, 2.615f, 17.927f, 2.327f, 17.362f) + curveTo(2f, 16.72f, 2f, 15.88f, 2f, 14.2f) + verticalLineTo(9.8f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(6.8f, 5.5f) + horizontalLineTo(15.2f) + curveTo(16.048f, 5.5f, 16.655f, 5.5f, 17.13f, 5.539f) + curveTo(17.599f, 5.577f, 17.896f, 5.651f, 18.135f, 5.772f) + curveTo(18.605f, 6.012f, 18.988f, 6.395f, 19.228f, 6.865f) + curveTo(19.349f, 7.104f, 19.423f, 7.401f, 19.461f, 7.87f) + curveTo(19.5f, 8.345f, 19.5f, 8.952f, 19.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(19.5f, 15.048f, 19.5f, 15.655f, 19.461f, 16.13f) + curveTo(19.423f, 16.599f, 19.349f, 16.896f, 19.228f, 17.135f) + curveTo(18.988f, 17.605f, 18.605f, 17.988f, 18.135f, 18.228f) + curveTo(17.896f, 18.349f, 17.599f, 18.423f, 17.13f, 18.461f) + curveTo(16.655f, 18.5f, 16.048f, 18.5f, 15.2f, 18.5f) + horizontalLineTo(6.8f) + curveTo(5.952f, 18.5f, 5.345f, 18.5f, 4.87f, 18.461f) + curveTo(4.401f, 18.423f, 4.104f, 18.349f, 3.865f, 18.228f) + curveTo(3.395f, 17.988f, 3.012f, 17.605f, 2.772f, 17.135f) + curveTo(2.651f, 16.896f, 2.577f, 16.599f, 2.539f, 16.13f) + curveTo(2.5f, 15.655f, 2.5f, 15.048f, 2.5f, 14.2f) + verticalLineTo(9.8f) + curveTo(2.5f, 8.952f, 2.5f, 8.345f, 2.539f, 7.87f) + curveTo(2.577f, 7.401f, 2.651f, 7.104f, 2.772f, 6.865f) + curveTo(3.012f, 6.395f, 3.395f, 6.012f, 3.865f, 5.772f) + curveTo(4.104f, 5.651f, 4.401f, 5.577f, 4.87f, 5.539f) + curveTo(5.345f, 5.5f, 5.952f, 5.5f, 6.8f, 5.5f) + close() + } + if (tColor != null) { + path(fill = SolidColor(tColor)) { + moveTo(10.082f, 16f) + verticalLineTo(9.635f) + horizontalLineTo(8f) + verticalLineTo(8f) + horizontalLineTo(14f) + verticalLineTo(9.635f) + horizontalLineTo(11.913f) + verticalLineTo(16f) + horizontalLineTo(10.082f) + close() + } + } + path(fill = SolidColor(secondColor)) { + moveTo(17.535f, 5f) + curveTo(19.272f, 5f, 20.142f, 5f, 20.806f, 5.327f) + curveTo(21.389f, 5.615f, 21.864f, 6.073f, 22.162f, 6.638f) + curveTo(22.5f, 7.279f, 22.5f, 8.12f, 22.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(22.5f, 15.88f, 22.5f, 16.721f, 22.162f, 17.362f) + curveTo(21.864f, 17.927f, 21.389f, 18.385f, 20.806f, 18.673f) + curveTo(20.142f, 19f, 19.272f, 19f, 17.535f, 19f) + horizontalLineTo(16.5f) + curveTo(18.238f, 19f, 19.107f, 19f, 19.771f, 18.673f) + curveTo(20.355f, 18.385f, 20.83f, 17.927f, 21.127f, 17.362f) + curveTo(21.465f, 16.721f, 21.465f, 15.88f, 21.465f, 14.2f) + verticalLineTo(9.8f) + curveTo(21.465f, 8.12f, 21.465f, 7.279f, 21.127f, 6.638f) + curveTo(20.83f, 6.073f, 20.355f, 5.615f, 19.771f, 5.327f) + curveTo(19.107f, 5f, 18.238f, 5f, 16.5f, 5f) + horizontalLineTo(17.535f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(21.831f, 7.153f) + curveTo(21.89f, 7.342f, 21.934f, 7.57f, 21.959f, 7.871f) + curveTo(21.999f, 8.345f, 22f, 8.951f, 22f, 9.8f) + verticalLineTo(14.2f) + lineTo(21.995f, 15.305f) + curveTo(21.99f, 15.621f, 21.979f, 15.892f, 21.959f, 16.129f) + curveTo(21.934f, 16.429f, 21.89f, 16.657f, 21.831f, 16.846f) + curveTo(21.873f, 16.648f, 21.902f, 16.438f, 21.921f, 16.213f) + curveTo(21.965f, 15.687f, 21.966f, 15.032f, 21.966f, 14.2f) + verticalLineTo(9.8f) + lineTo(21.96f, 8.679f) + curveTo(21.955f, 8.345f, 21.943f, 8.05f, 21.921f, 7.787f) + curveTo(21.902f, 7.561f, 21.873f, 7.351f, 21.831f, 7.153f) + close() + } + }.build() + + fun buildCard3( + mainColor: Color, + secondColor: Color, + thirdColor: Color, + borderColor: Color, + tColor: Color?, + ): ImageVector = ImageVector.Builder( + name = "KeyCard3", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path(fill = SolidColor(mainColor)) { + moveTo(0f, 9.8f) + curveTo(0f, 8.12f, 0f, 7.28f, 0.327f, 6.638f) + curveTo(0.615f, 6.074f, 1.074f, 5.615f, 1.638f, 5.327f) + curveTo(2.28f, 5f, 3.12f, 5f, 4.8f, 5f) + horizontalLineTo(13.2f) + curveTo(14.88f, 5f, 15.72f, 5f, 16.362f, 5.327f) + curveTo(16.927f, 5.615f, 17.385f, 6.074f, 17.673f, 6.638f) + curveTo(18f, 7.28f, 18f, 8.12f, 18f, 9.8f) + verticalLineTo(14.2f) + curveTo(18f, 15.88f, 18f, 16.72f, 17.673f, 17.362f) + curveTo(17.385f, 17.927f, 16.927f, 18.385f, 16.362f, 18.673f) + curveTo(15.72f, 19f, 14.88f, 19f, 13.2f, 19f) + horizontalLineTo(4.8f) + curveTo(3.12f, 19f, 2.28f, 19f, 1.638f, 18.673f) + curveTo(1.074f, 18.385f, 0.615f, 17.927f, 0.327f, 17.362f) + curveTo(0f, 16.72f, 0f, 15.88f, 0f, 14.2f) + verticalLineTo(9.8f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(4.8f, 5.5f) + horizontalLineTo(13.2f) + curveTo(14.048f, 5.5f, 14.655f, 5.5f, 15.13f, 5.539f) + curveTo(15.599f, 5.577f, 15.896f, 5.651f, 16.135f, 5.772f) + curveTo(16.605f, 6.012f, 16.988f, 6.395f, 17.228f, 6.865f) + curveTo(17.349f, 7.104f, 17.423f, 7.401f, 17.461f, 7.87f) + curveTo(17.5f, 8.345f, 17.5f, 8.952f, 17.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(17.5f, 15.048f, 17.5f, 15.655f, 17.461f, 16.13f) + curveTo(17.423f, 16.599f, 17.349f, 16.896f, 17.228f, 17.135f) + curveTo(16.988f, 17.605f, 16.605f, 17.988f, 16.135f, 18.228f) + curveTo(15.896f, 18.349f, 15.599f, 18.423f, 15.13f, 18.461f) + curveTo(14.655f, 18.5f, 14.048f, 18.5f, 13.2f, 18.5f) + horizontalLineTo(4.8f) + curveTo(3.952f, 18.5f, 3.345f, 18.5f, 2.87f, 18.461f) + curveTo(2.401f, 18.423f, 2.104f, 18.349f, 1.865f, 18.228f) + curveTo(1.395f, 17.988f, 1.012f, 17.605f, 0.772f, 17.135f) + curveTo(0.651f, 16.896f, 0.577f, 16.599f, 0.539f, 16.13f) + curveTo(0.5f, 15.655f, 0.5f, 15.048f, 0.5f, 14.2f) + verticalLineTo(9.8f) + curveTo(0.5f, 8.952f, 0.5f, 8.345f, 0.539f, 7.87f) + curveTo(0.577f, 7.401f, 0.651f, 7.104f, 0.772f, 6.865f) + curveTo(1.012f, 6.395f, 1.395f, 6.012f, 1.865f, 5.772f) + curveTo(2.104f, 5.651f, 2.401f, 5.577f, 2.87f, 5.539f) + curveTo(3.345f, 5.5f, 3.952f, 5.5f, 4.8f, 5.5f) + close() + } + if (tColor != null) { + path(fill = SolidColor(tColor)) { + moveTo(8.082f, 16f) + verticalLineTo(9.635f) + horizontalLineTo(6f) + verticalLineTo(8f) + horizontalLineTo(12f) + verticalLineTo(9.635f) + horizontalLineTo(9.913f) + verticalLineTo(16f) + horizontalLineTo(8.082f) + close() + } + } + path(fill = SolidColor(secondColor)) { + moveTo(16.035f, 5f) + curveTo(17.772f, 5f, 18.642f, 5f, 19.306f, 5.327f) + curveTo(19.889f, 5.615f, 20.364f, 6.073f, 20.662f, 6.638f) + curveTo(21f, 7.279f, 21f, 8.12f, 21f, 9.8f) + verticalLineTo(14.2f) + curveTo(21f, 15.88f, 21f, 16.721f, 20.662f, 17.362f) + curveTo(20.364f, 17.927f, 19.889f, 18.385f, 19.306f, 18.673f) + curveTo(18.642f, 19f, 17.772f, 19f, 16.035f, 19f) + horizontalLineTo(15f) + curveTo(16.738f, 19f, 17.607f, 19f, 18.271f, 18.673f) + curveTo(18.855f, 18.385f, 19.33f, 17.927f, 19.627f, 17.362f) + curveTo(19.965f, 16.721f, 19.965f, 15.88f, 19.965f, 14.2f) + verticalLineTo(9.8f) + curveTo(19.965f, 8.12f, 19.965f, 7.279f, 19.627f, 6.638f) + curveTo(19.33f, 6.073f, 18.855f, 5.615f, 18.271f, 5.327f) + curveTo(17.607f, 5f, 16.738f, 5f, 15f, 5f) + horizontalLineTo(16.035f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(20.331f, 7.153f) + curveTo(20.39f, 7.342f, 20.434f, 7.57f, 20.459f, 7.871f) + curveTo(20.499f, 8.345f, 20.5f, 8.951f, 20.5f, 9.8f) + verticalLineTo(14.2f) + lineTo(20.495f, 15.305f) + curveTo(20.49f, 15.621f, 20.479f, 15.892f, 20.459f, 16.129f) + curveTo(20.434f, 16.429f, 20.39f, 16.657f, 20.331f, 16.846f) + curveTo(20.373f, 16.648f, 20.402f, 16.438f, 20.421f, 16.213f) + curveTo(20.465f, 15.687f, 20.466f, 15.032f, 20.466f, 14.2f) + verticalLineTo(9.8f) + lineTo(20.46f, 8.679f) + curveTo(20.455f, 8.345f, 20.443f, 8.05f, 20.421f, 7.787f) + curveTo(20.402f, 7.561f, 20.373f, 7.351f, 20.331f, 7.153f) + close() + } + path(fill = SolidColor(thirdColor)) { + moveTo(19.035f, 5f) + curveTo(20.772f, 5f, 21.642f, 5f, 22.306f, 5.327f) + curveTo(22.889f, 5.615f, 23.364f, 6.073f, 23.662f, 6.638f) + curveTo(24f, 7.279f, 24f, 8.12f, 24f, 9.8f) + verticalLineTo(14.2f) + curveTo(24f, 15.88f, 24f, 16.721f, 23.662f, 17.362f) + curveTo(23.364f, 17.927f, 22.889f, 18.385f, 22.306f, 18.673f) + curveTo(21.642f, 19f, 20.772f, 19f, 19.035f, 19f) + horizontalLineTo(18f) + curveTo(19.738f, 19f, 20.607f, 19f, 21.271f, 18.673f) + curveTo(21.855f, 18.385f, 22.33f, 17.927f, 22.627f, 17.362f) + curveTo(22.965f, 16.721f, 22.965f, 15.88f, 22.965f, 14.2f) + verticalLineTo(9.8f) + curveTo(22.965f, 8.12f, 22.965f, 7.279f, 22.627f, 6.638f) + curveTo(22.33f, 6.073f, 21.855f, 5.615f, 21.271f, 5.327f) + curveTo(20.607f, 5f, 19.738f, 5f, 18f, 5f) + horizontalLineTo(19.035f) + close() + } + path( + stroke = SolidColor(borderColor), + strokeLineWidth = 1f, + ) { + moveTo(23.331f, 7.153f) + curveTo(23.39f, 7.342f, 23.434f, 7.57f, 23.459f, 7.871f) + curveTo(23.499f, 8.345f, 23.5f, 8.951f, 23.5f, 9.8f) + verticalLineTo(14.2f) + curveTo(23.5f, 15.049f, 23.499f, 15.655f, 23.459f, 16.129f) + curveTo(23.434f, 16.429f, 23.39f, 16.657f, 23.331f, 16.846f) + curveTo(23.373f, 16.648f, 23.402f, 16.438f, 23.421f, 16.213f) + curveTo(23.465f, 15.687f, 23.466f, 15.032f, 23.466f, 14.2f) + verticalLineTo(9.8f) + curveTo(23.466f, 8.968f, 23.465f, 8.313f, 23.421f, 7.787f) + curveTo(23.402f, 7.561f, 23.373f, 7.351f, 23.331f, 7.153f) + close() + } + }.build() +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt index b9b0c54c51..1f0df03ae4 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt @@ -326,7 +326,10 @@ class TangemColors2 internal constructor( class Border internal constructor( val neutral: Neutral, val status: Status, + walletIcon: Color, ) { + var walletIcon by mutableStateOf(walletIcon) + private set @Stable class Neutral internal constructor( @@ -367,6 +370,7 @@ class TangemColors2 internal constructor( fun update(other: Border) { neutral.update(other.neutral) status.update(other.status) + walletIcon = other.walletIcon } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index 93a3647d65..ef46023d32 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -85,6 +85,7 @@ private fun lightThemeColors2(): TangemColors2 { warning = TangemColorPalette.Amaranth, attention = TangemColorPalette.Tangerine, ), + walletIcon = TangemColorPalette.Dark_10, ) val overlay = TangemColors2.Overlay( overlayPrimary = TangemColorPalette.Overlay1, @@ -247,6 +248,7 @@ private fun darkThemeColors2(): TangemColors2 { warning = TangemColorPalette.Flamingo, attention = TangemColorPalette.Mustard, ), + walletIcon = TangemColorPalette.Light_10, ) val overlay = TangemColors2.Overlay( overlayPrimary = TangemColorPalette.Overlay1, diff --git a/core/ui/src/main/res/drawable/ic_shield_24.xml b/core/ui/src/main/res/drawable/ic_shield_24.xml new file mode 100644 index 0000000000..129f023c2e --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_shield_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWalletIcon.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWalletIcon.kt new file mode 100644 index 0000000000..dc5417d014 --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/wallet/UserWalletIcon.kt @@ -0,0 +1,21 @@ +package com.tangem.domain.models.wallet + +/** + * Represents the icon of a user wallet, which can be of different types such as hot, stub, default, or colored. + */ +sealed class UserWalletIcon { + data object Hot : UserWalletIcon() + data class Stub(val cardsCount: Int) : UserWalletIcon() + + data class Default( + val isRing: Boolean, + val cardsCount: Int, + ) : UserWalletIcon() + + data class Colored( + val isRing: Boolean, + val mainColor: String, + val secondColor: String? = null, + val thirdColor: String? = null, + ) : UserWalletIcon() +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletIconUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletIconUseCase.kt new file mode 100644 index 0000000000..c370657bd3 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletIconUseCase.kt @@ -0,0 +1,242 @@ +package com.tangem.domain.wallets.usecase + +import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.card.common.util.getCardsCount +import com.tangem.domain.demo.models.DemoConfig +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.models.wallet.UserWalletIcon +import com.tangem.domain.models.wallet.isHotWallet +import com.tangem.domain.models.wallet.requireColdWallet +import com.tangem.domain.wallets.repository.WalletsRepository +import kotlinx.coroutines.runBlocking + +class GetWalletIconUseCase( + private val walletsRepository: WalletsRepository, +) { + + @Suppress("CyclomaticComplexMethod", "UnsafeCallOnNullableType") + operator fun invoke(userWallet: UserWallet): UserWalletIcon { + if (userWallet.isHotWallet) { + return UserWalletIcon.Hot + } + + userWallet.requireColdWallet() + + val cardTypesResolver = userWallet.scanResponse.cardTypesResolver + val cardsCount = userWallet.getCardsCount() ?: 1 + + val cobrandColor by lazy { + colorByBatchId(userWallet.scanResponse.card.batchId) + } + + return when { + cardTypesResolver.isDevKit() -> otherColor(OtherCardType.Devkit) + userWallet.isRing() -> UserWalletIcon.Default(isRing = true, cardsCount = cardsCount) + cobrandColor != null -> cobrandColor!!.withCount(cardsCount) + cardTypesResolver.isWallet2() -> UserWalletIcon.Default(isRing = false, cardsCount = cardsCount) + cardTypesResolver.isShibaWallet() -> otherColor(OtherCardType.Shiba, cardsCount) + cardTypesResolver.isTangemWallet() -> otherColor(OtherCardType.Wallet1, cardsCount) + cardTypesResolver.isWhiteWallet() -> otherColor(OtherCardType.WhiteWallet, cardsCount) + cardTypesResolver.isTangemTwins() -> otherColor(OtherCardType.Twins, cardsCount) + cardTypesResolver.isStart2Coin() -> otherColor(OtherCardType.Starts2com, cardsCount) + cardTypesResolver.isTangemNote() -> + resolveNoteColor(userWallet) ?: UserWalletIcon.Stub(cardsCount = cardsCount) + DemoConfig.isDemoCardId(cardId = userWallet.cardId) -> + UserWalletIcon.Default(isRing = false, cardsCount = cardsCount) + else -> UserWalletIcon.Stub(cardsCount = cardsCount) + } + } + + private fun resolveNoteColor(userWallet: UserWallet.Cold): UserWalletIcon? { + val noteBlockchain = userWallet.scanResponse.cardTypesResolver.getBlockchain() + + val otherCardType = when (noteBlockchain) { + Blockchain.Bitcoin -> OtherCardType.NoteBitcoin + Blockchain.Ethereum -> OtherCardType.NoteEthereum + Blockchain.XRP -> OtherCardType.NoteXRP + Blockchain.Binance -> OtherCardType.NoteBinance + Blockchain.Cardano -> OtherCardType.NoteCardano + Blockchain.Dogecoin -> OtherCardType.NoteDoge + else -> return null + } + + return otherColor(otherCardType) + } + + private fun otherColor(otherCardType: OtherCardType, cardsCount: Int = 1): UserWalletIcon.Colored { + return UserWalletIcon.Colored( + isRing = false, + mainColor = otherCardType.mainColor, + secondColor = if (cardsCount > 1) otherCardType.mainColor else null, + thirdColor = if (cardsCount > 2) otherCardType.mainColor else null, + ) + } + + private fun UserWalletIcon.Colored.withCount(count: Int): UserWalletIcon.Colored { + return this.copy( + mainColor = mainColor, + secondColor = if (count > 1) secondColor else null, + thirdColor = if (count > 2) thirdColor else null, + ) + } + + private fun UserWallet.Cold.isRing(): Boolean { + return scanResponse.cardTypesResolver.isRing() || + runBlocking { walletsRepository.isWalletWithRing(userWalletId = this@isRing.walletId) } + } + + @Suppress("CyclomaticComplexMethod") + private fun colorByBatchId(batchId: String): UserWalletIcon.Colored? { + fun color(main: String, second: String = main, third: String = main) = + UserWalletIcon.Colored(isRing = false, mainColor = main, secondColor = second, thirdColor = third) + + val cobrandType = CobrandType.entries.firstOrNull { it.batchIds.contains(batchId) } + + return when (cobrandType) { + CobrandType.Avrora -> color("#1E1E1C") + CobrandType.BabyDoge -> color("#E7D34C") + CobrandType.Bad -> color("#395467") + CobrandType.BitcoinGold -> color("#F08A1F") + CobrandType.BitcoinPizzaDay -> color("#AF4D37") + CobrandType.BitcoinPizza2 -> color("#F3C63A") + CobrandType.BTC365 -> color("#191E3E") + CobrandType.CashClubGold -> color("#D1BF78") + CobrandType.Changenow -> color("#191B2C") + CobrandType.Chilliz -> color("#49324A") + CobrandType.CoinMetrica -> color("#A140C7") + CobrandType.COQ -> color("#ED1F3A") + CobrandType.CryptoCasey -> color("#301E45") + CobrandType.CryptoOrg -> color("#27B39A") + CobrandType.CryptoSeth -> color("#414954") + CobrandType.GetsMine -> color("#AFC3CE") + CobrandType.Grim -> color("#131313") + CobrandType.Hodl -> color("#111111") + CobrandType.Jr -> color("#1C1C1C") + CobrandType.Kaspa -> color("#545C5C") + CobrandType.Kaspa2 -> color("#353535") + CobrandType.KaspaReseller -> color("#3B3D3A") + CobrandType.Kaspa3 -> color("#85CBC1") + CobrandType.Kasper -> color("#34302E") + CobrandType.Kaspy -> color("#DEC764") + CobrandType.Keiro -> color("#4D645C") + CobrandType.KishuInu -> color("#2DA4CE") + CobrandType.Kango -> color("#5BA495") + CobrandType.Konan -> color("#64C9C9") + CobrandType.Kroak -> color("#8EB8AF") + CobrandType.Neiro -> color("#E7A524") + CobrandType.NewWorldElite -> color("#292722") + CobrandType.PassimPay -> color("#6A4361") + CobrandType.Pastel -> color("#FFC7A4", "#84A479", "#6FB5BB") + CobrandType.Pepecoin -> color("#303439") + CobrandType.RamenCat -> color("#DEBE88") + CobrandType.RedPanda -> color("#C5C5C5") + CobrandType.Rizo -> color("#1765A7") + CobrandType.Sakura -> color("#F0E9C4") + CobrandType.SatoshiFriends -> color("#242424") + CobrandType.SinCity -> color("#D3C487") + CobrandType.SpringBloom -> color("#FAC73A") + CobrandType.StealthCard -> color("#555557") + CobrandType.SunDrop -> color("#FEC035") + CobrandType.Trillant -> color("#955091") + CobrandType.Tron -> color("#D4221D") + CobrandType.Upbit -> color("#2B2B2B") + CobrandType.USA -> color("#0D185F") + CobrandType.VeChain -> color("#5186A2") + CobrandType.Vivid -> color("#D3CF09", "#F76952", "#2BCAD1") + CobrandType.Vnish -> color("#292522") + CobrandType.VoltInu -> color("#34312B") + CobrandType.WhiteTangem -> color("#D3D3D3") + CobrandType.WildGoat -> color("#1B1B1B") + CobrandType.Winter -> color("#7FB9C8", "#80BDE8", "#B2C6E4") + CobrandType.WinterSakura -> color("#88B9E9") + CobrandType.LockedMoney -> color("#272625") + CobrandType.Ghoad -> color("#6EC5C5") + CobrandType.BlushSky -> color("#C1E9E8", "#FACAD3", "#DCCEE0") + CobrandType.ElectraSea -> color("#0D5A67", "#29939E", "#30C6B1") + CobrandType.HyperBlue -> color("#0F397C", "#1474D3", "#0BC9EC") + CobrandType.Lunar -> color("#B0313A") + null -> null + } + } +} + +private enum class CobrandType(val batchIds: List) { + Avrora(listOf("AF18")), + BabyDoge(listOf("AF51")), + Bad(listOf("AF09")), + BitcoinGold(listOf("AF71", "AF990016", "AF990009")), + BitcoinPizzaDay(listOf("AF33")), + BitcoinPizza2(listOf("AF990019")), + BTC365(listOf("AF97")), + CashClubGold(listOf("BB000004")), + Changenow(listOf("BB000013")), + Chilliz(listOf("BB000016")), + CoinMetrica(listOf("AF27")), + COQ(listOf("AF28")), + CryptoCasey(listOf("AF21", "AF22", "AF23")), + CryptoOrg(listOf("AF57")), + CryptoSeth(listOf("AF32")), + GetsMine(listOf("BB000008")), + Grim(listOf("AF13")), + Hodl(listOf("BB000009")), + Jr(listOf("AF14")), + Kaspa(listOf("AF08")), + Kaspa2(listOf("AF25", "AF61", "AF72")), + KaspaReseller(listOf("AF31")), + Kaspa3(listOf("AF73")), + Kasper(listOf("AF96")), + Kaspy(listOf("AF95")), + Keiro(listOf("BB000017")), + KishuInu(listOf("AF52")), + Kango(listOf("BB000006")), + Konan(listOf("AF93")), + Kroak(listOf("BB000011")), + Neiro(listOf("AF98")), + NewWorldElite(listOf("AF26")), + PassimPay(listOf("BB000007")), + Pastel(listOf("AF43", "AF44", "AF45", "AF78", "AF79", "AF80")), + Pepecoin(listOf("BB000015")), + RamenCat(listOf("AF990006", "AF990007", "AF990008")), + RedPanda(listOf("AF34")), + Rizo(listOf("BB000012")), + Sakura(listOf("AF990029", "AF990030", "AF990031", "AF990071", "AF990072", "AF990073")), + SatoshiFriends(listOf("AF19")), + SinCity(listOf("BB000010")), + SpringBloom(listOf("AF990001", "AF990002", "AF990004")), + StealthCard(listOf("AF60", "AF74", "AF88")), + SunDrop(listOf("AF990005", "AF990003")), + Trillant(listOf("AF16")), + Tron(listOf("AF07")), + Upbit(listOf("BB000019")), + USA(listOf("AF91", "AF990017", "AF990056")), + VeChain(listOf("AF29")), + Vivid(listOf("AF40", "AF41", "AF42", "AF75", "AF76", "AF77")), + Vnish(listOf("BB000005")), + VoltInu(listOf("AF35")), + WhiteTangem(listOf("AF15")), + WildGoat(listOf("BB000001")), + Winter(listOf("AF85", "AF86", "AF87", "AF990013", "AF990012", "AF990011")), + WinterSakura(listOf("AF990053", "AF990054", "AF990055")), + LockedMoney(listOf("AF63")), + Ghoad(listOf("AF89")), + BlushSky(listOf("AF990020", "AF990021", "AF990022")), + ElectraSea(listOf("AF990023", "AF990024", "AF990025")), + HyperBlue(listOf("AF990026", "AF990027", "AF990028", "AF990050", "AF990051", "AF990052")), + Lunar(listOf("AF990057", "AF990058", "AF990059")), +} + +private enum class OtherCardType(val mainColor: String) { + NoteXRP("#726799"), + NoteDoge("#BFB565"), + NoteEthereum("#989C9F"), + NoteBinance("#C9B87C"), + NoteCardano("#5979AD"), + NoteBitcoin("#EABE8B"), + Starts2com("#356F99"), + Wallet1("#2E3944"), + Twins("#B8B7B6"), + Devkit("#938C92"), + WhiteWallet("#DDDDDD"), + Shiba("#D5963A"), +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 9adf3437d5..46a932bf55 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 @@ -27,6 +27,7 @@ import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.usecase.* +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.router.InnerWalletRouter @@ -100,6 +101,7 @@ internal class WalletModel @Inject constructor( private val bindRefcodeWithWalletUseCase: BindRefcodeWithWalletUseCase, private val appsFlyerStore: AppsFlyerStore, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + private val getWalletIconUseCase: GetWalletIconUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -520,6 +522,7 @@ internal class WalletModel @Inject constructor( wallets = action.wallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) @@ -565,6 +568,7 @@ internal class WalletModel @Inject constructor( newUserWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) } @@ -585,6 +589,7 @@ internal class WalletModel @Inject constructor( userWallet = userWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) } @@ -598,6 +603,7 @@ internal class WalletModel @Inject constructor( userWallet = action.selectedWallet, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) @@ -658,6 +664,7 @@ internal class WalletModel @Inject constructor( unlockedWallets = action.unlockedWallets, clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt index f71de661de..dc1d34c847 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/preview/WalletBalancePreview.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.preview import androidx.compose.ui.text.SpanStyle +import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.styledStringReference @@ -31,6 +32,7 @@ internal object WalletBalancePreview { ), stringReference(" $"), ), + deviceIcon = DeviceIconUM.Stub(cardsCount = 3), isBalanceFlickering = false, isZeroBalance = false, ) @@ -38,10 +40,12 @@ internal object WalletBalancePreview { val loading: WalletBalanceUM.Loading = WalletBalanceUM.Loading( id = UserWalletId("1"), name = "My Wallet", + deviceIcon = DeviceIconUM.Mobile, ) val error: WalletBalanceUM.Error = WalletBalanceUM.Error( id = UserWalletId("2"), name = "My Wallet", + deviceIcon = DeviceIconUM.Stub(cardsCount = 3), ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt index eb3e310843..1d84273c15 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletImageResolver.kt @@ -17,6 +17,7 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ +@Deprecated("Will be removed in favor of getWalletIconUseCase") internal class WalletImageResolver @Inject constructor( private val walletsRepository: WalletsRepository, ) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt index a081ffe922..b3a4710b3a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletBalanceUM.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable +import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.wallet.UserWalletId @@ -24,6 +25,9 @@ internal sealed interface WalletBalanceUM { /** Wallet Name */ val name: String + /** Wallet Icon */ + val deviceIcon: DeviceIconUM + /** * Wallet card content state * @@ -34,6 +38,7 @@ internal sealed interface WalletBalanceUM { data class Content( override val id: UserWalletId, override val name: String, + override val deviceIcon: DeviceIconUM, val balance: TextReference, val balanceInAppBar: TextReference, val isBalanceFlickering: Boolean, @@ -49,6 +54,7 @@ internal sealed interface WalletBalanceUM { data class Error( override val id: UserWalletId, override val name: String, + override val deviceIcon: DeviceIconUM, ) : WalletBalanceUM /** @@ -60,6 +66,7 @@ internal sealed interface WalletBalanceUM { data class Loading( override val id: UserWalletId, override val name: String, + override val deviceIcon: DeviceIconUM, ) : WalletBalanceUM fun copySealed(name: String): WalletBalanceUM { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index 69574485b2..ec3d6943be 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState @@ -11,12 +12,14 @@ internal class AddWalletTransformer( private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index 8fe34bed8e..eb010c9a39 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -1,9 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers +import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver @@ -22,12 +24,14 @@ internal class InitializeWalletsTransformer( private val wallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } @@ -111,6 +115,8 @@ internal class InitializeWalletsTransformer( walletsBalanceUM = WalletBalanceUM.Loading( id = walletId, name = name, + deviceIcon = getWalletIconUseCase.invoke(userWallet = this) + .let { WalletIconUMConverter().convert(it) }, ), buttons = createWalletActions(userWallet = this), type = when (this) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt index 59a5358db8..0c1b198d0c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState @@ -22,12 +23,14 @@ internal class ReinitializeNewWalletTransformer( private val newUserWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index fc2ade623b..6dab085776 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState @@ -18,12 +19,14 @@ internal class ReinitializeWalletTransformer( private val userWallet: UserWallet, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletStateTransformer(userWalletId = userWallet.walletId) { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index aacaf7a5d2..0d7dc02991 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -91,6 +91,7 @@ internal class SetTokenListErrorTransformer( return WalletBalanceUM.Content( id = id, name = name, + deviceIcon = deviceIcon, balanceInAppBar = BigDecimal.ZERO.formatStyled { fiat( fiatCurrencyCode = appCurrency.code, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 126d447ac9..fcef9841df 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -2,6 +2,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState @@ -16,12 +17,14 @@ internal class UnlockWalletTransformer( private val unlockedWallets: List, private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { WalletLoadingStateFactory( clickIntents = clickIntents, walletImageResolver = walletImageResolver, + getWalletIconUseCase = getWalletIconUseCase, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt index 8369a81071..d3a37dbb31 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletBalanceUMTransformer.kt @@ -28,6 +28,7 @@ internal class MultiWalletBalanceUMTransformer( return WalletBalanceUM.Loading( id = id, name = name, + deviceIcon = deviceIcon, ) } @@ -35,6 +36,7 @@ internal class MultiWalletBalanceUMTransformer( return WalletBalanceUM.Error( id = id, name = name, + deviceIcon = deviceIcon, ) } @@ -42,6 +44,7 @@ internal class MultiWalletBalanceUMTransformer( return WalletBalanceUM.Content( id = id, name = name, + deviceIcon = deviceIcon, balanceInAppBar = fiatBalance.amount.formatStyled { fiat( fiatCurrencyCode = appCurrency.code, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 673b914f18..06d9f69510 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -1,5 +1,6 @@ package com.tangem.feature.wallet.presentation.wallet.state.utils +import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter import com.tangem.core.analytics.models.event.MainScreenAnalyticsEvent.Companion.WALLET_TYPE import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState @@ -10,6 +11,7 @@ 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.wallets.usecase.GetWalletIconUseCase import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents import com.tangem.feature.wallet.impl.R import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory @@ -30,6 +32,7 @@ import kotlinx.coroutines.flow.MutableStateFlow internal class WalletLoadingStateFactory( private val clickIntents: WalletClickIntents, private val walletImageResolver: WalletImageResolver, + private val getWalletIconUseCase: GetWalletIconUseCase, ) { fun create(userWallet: UserWallet): WalletState { @@ -52,6 +55,8 @@ internal class WalletLoadingStateFactory( walletsBalanceUM = WalletBalanceUM.Loading( id = userWallet.walletId, name = userWallet.name, + deviceIcon = getWalletIconUseCase.invoke(userWallet = userWallet) + .let { WalletIconUMConverter().convert(it) }, ), buttons = createWalletActions(userWallet), notifications = persistentListOf(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt index d5e4a5649a..0d4a5903ce 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletBalance.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.TextAutoSize -import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.key @@ -16,21 +15,19 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.scale -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.vectorResource 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.core.ui.R import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.ds.button.SecondaryTangemButton import com.tangem.core.ui.ds.button.TangemButtonShape import com.tangem.core.ui.ds.button.TangemButtonUM +import com.tangem.core.ui.ds.image.TangemDeviceIcon import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior import com.tangem.core.ui.ds.topbar.collapsing.snapToExitUntilCollapsed @@ -87,19 +84,14 @@ internal fun WalletBalance( SpacerH(TangemTheme.dimens2.x3) Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), ) { Text( text = walletBalanceUM.name, style = TangemTheme.typography2.bodyRegular14, color = TangemTheme.colors2.text.neutral.tertiary, ) - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_tangem_24), - contentDescription = null, - tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, - modifier = Modifier.size(TangemTheme.dimens2.x6), - ) + TangemDeviceIcon(state = walletBalanceUM.deviceIcon) } } SpacerH(TangemTheme.dimens2.x2) From d06051845b317ff250a1054fab020b89ad365ba4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Mar 2026 16:52:14 +0400 Subject: [PATCH 96/97] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 43 +-- .../tangem/data/tokens/di/TokensDataModule.kt | 3 - .../repository/DefaultCurrenciesRepository.kt | 227 +----------- ...AllWalletsCryptoCurrencyStatusesUseCase.kt | 56 --- .../tokens/GetCryptoCurrenciesUseCase.kt | 26 -- .../domain/tokens/GetTokenListUseCase.kt | 62 ---- .../tokens/GetWalletTotalBalanceUseCase.kt | 119 ------ .../error/mapper/TokenListErrorMappers.kt | 9 - .../BaseCurrencyStatusOperations.kt | 18 +- .../CachedCurrenciesStatusesOperations.kt | 340 ------------------ .../tokens/operations/TokenListOperations.kt | 66 ---- .../tokens/repository/CurrenciesRepository.kt | 44 --- features/walletconnect/impl/build.gradle.kts | 1 + .../connections/model/WcSelectWalletModel.kt | 6 +- .../connections/utils/WcUserWalletsFetcher.kt | 19 +- 15 files changed, 37 insertions(+), 1002 deletions(-) delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt delete mode 100644 domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index 56bf1c96ba..8e58e90e09 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -20,7 +20,6 @@ import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.* import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.CachedCurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.CurrencyChecksRepository import com.tangem.domain.tokens.repository.TokenReceiveWarningsViewedRepository @@ -54,18 +53,6 @@ internal object TokensDomainModule { return DefaultTokensFeatureToggles(featureTogglesManager = featureTogglesManager) } - @Provides - @Singleton - fun provideGetTokenListUseCase( - currenciesRepository: CurrenciesRepository, - currenciesStatusesOperations: BaseCurrencyStatusOperations, - ): GetTokenListUseCase { - return GetTokenListUseCase( - currenciesRepository = currenciesRepository, - currenciesStatusesOperations = currenciesStatusesOperations, - ) - } - @Provides @Singleton fun provideGetCurrencyUseCase( @@ -78,20 +65,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetAllWalletsCryptoCurrencyStatusesUseCase( - currenciesRepository: CurrenciesRepository, - currencyStatusOperations: BaseCurrencyStatusOperations, - dispatchers: CoroutineDispatcherProvider, - ): GetAllWalletsCryptoCurrencyStatusesUseCase { - return GetAllWalletsCryptoCurrencyStatusesUseCase( - currenciesRepository = currenciesRepository, - currencyStatusOperations = currencyStatusOperations, - dispatchers = dispatchers, - ) - } - @Provides @Singleton fun provideGetCurrencyWarningsUseCase( @@ -245,14 +218,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetWalletTotalBalanceUseCase( - currenciesStatusesOperations: BaseCurrencyStatusOperations, - ): GetWalletTotalBalanceUseCase { - return GetWalletTotalBalanceUseCase(currenciesStatusesOperations) - } - @Provides @Singleton fun provideRefreshMultiCurrencyWalletQuotesUseCase( @@ -287,7 +252,7 @@ internal object TokensDomainModule { multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, stakingIdFactory: StakingIdFactory, ): BaseCurrencyStatusOperations { - return CachedCurrenciesStatusesOperations( + return BaseCurrencyStatusOperations( currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, singleNetworkStatusSupplier = singleNetworkStatusSupplier, @@ -300,12 +265,6 @@ internal object TokensDomainModule { ) } - @Provides - @Singleton - fun provideGetCryptoCurrenciesUseCase(currenciesRepository: CurrenciesRepository): GetCryptoCurrenciesUseCase { - return GetCryptoCurrenciesUseCase(currenciesRepository) - } - @Provides @Singleton fun provideWalletBalanceFetcher( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt index 26626eb998..2f577c7b2b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/di/TokensDataModule.kt @@ -4,7 +4,6 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.common.currency.CardCryptoCurrencyFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.data.common.currency.UserTokensSaver import com.tangem.data.tokens.repository.DefaultCurrenciesRepository import com.tangem.data.tokens.repository.DefaultCurrencyChecksRepository import com.tangem.data.tokens.repository.DefaultTokenReceiveWarningsViewedRepository @@ -44,7 +43,6 @@ internal object TokensDataModule { expressServiceFetcher: ExpressServiceFetcher, excludedBlockchains: ExcludedBlockchains, cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - tokensSaver: UserTokensSaver, responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, ): CurrenciesRepository { @@ -58,7 +56,6 @@ internal object TokensDataModule { dispatchers = dispatchers, excludedBlockchains = excludedBlockchains, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, - userTokensSaver = tokensSaver, responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, ) diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index 16b2415e7e..952b4aa680 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -2,15 +2,14 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.TransactionStatus -import com.tangem.blockchainsdk.compatibility.getL2CompatibilityTokenComparison import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.common.currency.* -import com.tangem.data.tokens.utils.CustomTokensMerger -import com.tangem.datasource.api.common.response.ApiResponseError +import com.tangem.data.common.currency.CardCryptoCurrencyFactory +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.currency.getTokenId import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.UserTokensResponse @@ -19,24 +18,28 @@ import com.tangem.domain.card.CardTypesResolver import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncStrict -import com.tangem.domain.common.wallets.loadAndGet import com.tangem.domain.core.error.DataError -import com.tangem.domain.demo.models.DemoConfig import com.tangem.domain.express.ExpressServiceFetcher import com.tangem.domain.express.models.ExpressAsset import com.tangem.domain.models.account.DerivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.* +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 com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.tokens.model.FeePaidCurrency import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.* +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import kotlinx.coroutines.plus +import kotlinx.coroutines.withContext import timber.log.Timber import com.tangem.blockchain.common.FeePaidCurrency as FeePaidSdkCurrency @@ -49,21 +52,13 @@ internal class DefaultCurrenciesRepository( private val expressServiceFetcher: ExpressServiceFetcher, private val dispatchers: CoroutineDispatcherProvider, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, - private val userTokensSaver: UserTokensSaver, private val userTokensResponseStore: UserTokensResponseStore, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { - private val demoConfig = DemoConfig private val cryptoCurrencyFactory = CryptoCurrencyFactory(excludedBlockchains) - private val userTokensResponseFactory = UserTokensResponseFactory() - private val customTokensMerger = CustomTokensMerger( - tangemTechApi = tangemTechApi, - dispatchers = dispatchers, - userTokensSaver = userTokensSaver, - ) override fun getWalletCurrenciesUpdates(userWalletId: UserWalletId): Flow> { return channelFlow { @@ -159,29 +154,6 @@ internal class DefaultCurrenciesRepository( } } - override suspend fun getMultiCurrencyWalletCurrenciesSync( - userWalletId: UserWalletId, - refresh: Boolean, - ): List = withContext(dispatchers.io) { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - ensureIsCorrectUserWallet(userWallet, isMultiCurrencyWalletExpected = true) - - fetchTokensIfCacheExpired(userWallet, refresh) - - val storedTokens = requireNotNull( - value = getSavedUserTokensResponseSync(key = userWallet.walletId), - lazyMessage = { - "Unable to find tokens response for user wallet with provided ID: $userWalletId" - }, - ) - - responseCryptoCurrenciesFactory.createCurrencies( - response = storedTokens, - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - } - override suspend fun getNetworkCoin( userWalletId: UserWalletId, networkId: Network.ID, @@ -199,38 +171,6 @@ internal class DefaultCurrenciesRepository( ?: error("Unable to find coin for network ID: $networkId") } - override fun isTokensGrouped(userWalletId: UserWalletId): Flow { - return channelFlow { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - - if (userWallet.isMultiCurrency) { - getSavedUserTokensResponse(userWalletId) - .map { response -> response.group == UserTokensResponse.GroupType.NETWORK } - .distinctUntilChanged() - .onEach { isGrouped -> send(isGrouped) } - .launchIn(scope = this + dispatchers.io) - } else { - send(element = false) - } - } - } - - override fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow { - return channelFlow { - val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - - if (userWallet.isMultiCurrency) { - getSavedUserTokensResponse(userWalletId) - .map { response -> response.sort == UserTokensResponse.SortType.BALANCE } - .distinctUntilChanged() - .onEach { isSorted -> send(isSorted) } - .launchIn(scope = this + dispatchers.io) - } else { - send(element = false) - } - } - } - override suspend fun isSendBlockedByPendingTransactions( userWalletId: UserWalletId, cryptoCurrencyStatus: CryptoCurrencyStatus, @@ -332,67 +272,6 @@ internal class DefaultCurrenciesRepository( ) ?: error("Unable to create token") } - @OptIn(ExperimentalCoroutinesApi::class) - override fun getAllWalletsCryptoCurrencies( - currencyRawId: CryptoCurrency.RawID, - ): Flow>> { - return userWalletsListRepository.loadAndGet().flatMapLatest { userWallets -> - - userWallets.filter { it.isMultiCurrency } - .forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) } - - val userWalletsWithCurrencies = userWallets - .filterNot(UserWallet::isLocked) - .map { userWallet -> - getCurrenciesForWallet(userWallet, currencyRawId).map { userWallet to it } - } - - combine(userWalletsWithCurrencies) { it.toMap() } - .onEmpty { emit(value = emptyMap()) } - } - } - - @Suppress("SuspendFunWithFlowReturnType") - private suspend fun getCurrenciesForWallet( - userWallet: UserWallet, - currencyRawId: CryptoCurrency.RawID, - ): Flow> { - return when { - userWallet.isMultiCurrency -> { - getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> - val filterResponse = storedTokens.tokens.filter { - getL2CompatibilityTokenComparison(it, currencyRawId.value) - } - - responseCryptoCurrenciesFactory.createCurrencies( - response = storedTokens.copy(tokens = filterResponse), - userWallet = userWallet, - accountIndex = DerivationIndex.Main, - ) - } - } - - else -> { - val currencies = - if (userWallet.requireColdWallet().scanResponse.cardTypesResolver.isSingleWalletWithToken()) { - getSingleCurrencyWalletWithCardCurrencies(userWallet.walletId) - } else { - val currency = - getSingleCurrencyWalletPrimaryCurrency(userWalletId = userWallet.walletId) - - if (currency.id.rawCurrencyId == currencyRawId) { - listOf(currency) - } else { - emptyList() - } - } - flow { - emit(currencies) - } - } - } - } - override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean { val blockchain = Blockchain.fromNetworkId(network.backendId) return blockchain?.isNetworkFeeZero() == true @@ -412,53 +291,6 @@ internal class DefaultCurrenciesRepository( } } - private suspend fun fetchTokensIfCacheExpired(userWallet: UserWallet, refresh: Boolean) { - cacheRegistry.invokeOnExpire( - key = getTokensCacheKey(userWallet.walletId), - skipCache = refresh, - block = { fetchTokens(userWallet) }, - ) - } - - private suspend fun fetchTokens(userWallet: UserWallet) { - val userWalletId = userWallet.walletId - - val response = if (userWallet is UserWallet.Cold && checkIsEmptyDemoWallet(userWallet)) { - createDefaultUserTokensResponse(userWallet) - } else { - safeApiCall({ tangemTechApi.getUserTokens(userWalletId.stringValue).bind() }) { - handleFetchTokensError(userWallet, it) - } - } - - val compatibleUserTokensResponse = response - .let { it.copy(tokens = it.tokens.distinct()) } - .let { customTokensMerger.mergeIfPresented(userWalletId, it) } - - userTokensSaver.store(userWalletId, compatibleUserTokensResponse) - - fetchExpressAssetsByNetworkIds(userWallet, compatibleUserTokensResponse) - } - - private suspend fun checkIsEmptyDemoWallet(userWallet: UserWallet.Cold): Boolean { - val response = getSavedUserTokensResponseSync(key = userWallet.walletId) - - return demoConfig.isDemoCardId(userWallet.cardId) && response == null - } - - private suspend fun fetchExpressAssetsByNetworkIds(userWallet: UserWallet, userTokens: UserTokensResponse) { - val tokens = userTokens.tokens.mapTo(hashSetOf()) { token -> - ExpressAsset.ID( - networkId = token.networkId, - contractAddress = token.contractAddress, - ) - } - - coroutineScope { - launch { expressServiceFetcher.fetch(userWallet, tokens) } - } - } - private suspend fun fetchExpressAssetsByNetworkIds( userWallet: UserWallet, cryptoCurrencies: List, @@ -484,35 +316,6 @@ internal class DefaultCurrenciesRepository( private fun getAssetsCacheKey(userWalletId: UserWalletId): String = "assets_cache_key_${userWalletId.stringValue}" - private suspend fun handleFetchTokensError(userWallet: UserWallet, e: ApiResponseError): UserTokensResponse { - val userWalletId = userWallet.walletId - val response = userTokensResponseStore.getSyncOrNull(userWalletId = userWalletId) - ?: createDefaultUserTokensResponse(userWallet = userWallet) - - if (e is ApiResponseError.HttpException && e.code == ApiResponseError.HttpException.Code.NOT_FOUND) { - Timber.w( - e, - "Requested currencies could not be found in the remote store for: $userWalletId", - ) - - userTokensSaver.push(userWalletId, response) - } else { - cacheRegistry.invalidate(getTokensCacheKey(userWalletId)) - } - - return response - } - - private fun createDefaultUserTokensResponse(userWallet: UserWallet) = - userTokensResponseFactory.createUserTokensResponse( - currencies = cardCryptoCurrencyFactory.createDefaultCoinsForMultiCurrencyWallet( - userWallet = userWallet, - ), - isGroupedByNetwork = false, - isSortedByBalance = false, - accountId = null, - ) - private fun ensureIsCorrectUserWallet(userWallet: UserWallet, isMultiCurrencyWalletExpected: Boolean) { val userWalletId = userWallet.walletId @@ -536,13 +339,7 @@ internal class DefaultCurrenciesRepository( } } - private fun getTokensCacheKey(userWalletId: UserWalletId): String = "tokens_cache_key_${userWalletId.stringValue}" - private fun getSavedUserTokensResponse(key: UserWalletId): Flow { return userTokensResponseStore.get(userWalletId = key).filterNotNull() } - - private suspend fun getSavedUserTokensResponseSync(key: UserWalletId): UserTokensResponse? { - return userTokensResponseStore.getSyncOrNull(userWalletId = key) - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt deleted file mode 100644 index 9bba447d40..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -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.error.CurrencyStatusError -import com.tangem.domain.tokens.error.mapper.mapToCurrencyError -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* - -/** - * Get crypto currency statuses by raw ID for all wallets - * - * @property currenciesRepository currencies repository - * @property dispatchers dispatchers - * -[REDACTED_AUTHOR] - */ -class GetAllWalletsCryptoCurrencyStatusesUseCase( - private val currenciesRepository: CurrenciesRepository, - private val dispatchers: CoroutineDispatcherProvider, - private val currencyStatusOperations: BaseCurrencyStatusOperations, -) { - - /** - * Get crypto currency statuses by [currencyRawId] for all wallets - * - * @param currencyRawId currency raw ID - */ - @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke( - currencyRawId: CryptoCurrency.RawID, - ): Flow>>> { - return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId) - .flatMapLatest { userWalletsWithCurrencies: Map> -> - val walletStatusFlows = userWalletsWithCurrencies.map { (userWallet, cryptoCurrencies) -> - val currencyStatusFlows = cryptoCurrencies.map { cryptoCurrency -> - currencyStatusOperations.getCurrencyStatusFlow(userWallet.walletId, cryptoCurrency) - .map { it.mapLeft(CurrenciesStatusesOperations.Error::mapToCurrencyError) } - } - - combine(currencyStatusFlows) { statuses -> userWallet to statuses.toList() } - .onEmpty { emit(userWallet to emptyList()) } - } - - combine(walletStatusFlows) { it.toMap() } - .onEmpty { emit(emptyMap()) } - } - .flowOn(dispatchers.io) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt deleted file mode 100644 index 839334727f..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrenciesUseCase.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.core.Either -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.CurrencyStatusError -import com.tangem.domain.tokens.repository.CurrenciesRepository - -@Deprecated("Use MultiWalletCryptoCurrenciesSupplier") -class GetCryptoCurrenciesUseCase( - private val currenciesRepository: CurrenciesRepository, -) { - - /** - * Retrieves the list of cryptocurrencies within a multi-currency wallet. - * - * @param userWalletId The unique identifier of the user wallet. - * - * @return An [Either] representing success (Right) or an error (Left) in fetching the status. - */ - suspend operator fun invoke(userWalletId: UserWalletId): Either> { - return Either.catch { - currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId) - }.mapLeft(CurrencyStatusError::DataError) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt deleted file mode 100644 index 042f4d257f..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetTokenListUseCase.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.tangem.domain.tokens - -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.utils.lceError -import com.tangem.domain.core.utils.lceLoading -import com.tangem.domain.core.utils.toLce -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.error.mapper.mapToTokenListError -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.TokenListOperations -import com.tangem.domain.tokens.repository.CurrenciesRepository -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.emitAll -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.transformLatest - -class GetTokenListUseCase( - private val currenciesRepository: CurrenciesRepository, - private val currenciesStatusesOperations: BaseCurrencyStatusOperations, -) { - - @OptIn(ExperimentalCoroutinesApi::class) - fun launch(userWalletId: UserWalletId): LceFlow { - return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId) - .transformLatest { maybeCurrencies -> - maybeCurrencies.fold( - ifLoading = { maybeContent -> - if (maybeContent != null) { - emitAll(createTokenListLce(userWalletId, maybeContent, isCurrenciesLoading = true)) - } else { - emit(lceLoading()) - } - }, - ifContent = { content -> - emitAll(createTokenListLce(userWalletId, content, isCurrenciesLoading = false)) - }, - ifError = { error -> emit(error.lceError()) }, - ) - } - } - - private fun createTokenListLce( - userWalletId: UserWalletId, - currencies: List, - isCurrenciesLoading: Boolean, - ): LceFlow { - val operations = TokenListOperations( - userWalletId = userWalletId, - tokens = currencies, - currenciesRepository = currenciesRepository, - ) - - return operations.getTokenListFlow().map { maybeTokenList -> - maybeTokenList - .mapLeft(TokenListOperations.Error::mapToTokenListError) - .toLce(isCurrenciesLoading) - } - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt deleted file mode 100644 index 11760b5e2f..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetWalletTotalBalanceUseCase.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.tangem.domain.tokens - -import arrow.atomic.update -import arrow.core.raise.ensureNotNull -import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lce -import com.tangem.domain.core.utils.lceContent -import com.tangem.domain.core.utils.lceLoading -import com.tangem.domain.models.TotalFiatBalance -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.operations.BaseCurrencyStatusOperations -import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.* -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap - -class GetWalletTotalBalanceUseCase( - private val currenciesStatusesOperations: BaseCurrencyStatusOperations, -) { - - private val walletBalanceCache = ConcurrentHashMap() - - operator fun invoke( - userWalletsIds: Collection, - ): LceFlow> { - val flows = userWalletsIds.distinct() - .map { userWalletId -> - invoke(userWalletId).map { maybeBalance -> - userWalletId to maybeBalance - } - } - - return combine(flows) { balances -> - lce { - balances.fold(mutableMapOf()) { acc, (userWalletId, maybeBalance) -> - val balance = maybeBalance.fold( - ifLoading = { TotalFiatBalance.Loading }, - ifContent = { it }, - ifError = { - Timber.e("failed to load balances with error: $it") - TotalFiatBalance.Failed - }, - ) - - isLoading.update { it || balance is TotalFiatBalance.Loading } - - acc[userWalletId] = balance - acc - } - } - } - } - - @OptIn(ExperimentalCoroutinesApi::class) - operator fun invoke(userWalletId: UserWalletId): LceFlow { - return currenciesStatusesOperations.getCurrenciesStatuses(userWalletId).map(::createBalance) - .distinctUntilChanged() - .onStart { - val cachedBalance = walletBalanceCache[userWalletId] - - if (cachedBalance != null) { - emit(cachedBalance.lceContent()) - } - } - .mapLatest { lceBalance -> - val cachedBalance = walletBalanceCache[userWalletId] - - if (cachedBalance == null) { - lceBalance.onContent { content -> - if (content is TotalFiatBalance.Loaded) { - walletBalanceCache.put(userWalletId, content) - } - } - - lceBalance - } else { - val content = lceBalance.getOrNull(isPartialContentAccepted = false) - - if (content is TotalFiatBalance.Loaded && content != cachedBalance) { - walletBalanceCache.put(userWalletId, content) - - lceBalance - } else { - cachedBalance.lceContent() - } - } - } - .distinctUntilChanged() - } - - private fun createBalance( - maybeStatuses: Lce>, - ): Lce = lce { - val statuses = when (maybeStatuses) { - is Lce.Content -> maybeStatuses.content - is Lce.Error -> raise(maybeStatuses) - is Lce.Loading -> { - val content = maybeStatuses.partialContent - - if (content == null) { - isLoading.set(true) - - raise(lceLoading()) - } else { - content - } - } - } - - TotalFiatBalanceCalculator.calculate( - statuses = ensureNotNull(statuses.toNonEmptyListOrNull()) { lceLoading() }, - ) - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt index 081d02f18f..51e08ac450 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/mapper/TokenListErrorMappers.kt @@ -2,7 +2,6 @@ package com.tangem.domain.tokens.error.mapper import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations -import com.tangem.domain.tokens.operations.TokenListOperations internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenListError { return when (this) { @@ -15,12 +14,4 @@ internal fun CurrenciesStatusesOperations.Error.mapToTokenListError(): TokenList is CurrenciesStatusesOperations.Error.EmptyStakingBalances, -> TokenListError.EmptyTokens } -} - -internal fun TokenListOperations.Error.mapToTokenListError(): TokenListError { - return when (this) { - is TokenListOperations.Error.DataError -> TokenListError.DataError(this.cause) - is TokenListOperations.Error.UnableToSortTokenList -> - TokenListError.UnableToSortTokenList(this.unsortedTokenList) - } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt index 10043d51ca..117b907f78 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/BaseCurrencyStatusOperations.kt @@ -3,7 +3,6 @@ package com.tangem.domain.tokens.operations import arrow.core.* import arrow.core.raise.* import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.domain.core.lce.LceFlow import com.tangem.domain.core.utils.EitherFlow import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -28,7 +27,6 @@ import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.domain.staking.single.SingleStakingBalanceSupplier import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.utils.CurrencyStatusProxyCreator @@ -42,7 +40,7 @@ import kotlinx.coroutines.flow.* [REDACTED_AUTHOR] */ @Suppress("LargeClass", "LongParameterList") -abstract class BaseCurrencyStatusOperations( +class BaseCurrencyStatusOperations( private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val multiNetworkStatusSupplier: MultiNetworkStatusSupplier, @@ -56,10 +54,6 @@ abstract class BaseCurrencyStatusOperations( private val currencyStatusProxyCreator = CurrencyStatusProxyCreator() - abstract fun getCurrenciesStatuses(userWalletId: UserWalletId): LceFlow> - - protected abstract fun getQuotes(id: CryptoCurrency.RawID): Flow>> - suspend fun getCurrencyStatusFlow( userWalletId: UserWalletId, currencyId: CryptoCurrency.ID, @@ -389,6 +383,14 @@ abstract class BaseCurrencyStatusOperations( .bind() } + private fun getQuotes(id: CryptoCurrency.RawID): Flow>> { + return singleQuoteStatusSupplier( + params = SingleQuoteStatusProducer.Params(rawCurrencyId = id), + ) + .map>> { setOf(it).right() } + .distinctUntilChanged() + } + private suspend fun getStakingBalancesSync( userWalletId: UserWalletId, cryptoCurrencies: List, @@ -439,7 +441,7 @@ abstract class BaseCurrencyStatusOperations( ) } - protected fun getIds(currencies: List): Pair, NonEmptySet> { + private fun getIds(currencies: List): Pair, NonEmptySet> { val currencyIdToNetworkId = currencies.associate { currency -> currency.id to currency.network } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt deleted file mode 100644 index a1561d6d2e..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CachedCurrenciesStatusesOperations.kt +++ /dev/null @@ -1,340 +0,0 @@ -package com.tangem.domain.tokens.operations - -import arrow.core.* -import arrow.core.raise.recover -import com.tangem.domain.core.lce.Lce -import com.tangem.domain.core.lce.LceFlow -import com.tangem.domain.core.lce.lce -import com.tangem.domain.core.lce.lceFlow -import com.tangem.domain.core.utils.EitherFlow -import com.tangem.domain.core.utils.lceContent -import com.tangem.domain.core.utils.lceError -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.network.NetworkStatus -import com.tangem.domain.models.network.getAddress -import com.tangem.domain.models.quote.QuoteStatus -import com.tangem.domain.models.staking.StakingID -import com.tangem.domain.models.staking.StakingBalance -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.networks.multi.MultiNetworkStatusSupplier -import com.tangem.domain.networks.single.SingleNetworkStatusProducer -import com.tangem.domain.networks.single.SingleNetworkStatusSupplier -import com.tangem.domain.quotes.QuotesRepository -import com.tangem.domain.quotes.single.SingleQuoteStatusProducer -import com.tangem.domain.quotes.single.SingleQuoteStatusSupplier -import com.tangem.domain.staking.StakingIdFactory -import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier -import com.tangem.domain.staking.single.SingleStakingBalanceProducer -import com.tangem.domain.staking.single.SingleStakingBalanceSupplier -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier -import com.tangem.domain.tokens.error.TokenListError -import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations.Error -import com.tangem.domain.tokens.repository.CurrenciesRepository -import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch - -@Suppress("LongParameterList", "LargeClass") -class CachedCurrenciesStatusesOperations( - private val currenciesRepository: CurrenciesRepository, - quotesRepository: QuotesRepository, - private val singleNetworkStatusSupplier: SingleNetworkStatusSupplier, - multiNetworkStatusSupplier: MultiNetworkStatusSupplier, - private val singleQuoteStatusSupplier: SingleQuoteStatusSupplier, - private val singleStakingBalanceSupplier: SingleStakingBalanceSupplier, - multiStakingBalanceSupplier: MultiStakingBalanceSupplier, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, - private val stakingIdFactory: StakingIdFactory, -) : BaseCurrencyStatusOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - multiNetworkStatusSupplier = multiNetworkStatusSupplier, - singleNetworkStatusSupplier = singleNetworkStatusSupplier, - singleQuoteStatusSupplier = singleQuoteStatusSupplier, - singleStakingBalanceSupplier = singleStakingBalanceSupplier, - multiStakingBalanceSupplier = multiStakingBalanceSupplier, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, - stakingIdFactory = stakingIdFactory, -) { - - override fun getCurrenciesStatuses( - userWalletId: UserWalletId, - ): LceFlow> { - return transformToCurrenciesStatuses( - userWalletId = userWalletId, - currenciesFlow = getCurrencies(userWalletId), - ) - } - - @Suppress("LongMethod") - @OptIn(ExperimentalCoroutinesApi::class) - private fun transformToCurrenciesStatuses( - userWalletId: UserWalletId, - currenciesFlow: EitherFlow>, - ): LceFlow> = lceFlow { - val prevStatuses = MutableStateFlow(value = emptyList()) - - currenciesFlow.flatMapLatest { maybeCurrencies -> - val currencies = maybeCurrencies - .getOrElse { return@flatMapLatest flowOf(it.lceError()) } - .toNonEmptyListOrNull() - - if (currencies.isNullOrEmpty()) { - prevStatuses.value = emptyList() - return@flatMapLatest flowOf(TokenListError.EmptyTokens.lceError()) - } - - // This is only 'true' when the flow here is empty, such as during initial loading - if (isLoading.get()) { - val loadingCurrencies = createCurrenciesStatuses( - currencies = currencies, - maybeNetworkStatuses = null, - maybeQuotes = null, - maybeStakingBalances = null, - isUpdating = true, - ) - - loadingCurrencies.getOrNull()?.let { prevStatuses.value = it } - - send(loadingCurrencies) - } - - val (networks, currenciesIds) = getIds(currencies) - - fun createCurrenciesStatuses( - maybeQuotes: Either>, - maybeNetworkStatuses: Either>, - maybeStakingBalances: Either>, - isUpdating: Boolean, - ) = createCurrenciesStatuses( - currencies = currencies, - maybeQuotes = maybeQuotes, - maybeNetworkStatuses = maybeNetworkStatuses, - maybeStakingBalances = maybeStakingBalances, - isUpdating = isUpdating, - ) - - // removing token - val prevStatusesValue = prevStatuses.value - if (prevStatusesValue.size - currencies.size == 1) { - val removed = prevStatusesValue.map { it.currency } - currencies - - return@flatMapLatest flowOf( - prevStatusesValue.filter { it.currency !in removed }.lceContent(), - ) - } - - val networksStatusesUpdates = getNetworkStatusesUpdates(userWalletId, networks) - - combine( - flow = getQuotes(currenciesIds), - flow2 = networksStatusesUpdates, - flow3 = networksStatusesUpdates.flatMapLatest { maybeNetworksStatuses -> - val networksStatuses = maybeNetworksStatuses.getOrNull() - - val currenciesAddresses = if (networksStatuses == null) { - emptyMap() - } else { - currencies.associate { currency -> - val networkStatus = networksStatuses.firstOrNull { it.network == currency.network } - - currency.id to networkStatus.getAddress() - } - } - - getYieldsBalancesUpdates(userWalletId, currenciesAddresses) - }, - flow4 = flowOf(value = false), - transform = ::createCurrenciesStatuses, - ) - .distinctUntilChanged() - } - .onEach { statusesLce -> - statusesLce.getOrNull()?.let { prevStatuses.value = it } - - send(statusesLce) - } - .launchIn(scope = this) - } - - private fun createCurrenciesStatuses( - currencies: NonEmptyList, - maybeQuotes: Either>?, - maybeNetworkStatuses: Either>?, - maybeStakingBalances: Either>?, - isUpdating: Boolean, - ): Lce> = lce { - isLoading.set(isUpdating) - - val networksStatuses = maybeNetworkStatuses?.bindEither()?.toNonEmptySetOrNull() - val stakingBalances = maybeStakingBalances?.bindEither() - val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { - null - } - - currencies.map { currency -> - val quote = quotes?.firstOrNull { it.rawCurrencyId == currency.id.rawCurrencyId } - val networkStatus = networksStatuses?.firstOrNull { it.network == currency.network } - val stakingBalance = findStakingBalanceOrNull(stakingBalances, currency, networkStatus) - - val currencyStatus = CryptoCurrencyStatusFactory.create( - currency = currency, - maybeNetworkStatus = networkStatus.toOption(), - maybeQuoteStatus = quote.toOption(), - maybeStakingBalance = stakingBalance.toOption(), - ) - - currencyStatus - } - } - - private fun findStakingBalanceOrNull( - stakingBalances: List?, - currency: CryptoCurrency, - networkStatus: NetworkStatus?, - ): StakingBalance? { - if (stakingBalances.isNullOrEmpty()) return null - - val supportedIntegration = StakingIntegrationID.create(currencyId = currency.id)?.value - val address = networkStatus.getAddress() - - return if (supportedIntegration != null && address != null) { - val stakingId = StakingID(integrationId = supportedIntegration, address = address) - - stakingBalances.firstOrNull { it.stakingId == stakingId } - ?: StakingBalance.Error(stakingId = stakingId) - } else { - null - } - } - - private fun getCurrencies(userWalletId: UserWalletId): EitherFlow> { - return currenciesRepository.getWalletCurrenciesUpdates(userWalletId) - .map, Either>> { it.right() } - .catch { emit(TokenListError.DataError(it).left()) } - .distinctUntilChanged() - } - - private fun getQuotes(tokensIds: NonEmptySet): Flow>> { - return getQuotesUpdates( - rawCurrencyIds = tokensIds.mapNotNullTo( - destination = hashSetOf(), - transform = CryptoCurrency.ID::rawCurrencyId, - ), - ) - } - - override fun getQuotes(id: CryptoCurrency.RawID): Flow>> { - return singleQuoteStatusSupplier( - params = SingleQuoteStatusProducer.Params(rawCurrencyId = id), - ) - .map>> { setOf(it).right() } - .distinctUntilChanged() - } - - // temporary code because token list is built using networks list - @OptIn(FlowPreview::class) - private fun getNetworkStatusesUpdates( - userWalletId: UserWalletId, - networks: NonEmptySet, - ): EitherFlow> { - return channelFlow { - val state = MutableStateFlow(emptySet()) - - networks.onEach { - launch { - singleNetworkStatusSupplier( - params = SingleNetworkStatusProducer.Params(userWalletId = userWalletId, network = it), - ) - .onEach { status -> - state.update { loadedStatuses -> - loadedStatuses.addOrReplace(status) { it.network == status.network } - } - } - .launchIn(scope = this) - } - } - - state - .onEach(::send) - .launchIn(scope = this) - } - .debounce(timeoutMillis = 500) - .map, Either>> { it.right() } - .distinctUntilChanged() - } - - // temporary code because token list is built using networks list - private fun getQuotesUpdates( - rawCurrencyIds: Set, - ): EitherFlow> { - return channelFlow { - val state = MutableStateFlow(emptySet()) - - rawCurrencyIds.onEach { - launch { - singleQuoteStatusSupplier( - params = SingleQuoteStatusProducer.Params(rawCurrencyId = it), - ) - .onEach { quote -> - state.update { loadedStatuses -> - loadedStatuses.addOrReplace(quote) { it.rawCurrencyId == quote.rawCurrencyId } - } - } - .launchIn(scope = this) - } - } - - state - .onEach(::send) - .launchIn(scope = this) - } - .map, Either>> { it.right() } - .distinctUntilChanged() - } - - // temporary code because token list is built using networks list - private fun getYieldsBalancesUpdates( - userWalletId: UserWalletId, - cryptoCurrencies: Map, - ): EitherFlow> { - return channelFlow { - val state = MutableStateFlow(emptyList()) - - val stakingIds = cryptoCurrencies.mapNotNullTo(hashSetOf()) { currencyWithAddress -> - stakingIdFactory.create( - currencyId = currencyWithAddress.key, - defaultAddress = currencyWithAddress.value, - ) - .getOrNull() - } - - stakingIds.onEach { stakingId -> - launch { - singleStakingBalanceSupplier( - params = SingleStakingBalanceProducer.Params( - userWalletId = userWalletId, - stakingId = stakingId, - ), - ) - .onEach { balance -> - state.update { loadedBalances -> - loadedBalances.addOrReplace(balance) { balance.stakingId == it.stakingId } - } - } - .launchIn(scope = this) - } - } - - state - .onEach { send(it.right()) } - .launchIn(scope = this) - } - .distinctUntilChanged() - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt deleted file mode 100644 index e42588514f..0000000000 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListOperations.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.tangem.domain.tokens.operations - -import arrow.core.Either -import arrow.core.left -import arrow.core.raise.either -import arrow.core.right -import arrow.core.toNonEmptyListOrNull -import com.tangem.domain.models.TokensGroupType -import com.tangem.domain.models.TokensSortType -import com.tangem.domain.models.currency.CryptoCurrencyStatus -import com.tangem.domain.models.tokenlist.TokenList -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.repository.CurrenciesRepository -import kotlinx.coroutines.flow.* - -@Suppress("LongParameterList") -internal class TokenListOperations( - private val currenciesRepository: CurrenciesRepository, - private val userWalletId: UserWalletId, - private val tokens: List, -) { - - fun getTokenListFlow(): Flow> { - return combine( - flow = getIsGrouped(), - flow2 = getIsSortedByBalance(), - ) { isGrouped, isSortedByBalance -> - either { - createTokenList(isGrouped = isGrouped.bind(), isSortedByBalance = isSortedByBalance.bind()) - } - } - } - - private fun createTokenList(isGrouped: Boolean, isSortedByBalance: Boolean): TokenList { - val nonEmptyCurrencies = tokens.toNonEmptyListOrNull() ?: return TokenList.Empty - - return TokenListFactory.create( - statuses = nonEmptyCurrencies, - groupType = if (isGrouped) TokensGroupType.NETWORK else TokensGroupType.NONE, - sortType = if (isSortedByBalance) TokensSortType.BALANCE else TokensSortType.NONE, - ) - } - - private fun getIsGrouped(): Flow> { - return currenciesRepository.isTokensGrouped(userWalletId) - .map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(value = false.right()) } - .cancellable() - } - - private fun getIsSortedByBalance(): Flow> { - return currenciesRepository.isTokensSortedByBalance(userWalletId) - .map> { it.right() } - .catch { emit(Error.DataError(it).left()) } - .onEmpty { emit(value = false.right()) } - .cancellable() - } - - sealed class Error { - - data class UnableToSortTokenList(val unsortedTokenList: TokenList.Ungrouped) : Error() - - data class DataError(val cause: Throwable) : Error() - } -} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 779e36f447..c830032607 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -5,7 +5,6 @@ import com.tangem.domain.core.error.DataError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.model.FeePaidCurrency import kotlinx.coroutines.flow.Flow @@ -71,23 +70,6 @@ interface CurrenciesRepository { id: CryptoCurrency.ID, ): CryptoCurrency - /** - * Retrieves the list of cryptocurrencies within a multi-currency wallet. - * - * Loads cryptocurrencies if they have expired or if [refresh] is `true`. - * - * @param userWalletId The unique identifier of the user wallet. - * @param refresh A boolean flag indicating whether the data should be refreshed. - * @return A list of [CryptoCurrency]. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use MultiWalletCryptoCurrenciesSupplier") - suspend fun getMultiCurrencyWalletCurrenciesSync( - userWalletId: UserWalletId, - refresh: Boolean = false, - ): List - /** * Get the coin for a specific network. * @@ -101,28 +83,6 @@ interface CurrenciesRepository { derivationPath: Network.DerivationPath, ): CryptoCurrency.Coin - /** - * Determines whether the tokens within a specific multi-currency user wallet are grouped. - * - * @param userWalletId The unique identifier of the user wallet. - * @return A [Flow] emitting a boolean value indicating whether the tokens are grouped. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use SingleAccountListSupplier instead") - fun isTokensGrouped(userWalletId: UserWalletId): Flow - - /** - * Determines whether the tokens within a specific multi-currency user wallet are sorted by balance. - * - * @param userWalletId The unique identifier of the user wallet. - * @return A [Flow] emitting a boolean value indicating whether the tokens are sorted by balance. - * @throws DataError.UserWalletError.WrongUserWallet If single-currency user wallet - * ID provided. - */ - @Deprecated("Use SingleAccountListSupplier instead") - fun isTokensSortedByBalance(userWalletId: UserWalletId): Flow - /** * Determines whether the currency sending is blocked by network pending transaction * @@ -153,10 +113,6 @@ interface CurrenciesRepository { networkId: String, ): CryptoCurrency.Token - /** Get crypto currencies by [currencyRawId] from all user wallets */ - @Deprecated("Use MultiAccountListSupplier instead") - fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow>> - fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean @Throws diff --git a/features/walletconnect/impl/build.gradle.kts b/features/walletconnect/impl/build.gradle.kts index fd948afd0b..48f84373fe 100644 --- a/features/walletconnect/impl/build.gradle.kts +++ b/features/walletconnect/impl/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { /** Domain models */ implementation(projects.domain.account) + implementation(projects.domain.account.status) implementation(projects.domain.appCurrency.models) implementation(projects.domain.balanceHiding.models) implementation(projects.domain.blockaid.models) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt index e9f6c92a3b..04132e0692 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt @@ -6,7 +6,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.domain.tokens.GetTokenListUseCase +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.wallet.utils.UserWalletsFetcher import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.WcSelectWalletParams @@ -24,7 +24,7 @@ internal class WcSelectWalletModel @Inject constructor( paramsContainer: ParamsContainer, messageSender: UiMessageSender, userWalletsFetcherFactory: UserWalletsFetcher.Factory, - getTokenListUseCase: GetTokenListUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val router: Router, override val dispatchers: CoroutineDispatcherProvider, ) : Model() { @@ -41,7 +41,7 @@ internal class WcSelectWalletModel @Inject constructor( private val userWalletsFetcher = WcUserWalletsFetcher( userWalletsFetcherFactory = userWalletsFetcherFactory, - getTokenListUseCase = getTokenListUseCase, + singleAccountStatusListSupplier = singleAccountStatusListSupplier, messageSender = messageSender, onWalletSelected = ::onWalletSelected, ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt index 742a0205bd..e5d1e47801 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt @@ -6,8 +6,9 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier +import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.GetTokenListUseCase import com.tangem.features.wallet.utils.UserWalletsFetcher import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -17,12 +18,11 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map -@Suppress("LongParameterList") @ModelScoped internal class WcUserWalletsFetcher( userWalletsFetcherFactory: UserWalletsFetcher.Factory, messageSender: UiMessageSender, - private val getTokenListUseCase: GetTokenListUseCase, + private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val onWalletSelected: (UserWalletId) -> Unit, ) { @@ -42,12 +42,13 @@ internal class WcUserWalletsFetcher( } private fun getTokenListFlow(walletItem: UserWalletItemUM): Flow { - return getTokenListUseCase.launch(UserWalletId(walletItem.id)).map { lce -> - val information = lce.fold( - ifLoading = { UserWalletItemUM.Information.Loading }, - ifError = { UserWalletItemUM.Information.Failed }, - ifContent = { tokenList -> tokenCountInfo(tokenList.flattenCurrencies().size) }, - ) + return singleAccountStatusListSupplier(userWalletId = UserWalletId(walletItem.id)).map { accountStatusList -> + val information = when (accountStatusList.totalFiatBalance) { + is TotalFiatBalance.Failed -> UserWalletItemUM.Information.Failed + is TotalFiatBalance.Loading -> UserWalletItemUM.Information.Loading + is TotalFiatBalance.Loaded -> tokenCountInfo(accountStatusList.flattenCurrencies().size) + } + walletItem.copy(information = information) } } From 5c74278e3db53e259fa91ce9a9d0ba5dd91a13e2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 4 Mar 2026 18:31:47 +0300 Subject: [PATCH 97/97] Updated on 2026-08-14 --- .../di/WalletConnectDataModule.kt | 3 - .../ethereum/WcEthAddNetworkUseCase.kt | 5 +- .../network/ethereum/WcEthNetwork.kt | 3 +- .../network/solana/WcSolanaNetwork.kt | 6 +- .../pair/AssociateNetworksDelegate.kt | 25 +- .../pair/DefaultWcPairUseCase.kt | 4 +- .../sessions/DefaultWcSessionsManager.kt | 25 +- .../sign/WcSignUseCaseDelegate.kt | 2 +- .../utils/WcNetworksConverter.kt | 41 +--- .../walletconnect/DefaultWcPairUseCaseTest.kt | 7 +- .../WcSignUseCaseDelegateTest.kt | 3 +- .../domain/walletconnect/model/WcSession.kt | 2 +- .../walletconnect/model/WcSessionApprove.kt | 2 +- .../walletconnect/model/WcSessionDTO.kt | 2 +- .../walletconnect/model/WcSessionProposal.kt | 3 +- .../connections/components/WcPairComponent.kt | 9 - .../components/WcSelectWalletComponent.kt | 216 ------------------ .../connections/entity/WcAppInfoUM.kt | 4 +- .../connections/model/WcPairModel.kt | 120 ++++------ .../connections/model/WcSelectWalletModel.kt | 60 ----- .../transformers/WcAppInfoTransformer.kt | 7 +- .../WcAppInfoWalletChangedTransformer.kt | 39 ---- .../WcSessionsAccountModeTransformer.kt | 2 +- .../connections/routes/WcAppInfoRoutes.kt | 4 - .../connections/ui/WcAppInfoBS.kt | 76 +----- .../connections/utils/WcUserWalletsFetcher.kt | 64 ------ .../di/WalletConnectModelModule.kt | 5 - .../transaction/model/WcAddNetworkModel.kt | 2 +- .../model/WcSendTransactionModel.kt | 4 +- .../model/WcSignTransactionModel.kt | 4 +- 30 files changed, 83 insertions(+), 666 deletions(-) delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt delete mode 100644 features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/utils/WcUserWalletsFetcher.kt diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt index 3ae3557cc0..b0012d93a3 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/di/WalletConnectDataModule.kt @@ -24,7 +24,6 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.MultiAccountListSupplier import com.tangem.domain.account.supplier.SingleAccountSupplier import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.WcRequestService import com.tangem.domain.walletconnect.WcRequestUseCaseFactory @@ -181,14 +180,12 @@ internal object WalletConnectDataModule { fun wcNetworksConverter( namespaceConverters: Set<@JvmSuppressWildcards WcNamespaceConverter>, walletManagersFacade: WalletManagersFacade, - multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, singleAccountStatusListSupplier: SingleAccountStatusListSupplier, singleAccountSupplier: SingleAccountSupplier, ): WcNetworksConverter = WcNetworksConverter( namespaceConverters = namespaceConverters, walletManagersFacade = walletManagersFacade, singleAccountStatusListSupplier = singleAccountStatusListSupplier, - multiWalletCryptoCurrenciesSupplier = multiWalletCryptoCurrenciesSupplier, singleAccountSupplier = singleAccountSupplier, ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt index d835bc5139..22da45b6f4 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthAddNetworkUseCase.kt @@ -131,12 +131,9 @@ internal class WcEthAddSwitchCommonDelegate @AssistedInject constructor( val caip2 = hexChainIdToCAIP2(hexChainId) ?: return HandleMethodError.UnknownError("Failed to parse CAIP2").left() val generalNetwork = networksConverter.createNetwork(caip2.raw, wallet) - if (generalNetwork == null) { - return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left() - } + ?: return HandleMethodError.TangemUnsupportedNetwork(caip2.raw).left() val addedNetwork = networksConverter.mainOrAnyWalletNetworkForRequest( rawChainId = caip2.raw, - wallet = wallet, account = context.session.account, ) if (addedNetwork == null) { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt index c03d165fa0..a1d65ed85f 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/ethereum/WcEthNetwork.kt @@ -47,7 +47,6 @@ internal class WcEthNetwork( ?: return error("Failed to parse $name") suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest( rawChainId = chainId, - wallet = wallet, account = account, ) @@ -74,7 +73,7 @@ internal class WcEthNetwork( -> anyExistNetwork() } ?: return error("Failed to find walletNetwork for accountAddress $accountAddress") - val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size + val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, account).size val context = WcMethodUseCaseContext( session = session, rawSdkRequest = request, diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt index bb68bd46d2..0265091789 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/network/solana/WcSolanaNetwork.kt @@ -49,7 +49,7 @@ internal class WcSolanaNetwork( val wallet = session.wallet val account = session.account val chainId = request.chainId.orEmpty() - suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, wallet, account) + suspend fun anyExistNetwork() = networksConverter.mainOrAnyWalletNetworkForRequest(chainId, account) suspend fun anyAddress() = anyExistNetwork() ?.let { network -> networksConverter.getAddressForWC(wallet.walletId, network).orEmpty() } .orEmpty() @@ -64,7 +64,7 @@ internal class WcSolanaNetwork( ?: anyExistNetwork() ?: return error("Failed to find walletNetwork for accountAddress $accountAddress") - val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, wallet, account).size + val networkDerivationsCount = networksConverter.filterWalletNetworkForRequest(chainId, account).size val context = WcMethodUseCaseContext( session = session, rawSdkRequest = request, @@ -86,7 +86,7 @@ internal class WcSolanaNetwork( override val namespaceKey: NamespaceKey = NamespaceKey("solana") override fun toBlockchain(chainId: CAIP2): Blockchain? { - val isMainNet = MAINNET_CHAIN_ID.any { it.lowercase() == chainId.reference.lowercase() } + val isMainNet = MAINNET_CHAIN_ID.any { it.equals(chainId.reference, ignoreCase = true) } if (chainId.namespace != namespaceKey.key) return null return when { isMainNet -> Blockchain.Solana diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt index a1384944b8..c35cd5c848 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/AssociateNetworksDelegate.kt @@ -8,7 +8,6 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.network.Network 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.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.model.WcPairError @@ -23,19 +22,6 @@ internal class AssociateNetworksDelegate( private val getWallets: GetWalletsUseCase, ) { - @Throws(WcPairError.UnsupportedBlockchains::class) - suspend fun associate(sessionProposal: Wallet.Model.SessionProposal): Map { - val userWallets = getWallets.invokeSync().filter { it.isMultiCurrency } - val requiredNamespaces: Set = sessionProposal.requiredNamespaces.setOfChainId() - val optionalNamespaces: Set = sessionProposal.optionalNamespaces.setOfChainId() - // remove duplicates - .subtract(requiredNamespaces) - - return userWallets.associateWith { wallet -> - mapNetworksForPortfolio(wallet, null, requiredNamespaces, optionalNamespaces, sessionProposal) - } - } - @Throws(WcPairError.UnsupportedBlockchains::class) suspend fun associateAccounts(sessionProposal: Wallet.Model.SessionProposal): Map { val userWallets = getWallets.invokeSync() @@ -67,13 +53,12 @@ internal class AssociateNetworksDelegate( @Suppress("CyclomaticComplexMethod") private suspend fun mapNetworksForPortfolio( wallet: UserWallet, - account: Account?, + account: Account, requiredNamespaces: Set, optionalNamespaces: Set, sessionProposal: Wallet.Model.SessionProposal, ): ProposalNetwork { - val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) } - ?: getWalletNetworks(userWalletId = wallet.walletId) + val portfolioNetworks = getAccountNetworks(account.accountId) val unknownRequired = mutableSetOf() val unknownOptional = mutableSetOf() @@ -127,12 +112,6 @@ internal class AssociateNetworksDelegate( ) } - private suspend fun getWalletNetworks(userWalletId: UserWalletId): List { - return networksConverter.getWalletNetworks(userWalletId) - // flatten all derivation - .distinctBy { it.rawId } - } - private suspend fun getAccountNetworks(accountId: AccountId): List { return networksConverter.getAccountNetworks(accountId) // flatten all derivation diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt index 432700dc15..bf81785e59 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/pair/DefaultWcPairUseCase.kt @@ -111,7 +111,7 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( val sessionDTO = WcSessionDTO( topic = "", walletId = sessionForApprove.wallet.walletId, - accountId = sessionForApprove.account?.accountId, + accountId = sessionForApprove.account.accountId, url = sdkVerifyContext.getDappOriginUrl(), securityStatus = proposalState.dAppSession.securityStatus, connectingTime = connectingTime, @@ -195,7 +195,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( sessionProposal: Wallet.Model.SessionProposal, verifyContext: Wallet.Model.VerifyContext, ): Either = runCatching { - val proposalNetwork = associateNetworksDelegate.associate(sessionProposal) val proposalAccountNetwork = associateNetworksDelegate.associateAccounts(sessionProposal) val verificationInfo = when { verifyContext.validation == Wallet.Model.Validation.INVALID -> CheckDAppResult.UNSAFE @@ -224,7 +223,6 @@ internal class DefaultWcPairUseCase @AssistedInject constructor( ) val dAppSession = WcSessionProposal( dAppMetaData = appMetaData, - proposalNetwork = proposalNetwork, securityStatus = verificationInfo, proposalAccountNetwork = proposalAccountNetwork, ) diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt index 7967e2d59f..96a3982807 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sessions/DefaultWcSessionsManager.kt @@ -9,7 +9,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.utils.* import com.tangem.datasource.local.walletconnect.WalletConnectStore import com.tangem.domain.models.account.Account -import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcSession @@ -37,15 +36,12 @@ internal class DefaultWcSessionsManager( ) : WcSessionsManager, WcSdkObserver { private val onSessionDelete = Channel(capacity = Channel.BUFFERED) - private val oneTimeMigration = MutableStateFlow(false) override val sessions: Flow>> get() = combine(getWallets(), store.sessions) { wallets, inStore -> wallets to inStore } .transform { pair -> val (wallets, inStore) = pair val inSdk: List = WalletKit.getListOfActiveSessions() - val someMigrate = migrateToAccountSession(inStore) - if (someMigrate) return@transform val associatedSessions: List = associate(inSdk, inStore, wallets) val someRemove = removeUnknownSessions(inStore, inSdk, associatedSessions) if (someRemove) return@transform // ignore emit, wait next one @@ -54,25 +50,6 @@ internal class DefaultWcSessionsManager( .distinctUntilChanged() .flowOn(dispatchers.io) - private suspend fun migrateToAccountSession(inStore: Set): Boolean { - if (oneTimeMigration.value) return false - - var someMigrated = false - - val updatedSessions = inStore.mapTo(mutableSetOf()) { sessionDTO -> - if (sessionDTO.accountId == null) { - someMigrated = true - val mainAccountId = AccountId.forMainCryptoPortfolio(sessionDTO.walletId) - sessionDTO.copy(accountId = mainAccountId) - } else { - sessionDTO - } - } - if (someMigrated) store.saveSessions(updatedSessions) - oneTimeMigration.value = true - return someMigrated - } - override fun onWcSdkInit() { listenOnSessionDelete() extendSessions() @@ -114,7 +91,7 @@ internal class DefaultWcSessionsManager( val wcSessions = savedPending.plus(inStore).mapNotNull { storeSession -> val wallet = wallets.find { it.walletId == storeSession.walletId } ?: return@mapNotNull null val sdkSession = inSdk.find { it.topic == storeSession.topic } ?: return@mapNotNull null - val account = storeSession.accountId?.let { wcNetworksConverter.getAccount(it) } as? Account.CryptoPortfolio + val account = wcNetworksConverter.getAccount(storeSession.accountId) as? Account.CryptoPortfolio ?: return@mapNotNull null val networks = wcNetworksConverter.findWalletNetworks(wallet, account, sdkSession) val originUrl = storeSession.url ?: sdkSession.metaData?.url ?: "" diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt index ebf56dafcf..d3c3d64bdf 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/WcSignUseCaseDelegate.kt @@ -71,7 +71,7 @@ internal class WcSignUseCaseDelegate( network = context.network, errorCode = error.code(), errorMessage = errorMessage, - accountDerivation = context.session.account?.derivationIndex?.value, + accountDerivation = context.session.account.derivationIndex.value, ) analytics.send(event) } diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index d4b19d974a..9104712d62 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -18,8 +18,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer -import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.WcSessionApprove import com.tangem.domain.walletconnect.model.sdkcopy.WcSdkSessionRequest @@ -31,7 +29,6 @@ internal class WcNetworksConverter @Inject constructor( private val walletManagersFacade: WalletManagersFacade, private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, private val singleAccountSupplier: SingleAccountSupplier, - private val multiWalletCryptoCurrenciesSupplier: MultiWalletCryptoCurrenciesSupplier, ) { fun createNetwork(chainId: String, wallet: UserWallet): Network? { @@ -47,13 +44,12 @@ internal class WcNetworksConverter @Inject constructor( val wallet = session.wallet val allCoinNetwork = filterWalletNetworkForRequest( rawChainId = request.chainId.orEmpty(), - wallet = session.wallet, account = session.account, ) val requestNetwork = allCoinNetwork.find { network -> val address = getAddressForWC(wallet.walletId, network) - requestAddress.lowercase() == address?.lowercase() + requestAddress.equals(address, ignoreCase = true) } return requestNetwork } @@ -61,13 +57,13 @@ internal class WcNetworksConverter @Inject constructor( /** * return network with not custom derivationPath or first custom or any */ - suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, wallet: UserWallet, account: Account?): Network? { - val networks = filterWalletNetworkForRequest(rawChainId, wallet, account) + suspend fun mainOrAnyWalletNetworkForRequest(rawChainId: String, account: Account): Network? { + val networks = filterWalletNetworkForRequest(rawChainId, account) return networks.firstOrNull { !isCustomCoin(it) } ?: networks.firstOrNull() } - suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet, account: Account?): List { - return filterWalletNetworkForRequest(rawChainId, wallet, account) + suspend fun allAddressForChain(rawChainId: String, wallet: UserWallet, account: Account): List { + return filterWalletNetworkForRequest(rawChainId, account) .mapNotNull { getAddressForWC(wallet.walletId, it)?.lowercase() } } @@ -85,13 +81,8 @@ internal class WcNetworksConverter @Inject constructor( /** * return all exist derivation networks */ - suspend fun filterWalletNetworkForRequest( - rawChainId: String, - wallet: UserWallet, - account: Account?, - ): List { - val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) } - ?: getWalletNetworks(wallet.walletId) + suspend fun filterWalletNetworkForRequest(rawChainId: String, account: Account): List { + val portfolioNetworks = getAccountNetworks(account.accountId) val blockchain = namespaceConverters .firstNotNullOfOrNull { it.toBlockchain(rawChainId) } ?: return listOf() @@ -102,11 +93,10 @@ internal class WcNetworksConverter @Inject constructor( suspend fun findWalletNetworks( wallet: UserWallet, - account: Account?, + account: Account, sdkSession: Wallet.Model.Session, ): Set { - val portfolioNetworks = account?.let { getAccountNetworks(it.accountId) } - ?: getWalletNetworks(wallet.walletId) + val portfolioNetworks = getAccountNetworks(account.accountId) val existNetworks = sdkSession.namespaces.values .map { it.accounts }.flatten().toSet() .mapNotNull { CAIP10.fromRaw(it) } @@ -120,7 +110,7 @@ internal class WcNetworksConverter @Inject constructor( // find equal address .firstOrNull { network -> val walletAddress = getAddressForWC(wallet.walletId, network) - walletAddress?.lowercase() == caip10.accountAddress.lowercase() + walletAddress.equals(caip10.accountAddress, ignoreCase = true) } } @@ -132,21 +122,12 @@ internal class WcNetworksConverter @Inject constructor( } suspend fun convertNetworksForApprove(sessionForApprove: WcSessionApprove): List { - val portfolioNetworks = sessionForApprove.account?.let { getAccountNetworks(it.accountId) } - ?: getWalletNetworks(sessionForApprove.wallet.walletId) + val portfolioNetworks = getAccountNetworks(sessionForApprove.account.accountId) return sessionForApprove.network .map { network -> portfolioNetworks.filter { walletNetwork -> walletNetwork.rawId == network.rawId } } .flatten() } - suspend fun getWalletNetworks(userWalletId: UserWalletId): List { - return multiWalletCryptoCurrenciesSupplier.getSyncOrNull( - params = MultiWalletCryptoCurrenciesProducer.Params(userWalletId = userWalletId), - ) - .orEmpty() - .filterIsInstance().map(CryptoCurrency.Coin::network) - } - private suspend fun getAccountStatus(accountId: AccountId): AccountStatus.CryptoPortfolio? { return singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(accountId.userWalletId), diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt index 9d81be58d2..afaed251c9 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/DefaultWcPairUseCaseTest.kt @@ -15,6 +15,7 @@ import com.tangem.data.walletconnect.pair.DefaultWcPairUseCase import com.tangem.data.walletconnect.pair.WcPairSdkDelegate import com.tangem.data.walletconnect.utils.WcSdkSessionConverter import com.tangem.domain.blockaid.BlockAidVerifier +import com.tangem.domain.models.account.Account import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairRequest @@ -73,7 +74,7 @@ internal class DefaultWcPairUseCaseTest { get() = WcSessionApprove( wallet = MockUserWalletFactory.create(), network = listOf(), - account = null, + account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId), ) private val sdkApprove: Wallet.Params.SessionApprove @@ -106,7 +107,7 @@ internal class DefaultWcPairUseCaseTest { networks = setOf(), connectingTime = null, showWalletInfo = false, - account = null, + account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId), ) private fun useCaseFactory() = DefaultWcPairUseCase( @@ -120,7 +121,6 @@ internal class DefaultWcPairUseCaseTest { @Before fun setup() { - coEvery { associateNetworksDelegate.associate(sdkProposal) } returns mapOf() coEvery { associateNetworksDelegate.associateAccounts(sdkProposal) } returns mapOf() coEvery { caipNamespaceDelegate.associate( @@ -254,7 +254,6 @@ internal class DefaultWcPairUseCaseTest { assertEquals(loading, awaitItem()) coVerifyOrder { sdkDelegate.pair(url) - associateNetworksDelegate.associate(sdkProposal) blockAidVerifier.verifyDApp(DAppData(sdkVerifyContext.origin)) } assert(awaitItem() is WcPairState.Proposal) diff --git a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt index d703676123..069d84224c 100644 --- a/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt +++ b/data/wallet-connect/src/test/kotlin/com/tangem/domain/walletconnect/WcSignUseCaseDelegateTest.kt @@ -10,6 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.data.walletconnect.sign.* import com.tangem.data.walletconnect.sign.SignStateConverter.toResult import com.tangem.data.walletconnect.sign.SignStateConverter.toSigning +import com.tangem.domain.models.account.Account import com.tangem.domain.walletconnect.model.WcRequestError import com.tangem.domain.walletconnect.model.WcSession import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData @@ -58,7 +59,7 @@ internal class WcSignUseCaseDelegateTest { session = WcSession( wallet = MockUserWalletFactory.create(), networks = setOf(), - account = null, + account = Account.CryptoPortfolio.createMainAccount(MockUserWalletFactory.create().walletId), securityStatus = CheckDAppResult.FAILED_TO_VERIFY, connectingTime = 0L, sdkModel = WcSdkSession( diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt index cf3f4c9f37..20ffd7cea6 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSession.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.wallet.UserWallet data class WcSession( val wallet: UserWallet, - val account: Account.CryptoPortfolio?, + val account: Account.CryptoPortfolio, val networks: Set, val sdkModel: WcSdkSession, val securityStatus: CheckDAppResult, diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt index e55e69f940..75c6fb808c 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionApprove.kt @@ -6,6 +6,6 @@ import com.tangem.domain.models.wallet.UserWallet data class WcSessionApprove( val wallet: UserWallet, - val account: Account?, + val account: Account, val network: List, ) \ No newline at end of file diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt index eaee191c92..21a22308f8 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionDTO.kt @@ -9,7 +9,7 @@ import com.tangem.domain.models.wallet.UserWalletId data class WcSessionDTO( val topic: String, val walletId: UserWalletId, - val accountId: AccountId? = null, + val accountId: AccountId = AccountId.forMainCryptoPortfolio(walletId), val url: String?, val securityStatus: CheckDAppResult = CheckDAppResult.FAILED_TO_VERIFY, val connectingTime: Long? = null, diff --git a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt index dbec25e6d2..9850e0418e 100644 --- a/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt +++ b/domain/wallet-connect/models/src/main/java/com/tangem/domain/walletconnect/model/WcSessionProposal.kt @@ -9,8 +9,7 @@ import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData data class WcSessionProposal( val dAppMetaData: WcAppMetaData, - val proposalNetwork: Map, - val proposalAccountNetwork: Map?, + val proposalAccountNetwork: Map, val securityStatus: CheckDAppResult, ) { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt index 220f294b49..1675ff7fba 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcPairComponent.kt @@ -66,7 +66,6 @@ internal class WcPairComponent( else -> model.stackNavigation.pop() } is WcAppInfoRoutes.SelectNetworks, - is WcAppInfoRoutes.SelectWallet, is WcAppInfoRoutes.PortfolioSelector, -> model.stackNavigation.pop() } @@ -102,14 +101,6 @@ internal class WcPairComponent( callback = model, ), ) - is WcAppInfoRoutes.SelectWallet -> WcSelectWalletComponent( - appComponentContext = appComponentContext, - params = WcSelectWalletComponent.WcSelectWalletParams( - selectedWalletId = config.selectedWalletId, - onDismiss = ::dismiss, - callback = model, - ), - ) WcAppInfoRoutes.PortfolioSelector -> portfolioSelectorComponentFactory.create( context = appComponentContext, params = PortfolioSelectorComponent.Params( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt deleted file mode 100644 index 93e8566ec0..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/components/WcSelectWalletComponent.kt +++ /dev/null @@ -1,216 +0,0 @@ -package com.tangem.features.walletconnect.connections.components - -import android.content.res.Configuration -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.tangem.common.ui.userwallet.UserWalletItem -import com.tangem.common.ui.userwallet.state.UserWalletItemUM -import com.tangem.core.decompose.context.AppComponentContext -import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.core.ui.components.block.TangemBlockCardColors -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig -import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet -import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle -import com.tangem.core.ui.decompose.ComposableBottomSheetComponent -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.res.TangemTheme -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.walletconnect.connections.model.WcSelectWalletModel -import com.tangem.features.walletconnect.impl.R -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf - -internal class WcSelectWalletComponent( - appComponentContext: AppComponentContext, - private val params: WcSelectWalletParams, -) : AppComponentContext by appComponentContext, ComposableBottomSheetComponent { - - private val model: WcSelectWalletModel = getOrCreateModel(params = params) - - override fun dismiss() { - params.onDismiss() - } - - @Composable - override fun BottomSheet() { - val state by model.state.collectAsStateWithLifecycle() - WcSelectWalletModalBS( - wallets = state.wallets, - selectedWalletId = state.selectedUserWalletId, - onBack = router::pop, - onDismiss = ::dismiss, - ) - } - - interface ModelCallback { - fun onWalletSelected(userWalletId: UserWalletId) - } - - data class WcSelectWalletParams( - val selectedWalletId: UserWalletId, - val callback: ModelCallback, - val onDismiss: () -> Unit, - ) -} - -@Composable -private fun WcSelectWalletModalBS( - wallets: ImmutableList, - selectedWalletId: UserWalletId, - onBack: () -> Unit, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, -) { - if (wallets.isEmpty()) return - - TangemModalBottomSheet( - config = TangemBottomSheetConfig( - isShown = true, - onDismissRequest = onDismiss, - content = TangemBottomSheetConfigContent.Empty, - ), - onBack = onBack, - containerColor = TangemTheme.colors.background.primary, - title = { - TangemModalBottomSheetTitle( - title = resourceReference(R.string.common_choose_wallet), - startIconRes = R.drawable.ic_back_24, - onStartClick = onBack, - ) - }, - content = { - WcSelectWalletContent( - modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - wallets = wallets, - selectedWalletId = selectedWalletId, - ) - }, - ) -} - -@Composable -private fun WcSelectWalletContent( - wallets: ImmutableList, - selectedWalletId: UserWalletId, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - wallets.fastForEach { state -> - key(state.id) { - val baseModifier = Modifier - .clip(RoundedCornerShape(14.dp)) - .clickable(onClick = state.onClick) - val itemModifier = if (state.id == selectedWalletId.stringValue) { - baseModifier.border( - width = 1.dp, - color = TangemTheme.colors.text.accent, - shape = RoundedCornerShape(14.dp), - ) - } else { - baseModifier - } - UserWalletItem( - modifier = itemModifier, - state = state, - blockColors = TangemBlockCardColors.copy( - containerColor = Color.Unspecified, - disabledContainerColor = Color.Unspecified, - ), - ) - } - } - } -} - -@Suppress("LongMethod") -@Composable -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO) -@Preview(showBackground = true, device = Devices.PIXEL_7_PRO, uiMode = Configuration.UI_MODE_NIGHT_YES) -private fun WcSelectWalletContent_Preview() { - val wallets = persistentListOf( - UserWalletItemUM( - id = "user_wallet_1", - name = stringReference("Tangem 2.0"), - information = getInformation(42), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_2", - name = stringReference("Tangem White"), - information = getInformation(24), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_3", - name = stringReference("Bitcoin"), - information = getInformation(1), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_4", - name = stringReference("Tangem 1.0"), - information = getInformation(21), - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_4", - name = stringReference("Tangem 1.0"), - information = UserWalletItemUM.Information.Loading, - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - UserWalletItemUM( - id = "user_wallet_4", - name = stringReference("Tangem 1.0"), - information = UserWalletItemUM.Information.Failed, - balance = UserWalletItemUM.Balance.Loaded("1 496,34 $", isFlickering = false), - isEnabled = true, - onClick = {}, - ), - ) - TangemThemePreview { - WcSelectWalletModalBS( - wallets = wallets, - selectedWalletId = UserWalletId(wallets.first().id.encodeToByteArray()), - onBack = {}, - onDismiss = {}, - ) - } -} - -private fun getInformation(tokenCount: Int): UserWalletItemUM.Information.Loaded { - val text = TextReference.PluralRes( - id = R.plurals.card_label_token_count, - count = tokenCount, - formatArgs = wrappedList(tokenCount), - ) - return UserWalletItemUM.Information.Loaded(text) -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt index cf9d41811a..16a0bfeda6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/entity/WcAppInfoUM.kt @@ -27,9 +27,7 @@ internal sealed class WcAppInfoUM : TangemBottomSheetConfigContent { val verifiedDAppState: VerifiedDAppState, val appSubtitle: String, val notification: WcAppInfoSecurityNotification?, - val portfolioSelectRow: PortfolioSelectUM?, - val walletName: String, - val onWalletClick: (() -> Unit)?, + val portfolioSelectRow: PortfolioSelectUM, val networksInfo: WcNetworksInfo, val onNetworksClick: () -> Unit, override val connectButtonConfig: WcPrimaryButtonConfig, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt index d59a81f60d..a8bc0da03c 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcPairModel.kt @@ -21,16 +21,11 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.message.SnackbarMessage import com.tangem.core.ui.message.ToastMessage -import com.tangem.domain.account.producer.SingleAccountListProducer -import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.network.Network 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.models.wallet.isMultiCurrency import com.tangem.domain.walletconnect.WcAnalyticEvents import com.tangem.domain.walletconnect.model.WcPairError import com.tangem.domain.walletconnect.model.WcPairError.Unknown @@ -40,16 +35,17 @@ import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.domain.walletconnect.model.sdkcopy.WcAppMetaData import com.tangem.domain.walletconnect.usecase.pair.WcPairState import com.tangem.domain.walletconnect.usecase.pair.WcPairUseCase -import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorComponent import com.tangem.features.account.PortfolioSelectorController import com.tangem.features.walletconnect.connections.components.WcPairComponent import com.tangem.features.walletconnect.connections.components.WcSelectNetworksComponent -import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM import com.tangem.features.walletconnect.connections.entity.WcPrimaryButtonConfig -import com.tangem.features.walletconnect.connections.model.transformers.* +import com.tangem.features.walletconnect.connections.model.transformers.WcAppInfoTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcConnectButtonProgressTransformer +import com.tangem.features.walletconnect.connections.model.transformers.WcDAppVerifiedStateConverter +import com.tangem.features.walletconnect.connections.model.transformers.WcNetworksSelectedTransformer import com.tangem.features.walletconnect.connections.routes.WcAppInfoRoutes import com.tangem.features.walletconnect.impl.R import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -61,11 +57,8 @@ import kotlin.properties.Delegates import com.tangem.utils.transformer.update as transformerUpdate internal interface WcPairComponentCallback : - WcSelectWalletComponent.ModelCallback, WcSelectNetworksComponent.ModelCallback -private const val WC_WALLETS_SELECTOR_MIN_COUNT = 2 - @Stable @ModelScoped @Suppress("LongParameterList", "LargeClass") @@ -75,11 +68,9 @@ internal class WcPairModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, private val analytics: AnalyticsEventHandler, val selectorController: PortfolioSelectorController, - private val singleAccountListSupplier: SingleAccountListSupplier, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, portfolioFetcherFactory: PortfolioFetcher.Factory, wcPairUseCaseFactory: WcPairUseCase.Factory, - getWalletsUseCase: GetWalletsUseCase, paramsContainer: ParamsContainer, ) : Model(), WcPairComponentCallback { @@ -102,9 +93,6 @@ internal class WcPairModel @Inject constructor( override val onBack: () -> Unit = { stackNavigation.pop() } } - private val selectedUserWalletFlow: MutableStateFlow by lazy { - MutableStateFlow(getWalletsUseCase.invokeSync().first { it.walletId == params.userWalletId }) - } private val selectedPortfolio = MutableSharedFlow>( replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST, @@ -119,14 +107,15 @@ internal class WcPairModel @Inject constructor( init { modelScope.launch { - val params = SingleAccountListProducer.Params(params.userWalletId) - val accountList = singleAccountListSupplier.getSyncOrNull(params) - if (accountList == null) { + val portfolioBalance = portfolioFetcher.data.first().balances + .firstNotNullOfOrNull { (walletId, balance) -> + if (params.userWalletId == walletId) balance else null + } + if (portfolioBalance == null) { router.pop() return@launch } - val firstAccount = accountList.accounts.first() - selectorController.selectAccount(firstAccount.accountId) + selectorController.selectAccount(portfolioBalance.accountsBalance.mainAccount.accountId) combineFlows(portfolioFetcher) } } @@ -135,10 +124,12 @@ internal class WcPairModel @Inject constructor( combine( flow = portfolioFetcher.data, flow2 = selectorController.selectedAccountWithData(portfolioFetcher) - .distinctUntilChanged() .filterNotNull() .onEach { selectedPortfolio.tryEmit(it) } - .onEach { stackNavigation.pop() }, + .runningReduce { _, new -> + stackNavigation.pop() + new + }, flow3 = wcPairUseCase(), flow4 = isAccountsModeEnabledUseCase(), transform = { portfolios, selected, pairState, isAccountMode -> @@ -155,9 +146,9 @@ internal class WcPairModel @Inject constructor( private suspend fun handlePairState( pairState: WcPairState, - portfolios: PortfolioFetcher.Data? = null, - selected: Pair? = null, - isAccountMode: Boolean? = null, + portfolios: PortfolioFetcher.Data, + selected: Pair, + isAccountMode: Boolean, ) { when (pairState) { is WcPairState.Approving.Loading -> appInfoUiState.transformerUpdate( @@ -184,36 +175,32 @@ internal class WcPairModel @Inject constructor( pairState = pairState, portfolios = portfolios, selected = selected, + isAccountMode = isAccountMode, ) } } private suspend fun handleProposalState( pairState: WcPairState.Proposal, - portfolios: PortfolioFetcher.Data? = null, - selected: Pair? = null, + portfolios: PortfolioFetcher.Data, + selected: Pair, + isAccountMode: Boolean, ) { - val availableWallets = pairState.dAppSession.proposalNetwork.keys - .filter { !it.isLocked && it.isMultiCurrency } sessionProposal = pairState.dAppSession - val selectedUserWalletFlow = this.selectedUserWalletFlow - val portfolioWallet = selected?.first - val portfolioAccount = selected?.second - val portfolioAccountId = portfolioAccount?.account?.accountId + val portfolioAccount = selected.second + val portfolioAccountId = portfolioAccount.account.accountId val proposalAccountNetwork = sessionProposal.proposalAccountNetwork - val foundNetwork = if (portfolioAccountId != null) { - requireNotNull(proposalAccountNetwork)[portfolioAccountId] - } else { - sessionProposal.proposalNetwork[selectedUserWalletFlow.value] - } + val foundNetwork = proposalAccountNetwork[portfolioAccountId] if (foundNetwork == null) { processError(Unknown("Selected wallet not found")) } else { - val portfolioSelectRow = tryToCreatePortfolioSelectRow(selected, portfolios) - if (proposalAccountNetwork != null) { - selectorController.isEnabled.value = { _, account -> - proposalAccountNetwork.contains(account.account.accountId) - } + val portfolioSelectRow = createPortfolioSelectRow( + selectedPortfolio = selected, + portfolios = portfolios, + isAccountMode = isAccountMode, + ) + selectorController.isEnabled.value = { _, account -> + proposalAccountNetwork.contains(account.account.accountId) } proposalNetwork = foundNetwork additionallyEnabledNetworks = proposalNetwork.available @@ -224,13 +211,6 @@ internal class WcPairModel @Inject constructor( onDismiss = ::rejectPairing, onConnect = ::onConnect, portfolioSelectRow = portfolioSelectRow, - onWalletClick = { - stackNavigation.pushNew( - WcAppInfoRoutes.SelectWallet(selectedUserWalletFlow.value.walletId), - ) - }.takeIf { - portfolioSelectRow == null && availableWallets.size >= WC_WALLETS_SELECTOR_MIN_COUNT - }, onNetworksClick = { stackNavigation.pushNew( WcAppInfoRoutes.SelectNetworks( @@ -242,7 +222,6 @@ internal class WcPairModel @Inject constructor( ), ) }, - userWallet = portfolioWallet ?: selectedUserWalletFlow.value, proposalNetwork = proposalNetwork, additionallyEnabledNetworks = additionallyEnabledNetworks, ), @@ -250,17 +229,15 @@ internal class WcPairModel @Inject constructor( } } - private suspend fun tryToCreatePortfolioSelectRow( - selectedPortfolio: Pair?, - portfolios: PortfolioFetcher.Data?, - ): PortfolioSelectUM? { - selectedPortfolio ?: return null - portfolios ?: return null + private suspend fun createPortfolioSelectRow( + selectedPortfolio: Pair, + portfolios: PortfolioFetcher.Data, + isAccountMode: Boolean, + ): PortfolioSelectUM { val (wallet, portfolioAccount) = selectedPortfolio val account = when (val account = portfolioAccount.account) { is Account.CryptoPortfolio -> account } - val isAccountMode = selectorController.isAccountMode.first() val icon: AccountIconUM.CryptoPortfolio? val name: TextReference if (isAccountMode) { @@ -303,15 +280,16 @@ internal class WcPairModel @Inject constructor( private fun connect() { val enabledAvailableNetworks = proposalNetwork.available.filter { network -> network in additionallyEnabledNetworks } - val selectedPortfolio = selectedPortfolio.replayCache.firstOrNull() - val wallet = selectedPortfolio?.first ?: selectedUserWalletFlow.value - val account = selectedPortfolio?.second?.account + val selectedPortfolio = selectedPortfolio.replayCache + .firstOrNull() + ?: return + val (wallet, account) = selectedPortfolio modelScope.launch { analytics.send( WcAnalyticEvents.PairButtonConnect( dAppName = sessionProposal.dAppMetaData.name, - accountDerivation = account?.derivationIndex?.value, + accountDerivation = account.account.derivationIndex.value, ), ) } @@ -319,7 +297,7 @@ internal class WcPairModel @Inject constructor( WcSessionApprove( wallet = wallet, network = enabledAvailableNetworks + proposalNetwork.required, - account = account, + account = account.account, ), ) } @@ -375,20 +353,6 @@ internal class WcPairModel @Inject constructor( alert?.let { stackNavigation.pushNew(it) } } - override fun onWalletSelected(userWalletId: UserWalletId) { - val selectedUserWallet = sessionProposal.proposalNetwork.keys.first { it.walletId == userWalletId } - proposalNetwork = sessionProposal.proposalNetwork[selectedUserWallet] ?: return - selectedUserWalletFlow.update { selectedUserWallet } - additionallyEnabledNetworks = proposalNetwork.available - appInfoUiState.transformerUpdate( - WcAppInfoWalletChangedTransformer( - selectedUserWallet = selectedUserWallet, - proposalNetwork = proposalNetwork, - additionallyEnabledNetworks = additionallyEnabledNetworks, - ), - ) - } - override fun onNetworksSelected(selectedNetworks: Set) { additionallyEnabledNetworks = selectedNetworks appInfoUiState.transformerUpdate( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt deleted file mode 100644 index 04132e0692..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/WcSelectWalletModel.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.tangem.features.walletconnect.connections.model - -import androidx.compose.runtime.Stable -import com.tangem.core.decompose.di.ModelScoped -import com.tangem.core.decompose.model.Model -import com.tangem.core.decompose.model.ParamsContainer -import com.tangem.core.decompose.navigation.Router -import com.tangem.core.decompose.ui.UiMessageSender -import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.features.wallet.utils.UserWalletsFetcher -import com.tangem.features.walletconnect.connections.components.WcSelectWalletComponent.WcSelectWalletParams -import com.tangem.features.walletconnect.connections.entity.WcAppInfoWalletUM -import com.tangem.features.walletconnect.connections.utils.WcUserWalletsFetcher -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.* -import javax.inject.Inject - -@Suppress("LongParameterList") -@Stable -@ModelScoped -internal class WcSelectWalletModel @Inject constructor( - paramsContainer: ParamsContainer, - messageSender: UiMessageSender, - userWalletsFetcherFactory: UserWalletsFetcher.Factory, - private val singleAccountStatusListSupplier: SingleAccountStatusListSupplier, - private val router: Router, - override val dispatchers: CoroutineDispatcherProvider, -) : Model() { - - private val params = paramsContainer.require() - - internal val state: StateFlow - field = MutableStateFlow( - WcAppInfoWalletUM( - wallets = persistentListOf(), - selectedUserWalletId = params.selectedWalletId, - ), - ) - - private val userWalletsFetcher = WcUserWalletsFetcher( - userWalletsFetcherFactory = userWalletsFetcherFactory, - singleAccountStatusListSupplier = singleAccountStatusListSupplier, - messageSender = messageSender, - onWalletSelected = ::onWalletSelected, - ) - - init { - userWalletsFetcher - .userWallets - .onEach { state.update { state -> state.copy(wallets = it) } } - .launchIn(modelScope) - } - - private fun onWalletSelected(userWalletId: UserWalletId) { - params.callback.onWalletSelected(userWalletId) - router.pop() - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt index d6099eaaa2..e8597c20e5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoTransformer.kt @@ -3,7 +3,6 @@ package com.tangem.features.walletconnect.connections.model.transformers import com.domain.blockaid.models.dapp.CheckDAppResult import com.tangem.common.ui.account.PortfolioSelectUM import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.walletconnect.model.WcSessionProposal import com.tangem.features.walletconnect.connections.entity.WcAppInfoSecurityNotification import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM @@ -16,10 +15,8 @@ internal class WcAppInfoTransformer( private val dAppVerifiedStateConverter: WcDAppVerifiedStateConverter, private val onDismiss: () -> Unit, private val onConnect: (securityStatus: CheckDAppResult) -> Unit, - private val portfolioSelectRow: PortfolioSelectUM?, - private val onWalletClick: (() -> Unit)?, + private val portfolioSelectRow: PortfolioSelectUM, private val onNetworksClick: () -> Unit, - private val userWallet: UserWallet, private val proposalNetwork: WcSessionProposal.ProposalNetwork, private val additionallyEnabledNetworks: Set, ) : Transformer { @@ -33,8 +30,6 @@ internal class WcAppInfoTransformer( ), appSubtitle = WcAppSubtitleConverter.convert(dAppSession.dAppMetaData), notification = createNotification(dAppSession.securityStatus), - walletName = userWallet.name, - onWalletClick = onWalletClick, portfolioSelectRow = portfolioSelectRow, networksInfo = WcNetworksInfoConverter.convert( value = WcNetworksInfoConverter.Input( diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt deleted file mode 100644 index 54d753bd72..0000000000 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcAppInfoWalletChangedTransformer.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.tangem.features.walletconnect.connections.model.transformers - -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.walletconnect.model.WcSessionProposal -import com.tangem.features.walletconnect.connections.entity.WcAppInfoUM -import com.tangem.utils.transformer.Transformer - -internal class WcAppInfoWalletChangedTransformer( - private val selectedUserWallet: UserWallet, - private val proposalNetwork: WcSessionProposal.ProposalNetwork, - private val additionallyEnabledNetworks: Set, -) : Transformer { - override fun transform(prevState: WcAppInfoUM): WcAppInfoUM { - val contentState = prevState as? WcAppInfoUM.Content ?: return prevState - return contentState.copy( - walletName = selectedUserWallet.name, - networksInfo = WcNetworksInfoConverter.convert( - WcNetworksInfoConverter.Input( - missingNetworks = proposalNetwork.missingRequired, - requiredNetworks = proposalNetwork.required, - availableNetworks = proposalNetwork.available, - notAddedNetworks = proposalNetwork.notAdded, - additionallyEnabledNetworks = additionallyEnabledNetworks, - ), - ), - connectButtonConfig = prevState.connectButtonConfig.copy( - enabled = WcConnectButtonAvailabilityConverter.convert( - WcConnectButtonAvailabilityConverter.Input( - missingNetworks = proposalNetwork.missingRequired, - requiredNetworks = proposalNetwork.required, - availableNetworks = proposalNetwork.available, - selectedNetworks = additionallyEnabledNetworks, - ), - ), - ), - ) - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt index f3991ee130..7ff2f97a29 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/model/transformers/WcSessionsAccountModeTransformer.kt @@ -49,7 +49,7 @@ internal class WcSessionsAccountModeTransformer( items.add(walletHeader) accountList.accounts.filterIsInstance().forEach accountsForEach@{ account -> - val accountSessions = sessions.filter { it.account?.accountId == account.accountId } + val accountSessions = sessions.filter { it.account.accountId == account.accountId } if (accountSessions.isEmpty()) return@accountsForEach val connectedApps = accountSessions.map { dappSession -> with(dappSession.sdkModel) { diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt index e2a866155b..63edee72e6 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/routes/WcAppInfoRoutes.kt @@ -3,7 +3,6 @@ package com.tangem.features.walletconnect.connections.routes import androidx.compose.runtime.Immutable import com.tangem.core.decompose.navigation.Route import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId import kotlinx.serialization.Serializable @Serializable @@ -15,9 +14,6 @@ internal sealed class WcAppInfoRoutes : Route { @Serializable data object PortfolioSelector : WcAppInfoRoutes() - @Serializable - data class SelectWallet(val selectedWalletId: UserWalletId) : WcAppInfoRoutes() - @Serializable data class SelectNetworks( val missingRequiredNetworks: Set, diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt index 9bfbb0ec01..ad8dad09c5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/connections/ui/WcAppInfoBS.kt @@ -22,8 +22,6 @@ import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.ContentScale 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.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -273,19 +271,7 @@ private fun WcAppInfoSecondBlock(state: WcAppInfoUM.Content, modifier: Modifier val itemsModifier = Modifier .fillMaxWidth() .padding(TangemTheme.dimens.spacing12) - if (state.portfolioSelectRow != null) { - PortfolioRowItem(portfolioSelectRow = state.portfolioSelectRow) - } else { - WalletRowItem( - modifier = if (state.onWalletClick != null) { - Modifier.clickableSingle(onClick = state.onWalletClick) - } else { - Modifier - }.then(itemsModifier), - walletName = state.walletName, - showEndIcon = state.onWalletClick != null, - ) - } + PortfolioRowItem(portfolioSelectRow = state.portfolioSelectRow) HorizontalDivider(thickness = 1.dp, color = TangemTheme.colors.stroke.primary) SelectNetworksBlock( modifier = Modifier @@ -341,56 +327,6 @@ private fun PortfolioRowItem(portfolioSelectRow: PortfolioSelectUM, modifier: Mo } } -@Composable -private fun WalletRowItem(walletName: String, showEndIcon: Boolean, modifier: Modifier = Modifier) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { - Icon( - modifier = Modifier - .size(24.dp) - .testTag(WalletConnectBottomSheetTestTags.WALLET_ICON), - painter = painterResource(R.drawable.ic_wallet_new_24), - contentDescription = null, - tint = TangemTheme.colors.icon.accent, - ) - Row( - modifier = Modifier.weight(1f), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing4) - .testTag(WalletConnectBottomSheetTestTags.WALLET_NAME_TITLE), - text = stringResourceSafe(R.string.manage_tokens_network_selector_wallet), - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.primary1, - maxLines = 1, - ) - Text( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing16) - .testTag(WalletConnectBottomSheetTestTags.WALLET_NAME), - text = walletName, - textAlign = TextAlign.End, - style = TangemTheme.typography.body1, - color = TangemTheme.colors.text.tertiary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - if (showEndIcon) { - Icon( - modifier = Modifier - .padding(start = TangemTheme.dimens.spacing12) - .size(width = 18.dp, height = 24.dp), - painter = painterResource(R.drawable.ic_select_18_24), - contentDescription = null, - tint = TangemTheme.colors.icon.informative, - ) - } - } -} - @Composable private fun SelectNetworksBlock(networksInfo: WcNetworksInfo, modifier: Modifier = Modifier) { Row( @@ -673,9 +609,7 @@ private class WcAppInfoStateProvider : CollectionPreviewParameterProvider Unit, -) { - - private val userWalletsFetcher = userWalletsFetcherFactory.create( - messageSender = messageSender, - onlyMultiCurrency = true, - isAuthMode = false, - isClickableIfLocked = false, - onWalletClick = { onWalletSelected(it) }, - ) - - @OptIn(ExperimentalCoroutinesApi::class) - val userWallets: Flow> = userWalletsFetcher.userWallets - .flatMapLatest { listOfWalletItem -> - val flows = listOfWalletItem.map(::getTokenListFlow) - combine(flows) { it.toList().toImmutableList() } - } - - private fun getTokenListFlow(walletItem: UserWalletItemUM): Flow { - return singleAccountStatusListSupplier(userWalletId = UserWalletId(walletItem.id)).map { accountStatusList -> - val information = when (accountStatusList.totalFiatBalance) { - is TotalFiatBalance.Failed -> UserWalletItemUM.Information.Failed - is TotalFiatBalance.Loading -> UserWalletItemUM.Information.Loading - is TotalFiatBalance.Loaded -> tokenCountInfo(accountStatusList.flattenCurrencies().size) - } - - walletItem.copy(information = information) - } - } - - private fun tokenCountInfo(count: Int): UserWalletItemUM.Information.Loaded { - val text = TextReference.PluralRes( - id = R.plurals.card_label_token_count, - count = count, - formatArgs = wrappedList(count), - ) - return UserWalletItemUM.Information.Loaded(text) - } -} \ No newline at end of file diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt index f40742eb25..620599339d 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/di/WalletConnectModelModule.kt @@ -28,11 +28,6 @@ internal interface WalletConnectModelModule { @ClassKey(WcPairModel::class) fun bindWcPairModel(model: WcPairModel): Model - @Binds - @IntoMap - @ClassKey(WcSelectWalletModel::class) - fun bindWcSelectWalletModel(model: WcSelectWalletModel): Model - @Binds @IntoMap @ClassKey(WcSelectNetworksModel::class) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt index 11fba2839f..78f0d51df5 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcAddNetworkModel.kt @@ -137,7 +137,7 @@ internal class WcAddNetworkModel @Inject constructor( network = useCase.network, emulationStatus = null, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ), ) 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 a156c5a69c..493b5158d4 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 @@ -426,7 +426,7 @@ internal class WcSendTransactionModel @Inject constructor( rawRequest = useCase.rawSdkRequest, network = useCase.network, securityStatus = securityStatusState.value.toCheckDAppResult(), - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ) analytics.send(event) showSuccessSignMessage() @@ -466,7 +466,7 @@ internal class WcSendTransactionModel @Inject constructor( network = useCase.network, emulationStatus = emulationStatus, securityStatus = securityCheck.toCheckDAppResult(), - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ), ) diff --git a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt index 7a0a21fe7f..2f36707a60 100644 --- a/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt +++ b/features/walletconnect/impl/src/main/kotlin/com/tangem/features/walletconnect/transaction/model/WcSignTransactionModel.kt @@ -143,7 +143,7 @@ internal class WcSignTransactionModel @Inject constructor( rawRequest = useCase.rawSdkRequest, network = useCase.network, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ) analytics.send(event) showSuccessSignMessage() @@ -172,7 +172,7 @@ internal class WcSignTransactionModel @Inject constructor( network = useCase.network, emulationStatus = null, securityStatus = CheckDAppResult.FAILED_TO_VERIFY, - accountDerivation = useCase.session.account?.derivationIndex?.value, + accountDerivation = useCase.session.account.derivationIndex.value, ), )