From 8ae1c2294f56f9237db1691828c4f3e73bf3dcee Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 12 Feb 2026 21:10:21 +0100 Subject: [PATCH 01/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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/69] 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 31e0591ef08c4872d9c2e5556742942eefb401b6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 27 Feb 2026 17:52:48 +0500 Subject: [PATCH 68/69] 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 69/69] 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