From a9fbfc8a5301ef2e64505b2a7b54b817272d89e4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Apr 2026 13:34:48 +0400 Subject: [PATCH 01/32] Updated on 2026-08-14 --- ...ccountCryptoPortfolioItemStateConverter.kt | 45 +++- .../tokenlist/state/TokensListItemUM.kt | 6 + .../wallets/usecase/GetWalletsUseCase.kt | 12 +- .../converter/ChooseTokenListItemConverter.kt | 37 +-- .../impl/model/ChooseTokenModel.kt | 54 ++--- .../choosetoken/impl/ui/ChooseTokenScreen.kt | 215 ++++++++++++++---- .../swap/choosetoken/impl/ui/ChooseTokenUM.kt | 10 +- 7 files changed, 272 insertions(+), 107 deletions(-) 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 1955e4b9a0..9780c3831c 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 @@ -27,6 +27,8 @@ class AccountCryptoPortfolioItemStateConverter( private val priceChangeLce: Lce? = null, private val onItemClick: ((Account.CryptoPortfolio) -> Unit)? = null, private val onItemLongClick: ((Account.CryptoPortfolio) -> Unit)? = null, + private val fiatAmountStateProvider: ((TotalFiatBalance) -> FiatAmountState?)? = null, + private val subtitle2StateProvider: ((Lce) -> Subtitle2State?)? = null, ) : Converter { override fun convert(value: TotalFiatBalance): TokenItemState { @@ -40,11 +42,11 @@ class AccountCryptoPortfolioItemStateConverter( private fun Account.CryptoPortfolio.mapToContentState( fiatBalance: TotalFiatBalance.Loaded, ): TokenItemState.Content { - val subtitle2State = priceChangeLce?.fold( - ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading }, - ifError = { null }, - ifContent = { priceChange -> priceChange.toSubtitle2State() }, - ) + val subtitle2State = if (priceChangeLce != null && subtitle2StateProvider != null) { + subtitle2StateProvider(priceChangeLce) + } else { + createSubtitle2State(priceChangeLce) + } return TokenItemState.Content( id = account.accountId.toItemId(), iconState = AccountIconItemStateConverter().convert(this), @@ -59,11 +61,8 @@ class AccountCryptoPortfolioItemStateConverter( ), isAvailable = false, ), - fiatAmountState = FiatAmountState.Content( - text = fiatBalance.amount - .format { fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) }, - isFlickering = fiatBalance.source == StatusSource.CACHE, - ), + fiatAmountState = fiatAmountStateProvider?.invoke(fiatBalance) + ?: createFiatAmountState(fiatBalance, appCurrency), subtitle2State = subtitle2State, onItemClick = onItemClick?.let { onItemClick -> { onItemClick(account) } }, onItemLongClick = onItemLongClick?.let { onItemLongClick -> { onItemLongClick(account) } }, @@ -127,4 +126,30 @@ class AccountCryptoPortfolioItemStateConverter( type = this.value.getPriceChangeType(), isFlickering = this.source.isFlickering(), ) + + private fun createFiatAmountState(fiatBalance: TotalFiatBalance, appCurrency: AppCurrency): FiatAmountState { + return when (fiatBalance) { + TotalFiatBalance.Failed, + TotalFiatBalance.Loading, + -> FiatAmountState.Empty + + is TotalFiatBalance.Loaded -> FiatAmountState.Content( + text = fiatBalance.amount.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + isFlickering = fiatBalance.source == StatusSource.CACHE, + ) + } + } + + private fun createSubtitle2State(priceChangeLce: Lce?): Subtitle2State? { + return priceChangeLce?.fold( + ifLoading = { priceChange -> priceChange?.toSubtitle2State() ?: Subtitle2State.Loading }, + ifError = { null }, + ifContent = { priceChange -> priceChange.toSubtitle2State() }, + ) + } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt index c09e64697b..f5c2277c35 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/state/TokensListItemUM.kt @@ -55,6 +55,12 @@ sealed interface TokensListItemUM { is PortfolioItemContentUM.Tokens -> content.tokens is PortfolioItemContentUM.Empty -> persistentListOf() } + + val tokensItemsList: List + get() = when (content) { + is PortfolioItemContentUM.Tokens -> content.tokens.filterIsInstance() + is PortfolioItemContentUM.Empty -> emptyList() + } } data class Text(override val id: Any, val text: TextReference) : TokensListItemUM diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt index 1a436c9f93..355ce124c1 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetWalletsUseCase.kt @@ -3,9 +3,9 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.isMultiCurrency import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import java.util.LinkedHashMap /** * Use case for getting list of user wallets @@ -22,9 +22,13 @@ class GetWalletsUseCase( operator fun invoke(): Flow> = userWalletsListRepository.userWallets.map { requireNotNull(it) } @Throws(IllegalArgumentException::class) - fun invokeAsMap(): Flow> = userWalletsListRepository.userWallets - .map { requireNotNull(it) } - .map { wallets -> + fun invokeAsMap(isOnlyMultiCurrency: Boolean = true): Flow> = invoke() + .map { list -> + val wallets = if (isOnlyMultiCurrency) { + list.filter { wallet -> wallet.isMultiCurrency } + } else { + list + } wallets.associateByTo( destination = linkedMapOf(), keySelector = { wallet -> wallet.walletId }, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt index a130a0eabf..ee77c4eeec 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/converter/ChooseTokenListItemConverter.kt @@ -6,9 +6,12 @@ import com.tangem.common.ui.tokens.TokenConverterParams import com.tangem.common.ui.tokens.TokenItemGrouping.toGroupedItems import com.tangem.common.ui.tokens.TokenItemGrouping.toUngroupedItems import com.tangem.common.ui.tokens.TokenItemStateConverter +import com.tangem.core.ui.components.icons.IconTint +import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.filterCryptoPortfolio @@ -17,6 +20,7 @@ import com.tangem.domain.models.tokenlist.TokenList import com.tangem.feature.swap.choosetoken.impl.model.ClickIntents import com.tangem.feature.swap.choosetoken.impl.model.isSearchingState import com.tangem.feature.swap.models.TokenListUMData +import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.toPersistentList internal class ChooseTokenListItemConverter( @@ -57,10 +61,9 @@ internal class ChooseTokenListItemConverter( if (accountItems.isEmpty()) { return TokenListUMData.EmptyList } - val accountsList = accountItems.toPersistentList() return TokenListUMData.AccountList( - tokensList = accountsList, - totalTokensCount = accountsList.size, + tokensList = accountItems.toPersistentList(), + totalTokensCount = accountItems.sumOf { portfolio -> portfolio.tokensItemsList.size }, ) } @@ -77,11 +80,19 @@ internal class ChooseTokenListItemConverter( clickIntents.onAccountExpandClick(clickedAccount) } } + val fiatAmountStateProvider: ((TotalFiatBalance) -> FiatAmountState?) = if (isSearchingState) { + { _ -> FiatAmountState.Empty } + } else { + { _ -> FiatAmountState.Icon(R.drawable.ic_chewron_down_20, IconTint.Informative) } + } + val converter = AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, account = account, onItemClick = onItemClick.takeIf { !isSearchingState }, priceChangeLce = this.priceChangeLce, + fiatAmountStateProvider = fiatAmountStateProvider, + subtitle2StateProvider = { _ -> null }, ) val accountItem = converter.convert(tokenList.totalFiatBalance) val tokenConverter = tokenStatusConverter(this) @@ -100,18 +111,14 @@ internal class ChooseTokenListItemConverter( return when (tokenList) { is TokenList.Empty -> TokenListUMData.EmptyList - is TokenList.GroupedByNetwork -> tokenList.toGroupedItems(tokenConverter).let { grouped -> - TokenListUMData.TokenList( - tokensList = grouped.toPersistentList(), - totalTokensCount = grouped.size, - ) - } - is TokenList.Ungrouped -> tokenList.toUngroupedItems(tokenConverter).let { ungrouped -> - TokenListUMData.TokenList( - tokensList = ungrouped.toPersistentList(), - totalTokensCount = ungrouped.size, - ) - } + is TokenList.GroupedByNetwork -> TokenListUMData.TokenList( + tokensList = tokenList.toGroupedItems(tokenConverter).toPersistentList(), + totalTokensCount = tokenList.flattenCurrencies().size, + ) + is TokenList.Ungrouped -> TokenListUMData.TokenList( + tokensList = tokenList.toUngroupedItems(tokenConverter).toPersistentList(), + totalTokensCount = tokenList.flattenCurrencies().size, + ) } } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt index 2eb003236d..ca32af6c5e 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -5,8 +5,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.ui.components.fields.entity.SearchBarUM -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.core.ui.extensions.stringReference import com.tangem.domain.models.account.AccountId @@ -19,13 +18,9 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.choosetoken.api.* import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarToggleTransformer import com.tangem.feature.swap.choosetoken.impl.converter.SearchBarUpdateQueryTransformer -import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenFullUM -import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenInitialUM -import com.tangem.feature.swap.choosetoken.impl.ui.ChooseTokenUM -import com.tangem.feature.swap.choosetoken.impl.ui.WalletListUM +import com.tangem.feature.swap.choosetoken.impl.ui.* import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.models.SwapSelectTokenStateHolder -import com.tangem.feature.swap.models.TokenListUMData import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.presentation.R import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent @@ -147,41 +142,39 @@ internal class ChooseTokenModel @Inject constructor( @Suppress("LongMethod") private fun combineUI(): StateFlow = channelFlow { - val allWalletsFlow: StateFlow> = + val allWalletsFlow: StateFlow> = getWalletsUseCase.invokeAsMap().stateIn(this) + // todo swap add optional param, store, and GetSelectedWalletUseCase + val firstSelectedWallet = allWalletsFlow.value.values.first() val selectedWalletFlow: StateFlow = onWalletSelected.receiveAsFlow() .mapNotNull { walletId -> allWalletsFlow.value[walletId] } - .stateIn(this, SharingStarted.Eagerly, allWalletsFlow.value.values.first()) + .stateIn(this, SharingStarted.Eagerly, firstSelectedWallet) - val selectedWalletTokensData: Flow = combine( - flow = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(), + val fullPortfolioBlockFlow = combine( + flow = allWalletsFlow, flow2 = portfolioListBlockDelegate.portfolioList, - transform = { selectedWalletId, allPortfoliosData -> allPortfoliosData[selectedWalletId] }, - ) - .filterNotNull() - .distinctUntilChanged() - - val walletListUmFlow = combine( - flow = selectedWalletFlow, - flow2 = allWalletsFlow, - transform = { selectedWallet, allWallets -> - allWallets.entries + flow3 = selectedWalletFlow.map { wallet -> wallet.walletId }.distinctUntilChanged(), + transform = { allWallets, portfolioList, selectedWalletId -> + val tokensListData = portfolioList[selectedWalletId] ?: return@combine null + val walletsUM = allWallets.entries .map { (walletId, wallet) -> - val type = if (selectedWallet.walletId == walletId) { - TangemButtonType.Primary - } else { - TangemButtonType.Secondary - } - TangemButtonUM( + val searchResultCount: TextReference? = portfolioList[walletId]?.totalTokensCount + ?.toString() + ?.let(::stringReference) + ?.takeIf { isSearchingState } + WalletTabUM( text = stringReference(wallet.name), onClick = { onWalletSelected.trySend(walletId) }, - type = type, + isSelected = selectedWalletId == walletId, + count = searchResultCount, ) } + walletsUM to tokensListData }, ) + .filterNotNull() .distinctUntilChanged() portfolioListBlockDelegate.onTokenItemClick.receiveAsFlow() @@ -195,11 +188,10 @@ internal class ChooseTokenModel @Inject constructor( .launchIn(this) combine( - flow = selectedWalletTokensData, + flow = fullPortfolioBlockFlow, flow2 = settingContextUseCase.invoke(), flow3 = marketsStateFlow, - flow4 = walletListUmFlow, - transform = { tokensData, settings, marketsData, walletList -> + transform = { (walletList, tokensData), settings, marketsData -> val walletsUM = if (walletList.size != 1) { WalletListUM(walletList.toPersistentList()) } else { diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt index 626a24fe4e..a02d3a52c2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/ui/ChooseTokenScreen.kt @@ -1,15 +1,23 @@ package com.tangem.feature.swap.choosetoken.impl.ui +import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider @@ -17,10 +25,6 @@ import androidx.compose.ui.unit.dp import com.tangem.common.ui.tokens.portfolioTokensList import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.appbar.AppBarWithBackButton -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.CurrencyIconState import com.tangem.core.ui.components.fields.SearchBar import com.tangem.core.ui.components.fields.TangemSearchBarDefaults @@ -34,8 +38,6 @@ import com.tangem.core.ui.components.tokenlist.state.PortfolioItemContentUM import com.tangem.core.ui.components.tokenlist.state.PortfolioTokensListItemUM import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.decorations.roundedShapeItemDecoration -import com.tangem.core.ui.ds.button.TangemButtonType -import com.tangem.core.ui.ds.button.TangemButtonUM import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview @@ -54,6 +56,18 @@ import kotlin.random.Random private const val LOAD_MORE_BUFFER = 25 +private val ChooseTokenUM.isNotFoundState: Boolean + get() = tokensListData.tokensList.isEmpty() && + isSearching && + marketsState !is SwapMarketState.Content && + marketsState !is SwapMarketState.Loading + +private val ChooseTokenUM.isEmptyState: Boolean + get() = tokensListData.tokensList.isEmpty() && + !isSearching && + marketsState !is SwapMarketState.Content && + marketsState !is SwapMarketState.Loading + @Composable internal fun ChooseTokenScreen(state: ChooseTokenFullUM, modifier: Modifier = Modifier) { Column( @@ -93,7 +107,6 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) { modifier = modifier .fillMaxSize() .nestedScroll(nestedScrollConnection), - horizontalAlignment = Alignment.CenterHorizontally, state = lazyListState, contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) { @@ -108,20 +121,26 @@ private fun Content(state: ChooseTokenFullUM, modifier: Modifier = Modifier) { assetsTitle() if (state.contentUM != null) { - walletListItem(state.contentUM.walletList) + when { + state.contentUM.isNotFoundState -> tokensNotFound() + state.contentUM.isEmptyState -> emptyTokensList() + else -> { + walletListItem(state.contentUM.walletList) - tokensListItems( - tokensListData = state.contentUM.tokensListData, - isBalanceHidden = state.contentUM.isBalanceHidden, - ) + tokensListItems( + tokensListData = state.contentUM.tokensListData, + isBalanceHidden = state.contentUM.isBalanceHidden, + ) - if (state.contentUM.marketsState != null) { - item("markets_title_spacer") { SpacerH(height = 20.dp) } - swapMarketsListItems(state.contentUM.marketsState) + if (state.contentUM.marketsState != null) { + item("markets_title_spacer") { SpacerH(height = 20.dp) } + swapMarketsListItems(state.contentUM.marketsState) + } + } } } } - if (state.contentUM?.marketsState != null) { + if (state.contentUM?.marketsState != null && !state.contentUM.isNotFoundState && !state.contentUM.isEmptyState) { SetupMarketScrollTracker(state.contentUM.marketsState, lazyListState) } } @@ -183,26 +202,59 @@ private fun LazyListScope.assetsTitle() { private fun LazyListScope.walletListItem(walletList: WalletListUM) { if (walletList.items.isEmpty()) return item("wallet_list") { - Row( - modifier = Modifier - .padding(horizontal = TangemTheme.dimens.spacing16) - .fillMaxWidth(), + LazyRow( + modifier = Modifier.padding(top = 12.dp, bottom = 4.dp), horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8), - verticalAlignment = Alignment.CenterVertically, + contentPadding = PaddingValues(horizontal = 16.dp), ) { - walletList.items.forEach { um -> - val colors = when (um.type) { - TangemButtonType.Primary -> TangemButtonsDefaults.primaryButtonColors - else -> TangemButtonsDefaults.secondaryButtonColors - } - TangemButton( - text = um.text?.resolveReference().orEmpty(), - icon = TangemButtonIconPosition.None, - size = TangemButtonSize.Action, - colors = colors, - showProgress = false, - onClick = um.onClick, - enabled = true, + items(walletList.items) { um -> + WalletTabItem(um) + } + } + } +} + +@Composable +private fun WalletTabItem(state: WalletTabUM, modifier: Modifier = Modifier) { + val isSelected = state.isSelected + val backgroundColor = if (isSelected) TangemTheme.colors.button.primary else TangemTheme.colors.button.secondary + val buttonTextColor = if (isSelected) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.primary1 + val countTextColor = if (isSelected) TangemTheme.colors.text.primary2 else TangemTheme.colors.text.secondary + val countBackground = if (isSelected) { + TangemTheme.colors.button.secondary.copy(alpha = 0.2f) + } else { + TangemTheme.colors.button.primary.copy(alpha = 0.1f) + } + + Row( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .background(backgroundColor) + .clickable(onClick = state.onClick) + .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.text.resolveReference(), + color = buttonTextColor, + style = TangemTheme.typography.button, + ) + + if (state.count != null) { + Spacer(modifier = Modifier.width(8.dp)) + + Box( + modifier = Modifier + .background(countBackground, shape = CircleShape) + .defaultMinSize(minWidth = 20.dp) + .padding(horizontal = 4.dp, vertical = 2.dp), + contentAlignment = Alignment.Center, + ) { + Text( + text = state.count.resolveReference(), + color = countTextColor, + style = TangemTheme.typography.caption1, ) } } @@ -253,6 +305,58 @@ private fun LazyListScope.tokensList(items: ImmutableList, isB ) } +private fun LazyListScope.emptyTokensList(modifier: Modifier = Modifier) { + item("EmptyTokensList") { + Box( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .fillParentMaxSize(), + ) { + Column(modifier = Modifier.align(Alignment.Center)) { + Image( + modifier = Modifier + .size(TangemTheme.dimens.size64) + .align(Alignment.CenterHorizontally), + painter = painterResource(id = R.drawable.ic_no_token_44), + colorFilter = ColorFilter.tint(TangemTheme.colors.icon.inactive), + contentDescription = null, + ) + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16) + .padding(horizontal = TangemTheme.dimens.spacing30) + .align(Alignment.CenterHorizontally), + text = stringResourceSafe(id = R.string.exchange_tokens_empty_tokens), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + } + } + } +} + +private fun LazyListScope.tokensNotFound(modifier: Modifier = Modifier) { + item("TokensNotFound") { + Box( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .fillParentMaxSize(), + ) { + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32) + .padding(horizontal = TangemTheme.dimens.spacing30) + .align(Alignment.TopCenter), + text = stringResourceSafe(id = R.string.express_token_list_empty_search), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + } + } +} + @Preview @Composable private fun TokenScreenPreview(@PreviewParameter(ChooseTokenScreenPreviewProvider::class) state: ChooseTokenFullUM) { @@ -321,31 +425,42 @@ private val accounts private val wallets get() = persistentListOf( - TangemButtonUM( + WalletTabUM( text = TextReference.Str(value = "Wallet 1"), - type = TangemButtonType.Primary, + isSelected = true, onClick = {}, + count = null, ), - TangemButtonUM( + WalletTabUM( + text = TextReference.Str(value = "Wallet 1"), + isSelected = true, + onClick = {}, + count = stringReference("3"), + ), + WalletTabUM( text = TextReference.Str(value = "Wallet 2"), - type = TangemButtonType.Secondary, + isSelected = false, onClick = {}, + count = stringReference("333"), ), - TangemButtonUM( + WalletTabUM( text = TextReference.Str(value = "Wallet 3"), - type = TangemButtonType.Secondary, + isSelected = false, onClick = {}, + count = null, ), ) +private val initialUM = ChooseTokenInitialUM( + screenTitle = stringReference("Choose token"), + onCloseClick = {}, + searchBar = searchBar, +) + private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( ChooseTokenFullUM( - initialUM = ChooseTokenInitialUM( - screenTitle = stringReference("Choose token"), - onCloseClick = {}, - searchBar = searchBar, - ), + initialUM = initialUM, contentUM = ChooseTokenUM( walletList = WalletListUM(wallets), isBalanceHidden = false, @@ -357,5 +472,15 @@ private class ChooseTokenScreenPreviewProvider : PreviewParameterProvider, + val items: ImmutableList, +) + +internal data class WalletTabUM( + val text: TextReference, + val count: TextReference?, + val isSelected: Boolean, + val onClick: () -> Unit, ) \ No newline at end of file From 52a607c1f5f4e479be9bb9dd65bc78d18f1eb540 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 2 Apr 2026 19:19:32 +0400 Subject: [PATCH 02/32] Updated on 2026-08-14 --- .../tap/di/domain/StakingDomainModule.kt | 11 +++- .../staking/DefaultP2PEthPoolRepository.kt | 3 +- .../data/staking/DefaultStakingRepository.kt | 38 +++--------- .../data/staking/di/StakingDataModule.kt | 2 +- .../toggles/DefaultStakingFeatureToggles.kt | 27 +++++++- .../DefaultStakingFeatureTogglesTest.kt | 61 +++++++++++++++++++ .../tangem/domain/staking/StakingIdFactory.kt | 8 ++- .../staking/toggles/StakingFeatureToggles.kt | 5 +- .../domain/staking/StakingIdFactoryTest.kt | 38 +++++++++++- 9 files changed, 153 insertions(+), 40 deletions(-) create mode 100644 data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt index a823079f33..5633c8744b 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/StakingDomainModule.kt @@ -2,6 +2,7 @@ package com.tangem.tap.di.domain import com.tangem.domain.staking.* import com.tangem.domain.staking.repositories.* +import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.staking.single.SingleStakingBalanceFetcher import com.tangem.domain.staking.usecase.StakingAvailabilityListUseCase import com.tangem.domain.walletmanager.WalletManagersFacade @@ -226,8 +227,14 @@ internal object StakingDomainModule { @Provides @Singleton - fun provideStakingIdFactory(walletManagersFacade: WalletManagersFacade): StakingIdFactory { - return StakingIdFactory(walletManagersFacade = walletManagersFacade) + fun provideStakingIdFactory( + walletManagersFacade: WalletManagersFacade, + stakingFeatureToggles: StakingFeatureToggles, + ): StakingIdFactory { + return StakingIdFactory( + walletManagersFacade = walletManagersFacade, + stakingFeatureToggles = stakingFeatureToggles, + ) } @Provides diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt index b189675a38..1403dcdaf9 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultP2PEthPoolRepository.kt @@ -22,6 +22,7 @@ import com.tangem.domain.staking.model.ethpool.P2PEthPoolUnsignedTx import com.tangem.domain.staking.model.ethpool.P2PEthPoolVault import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.repositories.P2PEthPoolRepository +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger @@ -65,7 +66,7 @@ internal class DefaultP2PEthPoolRepository( } override suspend fun fetchVaults(network: P2PEthPoolNetwork) { - val vaults = if (stakingFeatureToggles.isEthStakingEnabled) { + val vaults = if (stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)) { getVaults(network).getOrElse { error -> TangemLogger.e("Error fetching P2PEthPool vaults: $error") emptyList() 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 b46d6ea594..f98c982fa6 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 @@ -1,8 +1,6 @@ package com.tangem.data.staking import arrow.core.getOrElse -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.domain.card.common.TapWorkarounds.isWallet2 import com.tangem.domain.models.currency.CryptoCurrency @@ -22,14 +20,13 @@ import com.tangem.lib.crypto.BlockchainUtils.isSolana import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.flowOf + import kotlinx.coroutines.withContext -@Suppress("LargeClass", "LongParameterList", "TooManyFunctions") internal class DefaultStakingRepository( private val stakeKitRepository: StakeKitRepository, private val p2pEthPoolRepository: P2PEthPoolRepository, - private val stakingBalanceStoreV2: StakeKitBalancesStore, + private val stakeKitBalancesStore: StakeKitBalancesStore, private val dispatchers: CoroutineDispatcherProvider, private val getUserWalletUseCase: GetUserWalletUseCase, private val stakingFeatureToggles: StakingFeatureToggles, @@ -40,7 +37,8 @@ internal class DefaultStakingRepository( cryptoCurrency: CryptoCurrency, ): Flow { return channelFlow { - if (!checkFeatureToggleEnabled(cryptoCurrency)) { + val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id) + if (stakingIntegration == null || !stakingFeatureToggles.isIntegrationEnabled(stakingIntegration)) { send(StakingAvailability.Unavailable) return@channelFlow } @@ -56,8 +54,6 @@ internal class DefaultStakingRepository( return@channelFlow } - val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id) - val availabilityFlow = when (stakingIntegration) { StakingIntegrationID.P2PEthPool -> p2pEthPoolRepository.getStakingAvailability() is StakingIntegrationID.StakeKit -> stakeKitRepository.getStakingAvailability( @@ -65,7 +61,6 @@ internal class DefaultStakingRepository( rawCurrencyId, cryptoCurrency.symbol, ) - null -> flowOf(StakingAvailability.Unavailable) } availabilityFlow.collect { send(it) } @@ -76,20 +71,15 @@ internal class DefaultStakingRepository( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): StakingAvailability { - if (!checkFeatureToggleEnabled(cryptoCurrency)) { - return StakingAvailability.Unavailable - } + val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id) + ?.takeIf(stakingFeatureToggles::isIntegrationEnabled) + ?: return StakingAvailability.Unavailable if (checkForInvalidCardBatch(userWalletId, cryptoCurrency)) { return StakingAvailability.Unavailable } val rawCurrencyId = cryptoCurrency.id.rawCurrencyId - if (rawCurrencyId == null) { - return StakingAvailability.Unavailable - } - - val stakingIntegration = StakingIntegrationID.create(currencyId = cryptoCurrency.id) ?: return StakingAvailability.Unavailable return when (stakingIntegration) { @@ -104,7 +94,7 @@ internal class DefaultStakingRepository( override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { return withContext(dispatchers.default) { - val balances = stakingBalanceStoreV2.getAllSyncOrNull(userWalletId) ?: return@withContext false + val balances = stakeKitBalancesStore.getAllSyncOrNull(userWalletId) ?: return@withContext false val hasDataStakingBalance by lazy { balances.any { stakingBalance -> @@ -116,18 +106,6 @@ internal class DefaultStakingRepository( } } - private fun checkFeatureToggleEnabled(cryptoCurrency: CryptoCurrency): Boolean { - return when (cryptoCurrency.network.id.toBlockchain()) { - Blockchain.Ethereum -> { - when (cryptoCurrency) { - is CryptoCurrency.Coin -> stakingFeatureToggles.isEthStakingEnabled - is CryptoCurrency.Token -> true - } - } - else -> true - } - } - private fun checkForInvalidCardBatch(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { val userWallet = getUserWalletUseCase(userWalletId).getOrElse { error("Failed to get user wallet") 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 0aa8abf1b5..a997cbce26 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 @@ -65,7 +65,7 @@ internal object StakingDataModule { return DefaultStakingRepository( stakeKitRepository = stakeKitRepository, p2pEthPoolRepository = p2pEthPoolRepository, - stakingBalanceStoreV2 = stakeKitBalancesStore, + stakeKitBalancesStore = stakeKitBalancesStore, dispatchers = dispatchers, getUserWalletUseCase = getUserWalletUseCase, 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 011cc73bf9..6f62c8bc06 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 @@ -2,12 +2,35 @@ package com.tangem.data.staking.toggles import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.toggles.StakingFeatureToggles internal class DefaultStakingFeatureToggles( private val featureTogglesManager: FeatureTogglesManager, ) : StakingFeatureToggles { - override val isEthStakingEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) + override fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean { + val toggle = integrationId.getFeatureToggle() ?: return true + return featureTogglesManager.isFeatureEnabled(toggle) + } + + private fun StakingIntegrationID.getFeatureToggle(): FeatureToggles? = when (this) { + is StakingIntegrationID.P2PEthPool -> FeatureToggles.STAKING_ETH_ENABLED + is StakingIntegrationID.StakeKit -> this.getStakeKitFeatureToggle() + } + + private fun StakingIntegrationID.StakeKit.getStakeKitFeatureToggle(): FeatureToggles? = when (this) { + is StakingIntegrationID.StakeKit.Coin -> when (this) { + StakingIntegrationID.StakeKit.Coin.Ton, + StakingIntegrationID.StakeKit.Coin.Solana, + StakingIntegrationID.StakeKit.Coin.Cosmos, + StakingIntegrationID.StakeKit.Coin.Tron, + StakingIntegrationID.StakeKit.Coin.BSC, + StakingIntegrationID.StakeKit.Coin.Cardano, + -> null + } + is StakingIntegrationID.StakeKit.EthereumToken -> when (this) { + StakingIntegrationID.StakeKit.EthereumToken.Polygon -> null + } + } } \ No newline at end of file diff --git a/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt b/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt new file mode 100644 index 0000000000..ca6766c8c8 --- /dev/null +++ b/data/staking/src/test/kotlin/com/tangem/data/staking/toggles/DefaultStakingFeatureTogglesTest.kt @@ -0,0 +1,61 @@ +package com.tangem.data.staking.toggles + +import com.tangem.core.configtoggle.FeatureToggles +import com.tangem.core.configtoggle.feature.FeatureTogglesManager +import com.tangem.domain.staking.model.StakingIntegrationID +import com.google.common.truth.Truth.assertThat +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) +internal class DefaultStakingFeatureTogglesTest { + + private val featureTogglesManager: FeatureTogglesManager = mockk() + private val toggles = DefaultStakingFeatureToggles(featureTogglesManager = featureTogglesManager) + + @BeforeEach + fun resetMocks() { + clearMocks(featureTogglesManager) + } + + @Test + fun `P2PEthPool returns true when STAKING_ETH_ENABLED is enabled`() { + every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns true + + assertThat(toggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)).isTrue() + + verify(exactly = 1) { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } + } + + @Test + fun `P2PEthPool returns false when STAKING_ETH_ENABLED is disabled`() { + every { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } returns false + + assertThat(toggles.isIntegrationEnabled(StakingIntegrationID.P2PEthPool)).isFalse() + + verify(exactly = 1) { featureTogglesManager.isFeatureEnabled(FeatureToggles.STAKING_ETH_ENABLED) } + } + + @Test + fun `existing StakeKit Coin integrations are always enabled`() { + StakingIntegrationID.StakeKit.Coin.entries.forEach { coin -> + assertThat(toggles.isIntegrationEnabled(coin)).isTrue() + } + + verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) } + } + + @Test + fun `existing StakeKit EthereumToken integrations are always enabled`() { + StakingIntegrationID.StakeKit.EthereumToken.entries.forEach { token -> + assertThat(toggles.isIntegrationEnabled(token)).isTrue() + } + + verify(exactly = 0) { featureTogglesManager.isFeatureEnabled(any()) } + } +} \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt index c8648ad64c..6fb6297f3f 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/StakingIdFactory.kt @@ -2,23 +2,27 @@ package com.tangem.domain.staking import arrow.core.Either import arrow.core.raise.either +import arrow.core.raise.ensure import arrow.core.raise.ensureNotNull 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.models.staking.StakingID import com.tangem.domain.staking.model.StakingIntegrationID +import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.walletmanager.WalletManagersFacade /** * Factory class for creating instances of [StakingID] * - * @property walletManagersFacade wallet manager facade + * @property walletManagersFacade wallet manager facade + * @property stakingFeatureToggles staking feature toggles * [REDACTED_AUTHOR] */ class StakingIdFactory( private val walletManagersFacade: WalletManagersFacade, + private val stakingFeatureToggles: StakingFeatureToggles, ) { /** @@ -72,6 +76,8 @@ class StakingIdFactory( ensureNotNull(integrationId) { Error.UnsupportedCurrency } + ensure(stakingFeatureToggles.isIntegrationEnabled(integrationId)) { Error.UnsupportedCurrency } + val address = defaultAddressProvider().takeUnless { it.isNullOrEmpty() } ensureNotNull(address) { Error.UnableToGetAddress(integrationId = integrationId) } 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 3553692065..80761562fc 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,5 +1,8 @@ package com.tangem.domain.staking.toggles +import com.tangem.domain.staking.model.StakingIntegrationID + interface StakingFeatureToggles { - val isEthStakingEnabled: Boolean + + fun isIntegrationEnabled(integrationId: StakingIntegrationID): Boolean } \ No newline at end of file diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt index 71a5d271ed..3211cdff64 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt @@ -10,11 +10,13 @@ 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.staking.model.StakingIntegrationID +import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.test.core.ProvideTestModels import io.mockk.clearMocks import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach @@ -30,11 +32,16 @@ import org.junit.jupiter.params.ParameterizedTest internal class StakingIdFactoryTest { private val walletManagersFacade: WalletManagersFacade = mockk() - private val factory = StakingIdFactory(walletManagersFacade = walletManagersFacade) + private val stakingFeatureToggles: StakingFeatureToggles = mockk() + private val factory = StakingIdFactory( + walletManagersFacade = walletManagersFacade, + stakingFeatureToggles = stakingFeatureToggles, + ) @BeforeEach fun resetMocks() { - clearMocks(walletManagersFacade) + clearMocks(walletManagersFacade, stakingFeatureToggles) + every { stakingFeatureToggles.isIntegrationEnabled(any()) } returns true } @Nested @@ -66,6 +73,33 @@ internal class StakingIdFactoryTest { } } + @Test + fun `create returns UnsupportedCurrency if integration is disabled by toggle`() = runTest { + // Arrange + val userWalletId = UserWalletId(stringValue = "011") + val currency = MockCryptoCurrencyFactory().createCoin(Blockchain.TON) + + every { + stakingFeatureToggles.isIntegrationEnabled(StakingIntegrationID.StakeKit.Coin.Ton) + } returns false + + // Act + val actual = factory.create( + userWalletId = userWalletId, + currencyId = currency.id, + network = currency.network, + ) + + // Assert + val expected = StakingIdFactory.Error.UnsupportedCurrency + + Truth.assertThat(actual.leftOrNull()).isEqualTo(expected) + + coVerify(inverse = true) { + walletManagersFacade.getDefaultAddress(userWalletId = any(), network = any()) + } + } + @Test fun `create returns UnableToGetAddress if address is null`() = runTest { // Arrange From 9944036c4d2a6eae5d1dd986f87d3bcc8a1a9e87 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Apr 2026 14:09:57 +0300 Subject: [PATCH 03/32] Updated on 2026-08-14 --- .../di/domain/DynamicAddressesDomainModule.kt | 78 ++++++++ .../tap/domain/twins/FinalizeTwinTask.kt | 2 +- ...dynamic_addresses_bottomsheet_check_24.xml | 10 + ...namic_addresses_bottomsheet_enable_top.xml | 18 ++ ...dresses_bottomsheet_enable_unavailable.xml | 19 ++ ...dynamic_addresses_bottomsheet_flash_24.xml | 9 + data/dynamic-addresses/build.gradle.kts | 1 + .../DefaultDynamicAddressesRepository.kt | 69 ++++++- .../DefaultDerivationsRepository.kt | 18 ++ domain/dynamic-addresses/build.gradle.kts | 4 + .../DynamicAddressesSupportedBlockchains.kt | 53 ++++++ .../EnableDynamicAddressesError.kt | 8 + .../EnableDynamicAddressesUseCase.kt | 17 +- .../dynamicaddresses/IsXpubDerivedUseCase.kt | 37 ++++ .../IsXpubSupportedUseCase.kt | 22 +++ .../repository/DynamicAddressesRepository.kt | 3 + .../derivations/DerivationsRepository.kt | 3 + .../GetExtendedPublicKeyForCurrencyUseCase.kt | 45 ++--- features/tokendetails/impl/build.gradle.kts | 2 + .../DefaultTokenDetailsComponent.kt | 5 + .../model/DynamicAddressesDelegate.kt | 125 ++++++++++++ .../model/TokenDetailsClickIntents.kt | 4 + .../tokendetails/model/TokenDetailsModel.kt | 100 +++++++--- .../route/TokenDetailsBottomSheetConfig.kt | 3 + .../state/factory/TokenDetailsStateFactory.kt | 16 +- .../DynamicAddressesBottomSheetComponent.kt | 34 ++++ .../DynamicAddressesBottomSheet.kt | 28 +++ .../DynamicAddressesBottomSheetConfig.kt | 22 +++ .../DynamicAddressesBottomSheetContent.kt | 179 ++++++++++++++++++ 29 files changed, 874 insertions(+), 60 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt create mode 100644 core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_check_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_enable_top.xml create mode 100644 core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_enable_unavailable.xml create mode 100644 core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_flash_24.xml create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesError.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubSupportedUseCase.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/DynamicAddressesBottomSheetComponent.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheet.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetConfig.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetContent.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt new file mode 100644 index 0000000000..3b9ec07528 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt @@ -0,0 +1,78 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase +import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase +import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase +import com.tangem.domain.dynamicaddresses.GetDynamicAddressesStatusUseCase +import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase +import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase +import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase +import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.derivations.DerivationsRepository +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 DynamicAddressesDomainModule { + + @Provides + @Singleton + fun provideEnableDynamicAddressesUseCase( + dynamicAddressesRepository: DynamicAddressesRepository, + ): EnableDynamicAddressesUseCase { + return EnableDynamicAddressesUseCase(dynamicAddressesRepository) + } + + @Provides + @Singleton + fun provideDisableDynamicAddressesUseCase( + dynamicAddressesRepository: DynamicAddressesRepository, + ): DisableDynamicAddressesUseCase { + return DisableDynamicAddressesUseCase(dynamicAddressesRepository) + } + + @Provides + @Singleton + fun provideGetDynamicAddressesStatusUseCase( + dynamicAddressesRepository: DynamicAddressesRepository, + ): GetDynamicAddressesStatusUseCase { + return GetDynamicAddressesStatusUseCase(dynamicAddressesRepository) + } + + @Provides + @Singleton + fun provideGetDynamicReceiveAddressUseCase( + dynamicAddressesRepository: DynamicAddressesRepository, + ): GetDynamicReceiveAddressUseCase { + return GetDynamicReceiveAddressUseCase(dynamicAddressesRepository) + } + + @Provides + @Singleton + fun provideCreateConsolidationTransactionUseCase( + consolidationRepository: ConsolidationRepository, + ): CreateConsolidationTransactionUseCase { + return CreateConsolidationTransactionUseCase(consolidationRepository) + } + + @Provides + @Singleton + fun provideIsXpubSupportedUseCase(walletManagersFacade: WalletManagersFacade): IsXpubSupportedUseCase { + return IsXpubSupportedUseCase(walletManagersFacade) + } + + @Provides + @Singleton + fun provideIsXpubDerivedUseCase( + walletManagersFacade: WalletManagersFacade, + derivationsRepository: DerivationsRepository, + ): IsXpubDerivedUseCase { + return IsXpubDerivedUseCase(walletManagersFacade, derivationsRepository) + } +} \ No newline at end of file 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 cc707e1fce..548fadc6ad 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 @@ -33,7 +33,7 @@ class FinalizeTwinTask( visaCardScanHandler = null, visaCoroutineScope = null, shouldCheckIsAlreadyActivated = false, - isDynamicAddressesEnabled = false, + isDynamicAddressesEnabled = isDynamicAddressesEnabled, onboardingV2FeatureToggles = null, ).run(session, callback) is CompletionResult.Failure -> diff --git a/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_check_24.xml b/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_check_24.xml new file mode 100644 index 0000000000..fac7eb89dd --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_check_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_enable_top.xml b/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_enable_top.xml new file mode 100644 index 0000000000..a1e00c61f2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_enable_top.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_enable_unavailable.xml b/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_enable_unavailable.xml new file mode 100644 index 0000000000..57afa69fb6 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_enable_unavailable.xml @@ -0,0 +1,19 @@ + + + + + + + diff --git a/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_flash_24.xml b/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_flash_24.xml new file mode 100644 index 0000000000..71ad60eda8 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dynamic_addresses_bottomsheet_flash_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/dynamic-addresses/build.gradle.kts b/data/dynamic-addresses/build.gradle.kts index 47a15f3e68..a504298e11 100644 --- a/data/dynamic-addresses/build.gradle.kts +++ b/data/dynamic-addresses/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { // region Project - Libs implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } + implementation(tangemDeps.card.core) // endregion // region DI diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt index 28c3c017b6..a9d2bcd8b7 100644 --- a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt @@ -1,5 +1,6 @@ package com.tangem.data.dynamicaddresses +import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.data.common.account.WalletAccountsFetcher import com.tangem.data.common.account.WalletAccountsSaver import com.tangem.datasource.api.tangemTech.models.UserTokensResponse @@ -10,6 +11,7 @@ import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.blockchain.extensions.SimpleResult import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.logging.TangemLogger @@ -31,17 +33,20 @@ internal class DefaultDynamicAddressesRepository( .map { response -> val token = response.findToken(network) when { - token?.dynamicAddressesEnabled == true -> DynamicAddressesStatus.ENABLED - else -> DynamicAddressesStatus.DISABLED + token?.dynamicAddressesEnabled != true -> DynamicAddressesStatus.DISABLED + !isXpubAvailable(userWalletId, network) -> DynamicAddressesStatus.ENABLED_REQUIRES_SETUP + else -> DynamicAddressesStatus.ENABLED } - // TODO handle ENABLED_REQUIRES_SETUP when XPUB is not derived locally } .flowOn(dispatchers.io) } override suspend fun enable(userWalletId: UserWalletId, network: Network, xpub: String) { withContext(dispatchers.io) { - walletManagersFacade.enableXpubMode(userWalletId, network, xpub) + val result = walletManagersFacade.enableXpubMode(userWalletId, network, xpub) + if (result is SimpleResult.Failure) { + error("Failed to enable xpub mode for $userWalletId / ${network.id}: ${result.error}") + } updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true) runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } .onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) } @@ -50,7 +55,10 @@ internal class DefaultDynamicAddressesRepository( override suspend fun disable(userWalletId: UserWalletId, network: Network) { withContext(dispatchers.io) { - walletManagersFacade.disableXpubMode(userWalletId, network) + val result = walletManagersFacade.disableXpubMode(userWalletId, network) + if (result is SimpleResult.Failure) { + error("Failed to disable xpub mode for $userWalletId / ${network.id}: ${result.error}") + } updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false) runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } .onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) } @@ -70,6 +78,45 @@ internal class DefaultDynamicAddressesRepository( return walletManagersFacade.hasDynamicAddressesNonBaseBalances(userWalletId, network) } + override suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean { + return withContext(dispatchers.io) { + val response = walletAccountsFetcher.getSaved(userWalletId) ?: return@withContext false + val baseDerivationPath = network.derivationPath.value ?: return@withContext false + + response.accounts + .flatMap { it.tokens.orEmpty() } + .any { token -> + val tokenDerivationPath = token.derivationPath ?: return@any false + token.networkId == network.backendId && + tokenDerivationPath != baseDerivationPath && + hasNonZeroChangeOrIndex(tokenDerivationPath, baseDerivationPath) + } + } + } + + /** + * Checks if the token's derivation path has the same first 3 nodes (purpose/coin/account) + * as the base path but different change/index nodes (not both 0). + */ + private fun hasNonZeroChangeOrIndex(tokenPath: String, basePath: String): Boolean { + val tokenNodes = runCatching { DerivationPath(tokenPath).nodes }.getOrNull() ?: return false + val baseNodes = runCatching { DerivationPath(basePath).nodes }.getOrNull() ?: return false + + if (tokenNodes.size < DERIVATION_NODE_COUNT || baseNodes.size < DERIVATION_NODE_COUNT) return false + + // First 3 nodes must match (purpose/coin/account) by value, ignoring hardening + val isSameAccount = (0 until ACCOUNT_NODE_COUNT).all { i -> + tokenNodes[i].getIndex(includeHardened = false) == baseNodes[i].getIndex(includeHardened = false) + } + if (!isSameAccount) return false + + // Check if change or index ≠ 0 + val change = tokenNodes[CHANGE_NODE_INDEX].getIndex(includeHardened = false) + val index = tokenNodes[INDEX_NODE_INDEX].getIndex(includeHardened = false) + + return change != 0L || index != 0L + } + private suspend fun updateTokenDynamicAddressesFlag( userWalletId: UserWalletId, network: Network, @@ -92,6 +139,11 @@ internal class DefaultDynamicAddressesRepository( } } + private suspend fun isXpubAvailable(userWalletId: UserWalletId, network: Network): Boolean { + // Check if WalletManager is already in XPUB mode (DA was previously enabled on this device) + return walletManagersFacade.getDynamicAddressesReceiveAddress(userWalletId, network) != null + } + private fun GetWalletAccountsResponse.findToken(network: Network): UserTokensResponse.Token? { return accounts .flatMap { it.tokens.orEmpty() } @@ -103,4 +155,11 @@ internal class DefaultDynamicAddressesRepository( derivationPath == network.derivationPath.value && contractAddress == null } + + private companion object { + const val DERIVATION_NODE_COUNT = 5 + const val ACCOUNT_NODE_COUNT = 3 + const val CHANGE_NODE_INDEX = 3 + const val INDEX_NODE_INDEX = 4 + } } \ No newline at end of file diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt index c347bb1e54..3628a45990 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/derivations/DefaultDerivationsRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.wallets.derivations import arrow.core.getOrElse import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.toMapKey import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncStrict @@ -76,6 +77,23 @@ internal class DefaultDerivationsRepository @Inject constructor( } } + override suspend fun getExistingDerivedKeys( + userWalletId: UserWalletId, + seedKey: ByteArrayKey, + ): ExtendedPublicKeysMap { + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) + return userWallet.getExistingDerivedKeys()[seedKey] ?: ExtendedPublicKeysMap(emptyMap()) + } + + private fun UserWallet.getExistingDerivedKeys(): Map { + return when (this) { + is UserWallet.Cold -> scanResponse.derivedKeys + is UserWallet.Hot -> wallets + ?.associate { it.publicKey.toMapKey() to ExtendedPublicKeysMap(it.derivedKeys) } + .orEmpty() + } + } + override suspend fun hasMissedDerivations( userWalletId: UserWalletId, networksWithDerivationPath: Map, diff --git a/domain/dynamic-addresses/build.gradle.kts b/domain/dynamic-addresses/build.gradle.kts index 6190ff052f..d366a99097 100644 --- a/domain/dynamic-addresses/build.gradle.kts +++ b/domain/dynamic-addresses/build.gradle.kts @@ -13,8 +13,12 @@ dependencies { api(projects.domain.dynamicAddresses.models) implementation(projects.domain.models) + implementation(projects.domain.walletManager) + implementation(projects.domain.wallets) + implementation(projects.libs.blockchainSdk) implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } + implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt new file mode 100644 index 0000000000..3e732ca6dc --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt @@ -0,0 +1,53 @@ +package com.tangem.domain.dynamicaddresses + +import com.tangem.blockchain.common.Blockchain + +/** + * List of blockchains that support Dynamic Addresses (XPUB-based multi-address mode). + * Must match [Blockchain.isBip44DerivationStyleXPUB] from blockchain-sdk minus Kaspa (deferred). + * + * Per ASMPT-005: DA is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses. + * Only the default derivation style per blockchain is supported. + */ +object DynamicAddressesSupportedBlockchains { + + private const val BIP44_PURPOSE = 44L + private const val BIP84_PURPOSE = 84L + + private val supported = setOf( + Blockchain.Bitcoin, + Blockchain.BitcoinTestnet, + Blockchain.BitcoinCash, + Blockchain.BitcoinCashTestnet, + Blockchain.Litecoin, + Blockchain.Dogecoin, + Blockchain.Dash, + Blockchain.Ravencoin, + Blockchain.RavencoinTestnet, + ) + + private val supportedNetworkIds = supported.map { it.id }.toSet() + + /** + * Allowed BIP purpose nodes per network ID. + * BTC/LTC use BIP-84 (SegWit), others use BIP-44 (Legacy P2PKH). + */ + private val allowedPurposeByNetworkId: Map = buildMap { + put(Blockchain.Bitcoin.id, BIP84_PURPOSE) + put(Blockchain.BitcoinTestnet.id, BIP84_PURPOSE) + put(Blockchain.Litecoin.id, BIP84_PURPOSE) + put(Blockchain.BitcoinCash.id, BIP44_PURPOSE) + put(Blockchain.BitcoinCashTestnet.id, BIP44_PURPOSE) + put(Blockchain.Dogecoin.id, BIP44_PURPOSE) + put(Blockchain.Dash.id, BIP44_PURPOSE) + put(Blockchain.Ravencoin.id, BIP44_PURPOSE) + put(Blockchain.RavencoinTestnet.id, BIP44_PURPOSE) + } + + fun isSupported(blockchain: Blockchain): Boolean = blockchain in supported + + fun isSupportedByNetworkId(networkId: String): Boolean = networkId in supportedNetworkIds + + /** Returns the allowed BIP purpose node for the given network, or null if not supported */ + fun getAllowedPurpose(networkId: String): Long? = allowedPurposeByNetworkId[networkId] +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesError.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesError.kt new file mode 100644 index 0000000000..10d9872222 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesError.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.dynamicaddresses + +sealed class EnableDynamicAddressesError { + + data object ConflictingCustomTokens : EnableDynamicAddressesError() + + data class ServiceError(val cause: Throwable) : EnableDynamicAddressesError() +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesUseCase.kt index 8b888a54f4..7a4ceefba5 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesUseCase.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/EnableDynamicAddressesUseCase.kt @@ -1,6 +1,8 @@ package com.tangem.domain.dynamicaddresses import arrow.core.Either +import arrow.core.left +import arrow.core.right import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId @@ -9,8 +11,19 @@ class EnableDynamicAddressesUseCase( private val dynamicAddressesRepository: DynamicAddressesRepository, ) { - suspend operator fun invoke(userWalletId: UserWalletId, network: Network, xpub: String): Either = - Either.catch { + suspend operator fun invoke( + userWalletId: UserWalletId, + network: Network, + xpub: String, + ): Either { + return try { + if (dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network)) { + return EnableDynamicAddressesError.ConflictingCustomTokens.left() + } dynamicAddressesRepository.enable(userWalletId, network, xpub) + Unit.right() + } catch (e: Throwable) { + EnableDynamicAddressesError.ServiceError(e).left() } + } } \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt new file mode 100644 index 0000000000..3a62af2f51 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt @@ -0,0 +1,37 @@ +package com.tangem.domain.dynamicaddresses + +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.derivations.DerivationsRepository + +/** + * Checks if the account-level XPUB key is already derived (no card scan needed). + */ +class IsXpubDerivedUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val derivationsRepository: DerivationsRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean { + val blockchain = network.toBlockchain() + if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false + if (!blockchain.isBip44DerivationStyleXPUB()) return false + + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false + val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return false + if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return false + val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT)) + + val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey) + val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey) + return existingKeys[accountPath] != null + } + + private companion object { + const val ACCOUNT_PATH_DROP_COUNT = 2 + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubSupportedUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubSupportedUseCase.kt new file mode 100644 index 0000000000..ab998d8301 --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubSupportedUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.dynamicaddresses + +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletmanager.WalletManagersFacade + +/** + * Checks if XPUB generation is supported for the given wallet and network (hardware capability check). + */ +class IsXpubSupportedUseCase( + private val walletManagersFacade: WalletManagersFacade, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean { + val blockchain = network.toBlockchain() + if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false + + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false + return walletManager.wallet.publicKey.derivationType?.hdKey != null + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt index c30355fa48..4bcb627e16 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt @@ -19,4 +19,7 @@ interface DynamicAddressesRepository { suspend fun getLastUsedReceiveAddress(userWalletId: UserWalletId, network: Network): String? suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean + + /** Returns true if there are custom tokens with change/index ≠ 0 that conflict with DA */ + suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt index 9eefdc6899..a3ee510fde 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/derivations/DerivationsRepository.kt @@ -29,6 +29,9 @@ interface DerivationsRepository { derivations: Map>, ): Map + /** Returns already derived extended public keys for the given [seedKey] */ + suspend fun getExistingDerivedKeys(userWalletId: UserWalletId, seedKey: ByteArrayKey): ExtendedPublicKeysMap + /** Check if user [userWalletId] has missed derivations using map of [Network.ID] with extraDerivationPath */ suspend fun hasMissedDerivations( userWalletId: UserWalletId, diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt index e548eef8b7..100979e021 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/GetExtendedPublicKeyForCurrencyUseCase.kt @@ -1,7 +1,6 @@ package com.tangem.domain.wallets.usecase import arrow.core.Either -import arrow.core.right import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.extensions.ByteArrayKey @@ -38,23 +37,37 @@ class GetExtendedPublicKeyForCurrencyUseCase( error("No derivation found") } + val seedKey = walletManager.wallet.publicKey.seedKey + val existingKeys = derivationsRepository.getExistingDerivedKeys( + userWalletId = userWalletId, + seedKey = ByteArrayKey(seedKey), + ) + var childKey = makeChildKey( isBip44DerivationStyleXPUB = blockchain.isBip44DerivationStyleXPUB(), extendedPublicKey = hdKey.extendedPublicKey, derivationPath = hdKey.path, ) + // Fill from already derived keys if available + if (childKey.extendedPublicKey == null) { + existingKeys[childKey.derivationPath]?.let { + childKey = childKey.copy(extendedPublicKey = it) + } + } + + val parentPath = childKey.derivationPath.dropLastNodes(1) var parentKey = Key( - derivationPath = childKey.derivationPath.dropLastNodes(1), - extendedPublicKey = null, + derivationPath = parentPath, + extendedPublicKey = existingKeys[parentPath], ) val pendingDerivations = getPendingDerivations(childKey, parentKey) - val derivedKeys = deriveKeys( - userWalletId = userWalletId, - seedKey = walletManager.wallet.publicKey.seedKey, - paths = pendingDerivations, - ) + val derivedKeys = if (pendingDerivations.isNotEmpty()) { + deriveKeys(userWalletId = userWalletId, seedKey = seedKey, paths = pendingDerivations) + } else { + ExtendedPublicKeysMap(emptyMap()) + } if (childKey.extendedPublicKey == null) { childKey = childKey.copy( @@ -72,22 +85,6 @@ class GetExtendedPublicKeyForCurrencyUseCase( } } - /** - * @return true if xpub generation is supported, false otherwise - */ - suspend fun isSupported(userWalletId: UserWalletId, network: Network): Either = Either.catch { - val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) - ?: error("Wallet not found for user wallet $userWalletId and network ${network.id}") - - val blockchain = network.toBlockchain() - val isSecp256k1Blockchain = Blockchain.secp256k1Blockchains(network.isTestnet).contains(blockchain) - val isHdKey = walletManager.wallet.publicKey.derivationType?.hdKey - - val isSupported = isSecp256k1Blockchain && isHdKey != null - - return isSupported.right() - } - private suspend fun deriveKeys( userWalletId: UserWalletId, seedKey: ByteArray, diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 51a9203ba8..36bed031e2 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -68,6 +68,8 @@ dependencies { implementation(projects.domain.balanceHiding.models) implementation(projects.domain.card) implementation(projects.domain.demo) + implementation(projects.domain.dynamicAddresses) + implementation(projects.domain.dynamicAddresses.models) implementation(projects.domain.markets.models) implementation(projects.domain.models) implementation(projects.domain.notifications.models) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index a8f037db6d..035e4511bf 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -24,6 +24,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetails import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent import com.tangem.features.markets.token.block.TokenMarketBlockComponent import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokenreceive.TokenReceiveComponent @@ -148,6 +149,10 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( cloreMigrationModel = model.cloreMigrationModel, onDismiss = model.bottomSheetNavigation::dismiss, ) + is TokenDetailsBottomSheetConfig.DynamicAddresses -> DynamicAddressesBottomSheetComponent( + dynamicAddressesDelegate = model.dynamicAddressesDelegate, + onDismiss = model.bottomSheetNavigation::dismiss, + ) } @AssistedFactory diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt new file mode 100644 index 0000000000..e9fe02cce1 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt @@ -0,0 +1,125 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.model + +import com.tangem.common.core.TangemSdkError +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.res.R +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesError +import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase +import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheetConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@Suppress("LongParameterList") +internal class DynamicAddressesDelegate( + private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase, + private val isXpubDerivedUseCase: IsXpubDerivedUseCase, + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase, + private val uiMessageSender: UiMessageSender, + private val userWalletId: UserWalletId, + private val network: Network, + private val coroutineScope: CoroutineScope, + private val dispatchers: CoroutineDispatcherProvider, + private val showBottomSheet: () -> Unit, + private val dismissBottomSheet: () -> Unit, + private val onDynamicAddressesEnabled: () -> Unit, +) { + + private val _bottomSheetConfig = MutableStateFlow( + DynamicAddressesBottomSheetConfig.Enable( + isCardScanRequired = false, + onEnableClick = {}, + ), + ) + val bottomSheetConfig: StateFlow = _bottomSheetConfig.asStateFlow() + + fun onDynamicAddressesClick() { + coroutineScope.launch(dispatchers.main) { + val hasConflicts = dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) + if (hasConflicts) { + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Unavailable( + onGotItClick = dismissBottomSheet, + ) + showBottomSheet() + return@launch + } + + val isCardScanRequired = !isXpubAlreadyDerived() + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable( + isCardScanRequired = isCardScanRequired, + onEnableClick = ::onEnableClick, + ) + showBottomSheet() + } + } + + private fun onEnableClick() { + coroutineScope.launch(dispatchers.main) { + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable( + isCardScanRequired = false, + isLoading = true, + onEnableClick = {}, + ) + + val xpub = getExtendedPublicKeyUseCase(userWalletId, network).fold( + ifLeft = { error -> + if (isUserCancellation(error)) { + dismissBottomSheet() + } else { + TangemLogger.e("Failed to get XPUB: ${error.message}") + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( + onGotItClick = dismissBottomSheet, + ) + } + return@launch + }, + ifRight = { it }, + ) + + enableDynamicAddressesUseCase(userWalletId, network, xpub).fold( + ifLeft = { error -> + when (error) { + is EnableDynamicAddressesError.ConflictingCustomTokens -> { + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Unavailable( + onGotItClick = dismissBottomSheet, + ) + } + is EnableDynamicAddressesError.ServiceError -> { + TangemLogger.e("Failed to enable DA: ${error.cause.message}") + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( + onGotItClick = dismissBottomSheet, + ) + } + } + }, + ifRight = { + dismissBottomSheet() + onDynamicAddressesEnabled() + uiMessageSender.send( + SnackbarMessage(message = resourceReference(R.string.dynamic_addresses_enabled_toast_title)), + ) + }, + ) + } + } + + private suspend fun isXpubAlreadyDerived(): Boolean { + return isXpubDerivedUseCase(userWalletId, network) + } + + private fun isUserCancellation(error: Throwable): Boolean { + return error is TangemSdkError.UserCancelled || error.cause is TangemSdkError.UserCancelled + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index af6e80b86e..81e1b3a3d0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -48,6 +48,8 @@ interface TokenDetailsClickIntents { fun onGenerateExtendedKey() + fun onDynamicAddressesClick() + fun onCopyAddress(): TextReference? fun onAssociateClick() @@ -125,6 +127,8 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents { override fun onGenerateExtendedKey() { /* no op */ } + override fun onDynamicAddressesClick() { /* no op */ } + override fun onSellClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } override fun onSwapClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } 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 4cd0017182..46d9a4eb87 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 @@ -2,13 +2,19 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model import androidx.compose.runtime.Stable import arrow.core.getOrElse -import arrow.core.merge import arrow.core.right import com.tangem.utils.logging.TangemLogger import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate import com.arkivanov.decompose.router.slot.dismiss import com.tangem.blockchain.common.address.AddressType +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains +import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase +import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase +import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.receive.AddressModel @@ -16,9 +22,7 @@ import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels import com.tangem.common.ui.expressStatus.ExpressStatusBottomSheetConfig import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.analytics.api.AnalyticsEventHandler -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 @@ -53,6 +57,7 @@ import com.tangem.domain.models.TokenReceiveNotification import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId @@ -154,7 +159,6 @@ internal class TokenDetailsModel @Inject constructor( private val appRouter: AppRouter, private val router: InnerTokenDetailsRouter, private val tokenDetailsDeepLinkActionListener: TokenDetailsDeepLinkActionListener, - private val analyticsExceptionHandler: AnalyticsExceptionHandler, private val receiveAddressesFactory: ReceiveAddressesFactory, private val saveViewedYieldSupplyWarningUseCase: SaveViewedYieldSupplyWarningUseCase, private val saveViewedTokenReceiveWarningUseCase: SaveViewedTokenReceiveWarningUseCase, @@ -163,6 +167,11 @@ internal class TokenDetailsModel @Inject constructor( private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase, private val signCloreMessageUseCase: SignCloreMessageUseCase, + private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase, + private val isXpubSupportedUseCase: IsXpubSupportedUseCase, + private val isXpubDerivedUseCase: IsXpubDerivedUseCase, + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, ) : Model(), TokenDetailsClickIntents, ExpressTransactionsClickIntents, @@ -226,6 +235,27 @@ internal class TokenDetailsModel @Inject constructor( } // endregion + // region Dynamic Addresses + val dynamicAddressesDelegate by lazy(mode = LazyThreadSafetyMode.NONE) { + DynamicAddressesDelegate( + enableDynamicAddressesUseCase = enableDynamicAddressesUseCase, + isXpubDerivedUseCase = isXpubDerivedUseCase, + dynamicAddressesRepository = dynamicAddressesRepository, + getExtendedPublicKeyUseCase = getExtendedPublicKeyForCurrencyUseCase, + uiMessageSender = uiMessageSender, + userWalletId = userWalletId, + network = cryptoCurrency.network, + coroutineScope = modelScope, + dispatchers = dispatchers, + showBottomSheet = { + bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.DynamicAddresses) + }, + dismissBottomSheet = bottomSheetNavigation::dismiss, + onDynamicAddressesEnabled = ::onDynamicAddressesEnabled, + ) + } + // endregion Dynamic Addresses + private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) { tokenDetailsExpressStatusFactory.create( clickIntents = this, @@ -485,39 +515,45 @@ internal class TokenDetailsModel @Inject constructor( ).getOrElse { false } val isSupported = isXPUBSupported() + val isDynamicAddressesAvailable = isSupported && isDynamicAddressesAvailable() internalUiState.value = stateFactory.getStateWithUpdatedMenu( userWallet = userWallet, hasDerivations = hasDerivations, isSupported = isSupported, + isDynamicAddressesAvailable = isDynamicAddressesAvailable, ) } } + private fun isDynamicAddressesAvailable(): Boolean { + if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return false + if (cryptoCurrency !is CryptoCurrency.Coin) return false + + val networkId = cryptoCurrency.network.rawId + if (!DynamicAddressesSupportedBlockchains.isSupportedByNetworkId(networkId)) return false + + return isDefaultBaseDerivation(cryptoCurrency.network.derivationPath, networkId) + } + + private fun isDefaultBaseDerivation(derivationPath: Network.DerivationPath, networkId: String): Boolean { + val pathValue = derivationPath.value ?: return false + val nodes = runCatching { DerivationPath(pathValue).nodes }.getOrNull() ?: return false + if (nodes.size < BASE_DERIVATION_NODE_COUNT) return false + + val purposeNode = nodes.first() + val allowedPurpose = DynamicAddressesSupportedBlockchains.getAllowedPurpose(networkId) ?: return false + if (purposeNode.getIndex(includeHardened = false) != allowedPurpose) return false + + val changeNode = nodes[nodes.size - 2] + val indexNode = nodes.last() + + return changeNode.getIndex(includeHardened = false) == 0L && + indexNode.getIndex(includeHardened = false) == 0L + } + private suspend fun isXPUBSupported(): Boolean { - return getExtendedPublicKeyForCurrencyUseCase.isSupported( - userWalletId = userWalletId, - network = cryptoCurrency.network, - ) - .mapLeft { throwable -> - analyticsExceptionHandler.sendException( - event = ExceptionAnalyticsEvent( - exception = throwable, - params = mapOf( - "blockchainId" to cryptoCurrency.network.id.rawId.value, - "networkId" to cryptoCurrency.network.backendId, - ), - ), - ) - - TangemLogger.e( - "Unable to get wallet manager for user wallet $userWalletId and network ${cryptoCurrency.network}", - throwable, - ) - - false - } - .merge() + return isXpubSupportedUseCase(userWalletId = userWalletId, network = cryptoCurrency.network) } private fun createSelectedAppCurrencyFlow(): StateFlow { @@ -657,6 +693,15 @@ internal class TokenDetailsModel @Inject constructor( openStaking() } + override fun onDynamicAddressesClick() = dynamicAddressesDelegate.onDynamicAddressesClick() + + private fun onDynamicAddressesEnabled() { + updateTopBarMenu() + modelScope.launch(dispatchers.main) { + cryptoCurrencyBalanceFetcher.invokeAndAwait(userWalletId = userWalletId, currency = cryptoCurrency) + } + } + override fun onGenerateExtendedKey() { modelScope.launch(dispatchers.main) { val extendedKey = getExtendedPublicKeyForCurrencyUseCase( @@ -1382,5 +1427,6 @@ internal class TokenDetailsModel @Inject constructor( private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L + const val BASE_DERIVATION_NODE_COUNT = 5 } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/route/TokenDetailsBottomSheetConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/route/TokenDetailsBottomSheetConfig.kt index 8ba904843b..071fa53e51 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/route/TokenDetailsBottomSheetConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/route/TokenDetailsBottomSheetConfig.kt @@ -27,4 +27,7 @@ sealed class TokenDetailsBottomSheetConfig : Route { @Serializable data object CloreMigration : TokenDetailsBottomSheetConfig() + + @Serializable + data object DynamicAddresses : TokenDetailsBottomSheetConfig() } \ No newline at end of file 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 3739c109ba..bd9da5b438 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 @@ -167,12 +167,18 @@ internal class TokenDetailsStateFactory( userWallet: UserWallet, hasDerivations: Boolean, isSupported: Boolean, + isDynamicAddressesAvailable: Boolean = false, ): TokenDetailsState { return with(currentStateProvider()) { copy( topAppBarConfig = topAppBarConfig.copy( tokenDetailsAppBarMenuConfig = topAppBarConfig.tokenDetailsAppBarMenuConfig - ?.updateMenu(userWallet, hasDerivations, isSupported), + ?.updateMenu( + userWallet = userWallet, + hasDerivations = hasDerivations, + isSupported = isSupported, + isDynamicAddressesAvailable = isDynamicAddressesAvailable, + ), ), ) } @@ -206,6 +212,7 @@ internal class TokenDetailsStateFactory( userWallet: UserWallet, hasDerivations: Boolean, isSupported: Boolean, + isDynamicAddressesAvailable: Boolean, ): TokenDetailsAppBarMenuConfig? { if (userWallet is UserWallet.Cold && userWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() @@ -215,6 +222,13 @@ internal class TokenDetailsStateFactory( return copy( items = buildList { + if (isDynamicAddressesAvailable) { + TangemDropdownMenuItem( + title = resourceReference(R.string.dynamic_addresses), + textColor = themedColor { TangemTheme.colors.text.primary1 }, + onClick = tokenDetailsClickIntents::onDynamicAddressesClick, + ).let(::add) + } if (isSupported && hasDerivations) { TangemDropdownMenuItem( title = resourceReference(R.string.token_details_generate_xpub), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/DynamicAddressesBottomSheetComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/DynamicAddressesBottomSheetComponent.kt new file mode 100644 index 0000000000..6b476ee779 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/DynamicAddressesBottomSheetComponent.kt @@ -0,0 +1,34 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.feature.tokendetails.presentation.tokendetails.model.DynamicAddressesDelegate +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheet + +internal class DynamicAddressesBottomSheetComponent( + private val dynamicAddressesDelegate: DynamicAddressesDelegate, + private val onDismiss: () -> Unit, +) : ComposableBottomSheetComponent { + + override fun dismiss() { + onDismiss() + } + + @Composable + override fun BottomSheet() { + val content by dynamicAddressesDelegate.bottomSheetConfig.collectAsStateWithLifecycle() + + val config = remember(content) { + TangemBottomSheetConfig( + isShown = true, + onDismissRequest = ::dismiss, + content = content, + ) + } + DynamicAddressesBottomSheet(config = config) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheet.kt new file mode 100644 index 0000000000..a12557f768 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheet.kt @@ -0,0 +1,28 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet +import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.R as CoreR + +@Composable +internal fun DynamicAddressesBottomSheet(config: TangemBottomSheetConfig) { + TangemModalBottomSheet( + config = config, + title = { + TangemModalBottomSheetTitle( + endIconRes = CoreR.drawable.ic_close_24, + onEndClick = config.onDismissRequest, + ) + }, + ) { content -> + when (content) { + is DynamicAddressesBottomSheetConfig.Enable -> DynamicAddressesEnableContent(content = content) + is DynamicAddressesBottomSheetConfig.Unavailable -> DynamicAddressesUnavailableContent(content = content) + is DynamicAddressesBottomSheetConfig.ServiceUnavailable -> DynamicAddressesServiceUnavailableContent( + content = content, + ) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetConfig.kt new file mode 100644 index 0000000000..a228fb29b2 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetConfig.kt @@ -0,0 +1,22 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses + +import androidx.compose.runtime.Immutable +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +@Immutable +internal sealed class DynamicAddressesBottomSheetConfig : TangemBottomSheetConfigContent { + + data class Enable( + val isCardScanRequired: Boolean, + val isLoading: Boolean = false, + val onEnableClick: () -> Unit, + ) : DynamicAddressesBottomSheetConfig() + + data class Unavailable( + val onGotItClick: () -> Unit, + ) : DynamicAddressesBottomSheetConfig() + + data class ServiceUnavailable( + val onGotItClick: () -> Unit, + ) : DynamicAddressesBottomSheetConfig() +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetContent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetContent.kt new file mode 100644 index 0000000000..fb1eaa7605 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetContent.kt @@ -0,0 +1,179 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.res.R +import com.tangem.core.ui.R as CoreR + +@Composable +internal fun DynamicAddressesEnableContent(content: DynamicAddressesBottomSheetConfig.Enable) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = painterResource(id = CoreR.drawable.ic_dynamic_addresses_bottomsheet_enable_top), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size44), + tint = TangemTheme.colors.icon.accent, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12)) + + Text( + text = stringResourceSafe(id = R.string.dynamic_addresses), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8)) + + Text( + text = stringResourceSafe(id = R.string.dynamic_addresses_enter_subtitle), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24)) + + FeatureItem( + iconRes = CoreR.drawable.ic_dynamic_addresses_bottomsheet_flash_24, + title = stringResourceSafe(id = R.string.dynamic_addresses_enter_features_receving_title), + description = stringResourceSafe(id = R.string.dynamic_addresses_enter_features_receving_description), + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) + + FeatureItem( + iconRes = CoreR.drawable.ic_dynamic_addresses_bottomsheet_check_24, + title = stringResourceSafe(id = R.string.dynamic_addresses_enter_features_privacy_title), + description = stringResourceSafe(id = R.string.dynamic_addresses_enter_features_privacy_description), + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24)) + + PrimaryButton( + text = stringResourceSafe(id = R.string.dynamic_addresses_enter_main_button_title), + onClick = content.onEnableClick, + modifier = Modifier.fillMaxWidth(), + showProgress = content.isLoading, + enabled = !content.isLoading, + // TODO add card icon when isCardScanRequired + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) + } +} + +@Composable +internal fun DynamicAddressesUnavailableContent(content: DynamicAddressesBottomSheetConfig.Unavailable) { + ErrorContent( + titleRes = R.string.dynamic_addresses_error_has_custom_token_title, + descriptionRes = R.string.dynamic_addresses_error_has_custom_token_description, + buttonTextRes = R.string.common_got_it, + onButtonClick = content.onGotItClick, + ) +} + +@Composable +internal fun DynamicAddressesServiceUnavailableContent(content: DynamicAddressesBottomSheetConfig.ServiceUnavailable) { + ErrorContent( + titleRes = R.string.dynamic_addresses_error_service_unavailable_title, + descriptionRes = R.string.dynamic_addresses_error_service_unavailable_description, + buttonTextRes = R.string.common_got_it, + onButtonClick = content.onGotItClick, + ) +} + +@Composable +private fun ErrorContent(titleRes: Int, descriptionRes: Int, buttonTextRes: Int, onButtonClick: () -> Unit) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + painter = painterResource(id = CoreR.drawable.ic_dynamic_addresses_bottomsheet_enable_unavailable), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size44), + tint = TangemTheme.colors.icon.warning, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12)) + + Text( + text = stringResourceSafe(id = titleRes), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8)) + + Text( + text = stringResourceSafe(id = descriptionRes), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24)) + + PrimaryButton( + text = stringResourceSafe(id = buttonTextRes), + onClick = onButtonClick, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) + } +} + +@Composable +private fun FeatureItem(iconRes: Int, title: String, description: String) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12), + ) { + Icon( + painter = painterResource(id = iconRes), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size24), + tint = TangemTheme.colors.icon.accent, + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing4)) + Text( + text = description, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } + } +} \ No newline at end of file From 9c8bfd4139df6a555ad6b9e23ff009d1c399490c Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Apr 2026 15:25:25 +0200 Subject: [PATCH 04/32] Updated on 2026-08-14 --- .../components/fields/entity/SearchBarUM.kt | 1 + .../ui/ds/field/search/TangemSearchField.kt | 9 +- .../ui/ds/opportunities/OpportunitiesBG.kt | 4 +- .../domain/search/model/SearchResult.kt | 2 +- .../search/model/UserAssetSearchEntry.kt | 2 + .../search/model/UserAssetSearchItem.kt | 13 ++ .../search/usecase/GetSearchResultsUseCase.kt | 46 ++++++- .../feed/components/FeedEntryChildFactory.kt | 7 + .../search/DefaultSearchComponent.kt | 4 + .../features/feed/model/search/SearchModel.kt | 87 +++++++----- .../converter/UserAssetSearchItemConverter.kt | 92 +++++++++++++ .../features/feed/ui/search/SearchContent.kt | 125 +++++++++++++++--- .../ui/search/preview/SearchContentPreview.kt | 40 +++--- .../feed/ui/search/state/SearchCallbacks.kt | 1 + .../features/feed/ui/search/state/SearchUM.kt | 53 ++++++-- 15 files changed, 394 insertions(+), 92 deletions(-) create mode 100644 domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt index 158c768fe8..6050d96cb3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/entity/SearchBarUM.kt @@ -9,4 +9,5 @@ data class SearchBarUM( val isActive: Boolean, val onActiveChange: (Boolean) -> Unit, val onClearClick: () -> Unit = {}, + val onCancelClick: (() -> Unit)? = null, ) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt index 4e2e9ebb42..905ef8c5fb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt @@ -289,8 +289,13 @@ private fun CancelButton( state.onQueryChange("") } keyboardController?.hide() - state.onClearClick() - focusManager.clearFocus() + val onCancel = state.onCancelClick + if (onCancel != null) { + onCancel() + } else { + state.onClearClick() + focusManager.clearFocus() + } }, ) } 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 79f093e351..ac427c9961 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 @@ -193,7 +193,7 @@ private fun BoxScope.UrlBackground(imageUrl: String?, blurRadius: Dp) { AsyncImage( model = imageRequest, contentDescription = null, - contentScale = ContentScale.Crop, + contentScale = ContentScale.FillBounds, modifier = Modifier .matchParentSize() .scale(SCALE_FACTOR) @@ -207,7 +207,7 @@ private fun BoxScope.ResBackground(res: Int, blurRadius: Dp) { Image( painter = painterResource(res), contentDescription = null, - contentScale = ContentScale.Crop, + contentScale = ContentScale.FillBounds, modifier = Modifier .matchParentSize() .scale(SCALE_FACTOR) diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt index 98a25e4996..40fc76fed4 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/model/SearchResult.kt @@ -3,5 +3,5 @@ package com.tangem.domain.search.model data class SearchResult( val textHints: List, val recentTokens: List, - val userAssets: List, + val userAssets: List, ) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt index fd64a23e24..c6a3d45854 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchEntry.kt @@ -2,6 +2,7 @@ package com.tangem.domain.search.model import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId @@ -10,5 +11,6 @@ data class UserAssetSearchEntry( val userWalletName: String, val accountId: AccountId, val accountName: AccountName, + val accountIcon: CryptoPortfolioIcon, val currencyStatus: CryptoCurrencyStatus, ) \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt new file mode 100644 index 0000000000..7a63289486 --- /dev/null +++ b/domain/search/src/main/java/com/tangem/domain/search/model/UserAssetSearchItem.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.search.model + +sealed interface UserAssetSearchItem { + + data class Single(val entry: UserAssetSearchEntry) : UserAssetSearchItem + + data class Grouped( + val tokenName: String, + val tokenSymbol: String, + val tokenIconUrl: String?, + val entries: List, + ) : UserAssetSearchItem +} \ No newline at end of file diff --git a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt index b6167208a2..0b6b37be48 100644 --- a/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt +++ b/domain/search/src/main/java/com/tangem/domain/search/usecase/GetSearchResultsUseCase.kt @@ -9,10 +9,12 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isLocked import com.tangem.domain.search.model.SearchResult import com.tangem.domain.search.model.UserAssetSearchEntry +import com.tangem.domain.search.model.UserAssetSearchItem import com.tangem.domain.search.repository.SearchRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import java.math.BigDecimal /** * Primary search use case that produces [SearchResult] based on the current query. @@ -70,9 +72,12 @@ class GetSearchResultsUseCase( if (unlockedWallets.isEmpty()) return@combine emptyList() - statusLists + val entries = statusLists .filter { it.userWalletId in unlockedWallets } .flatMap { statusList -> extractMatchingAssets(statusList, unlockedWallets, lowerQuery) } + + val shouldGroup = needsGrouping(unlockedWallets.values, statusLists) + groupAndSort(entries, shouldGroup) }.map { userAssets -> SearchResult( textHints = emptyList(), @@ -82,6 +87,44 @@ class GetSearchResultsUseCase( } } + private fun needsGrouping(unlockedWallets: Collection, statusLists: List): Boolean { + if (unlockedWallets.size > 1) return true + + val totalAccounts = statusLists + .filter { sl -> unlockedWallets.any { it.walletId == sl.userWalletId } } + .sumOf { it.accountStatuses.filterCryptoPortfolio().size } + + return totalAccounts > 1 + } + + private fun groupAndSort(entries: List, shouldGroup: Boolean): List { + if (!shouldGroup) { + return entries + .map { UserAssetSearchItem.Single(it) } + .sortedByDescending { it.entry.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } + } + + val grouped = entries.groupBy { entry -> + val rawId = entry.currencyStatus.currency.id.rawCurrencyId + rawId?.value ?: "${entry.currencyStatus.currency.name}|${entry.currencyStatus.currency.symbol}" + } + + return grouped.map { (_, groupEntries) -> + val assetInfo = groupEntries.first() + UserAssetSearchItem.Grouped( + tokenName = assetInfo.currencyStatus.currency.name, + tokenSymbol = assetInfo.currencyStatus.currency.symbol, + tokenIconUrl = assetInfo.currencyStatus.currency.iconUrl, + entries = groupEntries, + ) + }.sortedByDescending { item -> + when (item) { + is UserAssetSearchItem.Grouped -> + item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } + } + } + } + private fun extractMatchingAssets( statusList: AccountStatusList, wallets: Map, @@ -103,6 +146,7 @@ class GetSearchResultsUseCase( userWalletName = wallet.name, accountId = accountStatus.accountId, accountName = accountStatus.account.accountName, + accountIcon = accountStatus.account.icon, currencyStatus = currencyStatus, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index a51d8689f7..fb93121de6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -148,6 +148,13 @@ internal class FeedEntryChildFactory @Inject constructor( appComponentContext = appComponentContext, params = DefaultSearchComponent.Params( onBackClick = onBackClicked, + onMarketTokenClick = { token, currency -> + feedEntryClickIntents.onMarketItemClick( + token = token, + appCurrency = currency, + source = AnalyticsParam.ScreensSources.Market.value, + ) + }, ), ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt index b8a8ac1119..677f40ed5a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/search/DefaultSearchComponent.kt @@ -15,6 +15,8 @@ import com.tangem.core.ui.ds.field.search.TangemFieldShape import com.tangem.core.ui.ds.field.search.TangemSearchField import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.feed.model.search.SearchModel import com.tangem.features.feed.ui.search.SearchContent import com.tangem.features.feed.ui.search.state.SearchCallbacks @@ -74,6 +76,7 @@ internal class DefaultSearchComponent( onClearHintsClick = model::clearSearchHistory, onTextHintClick = model::onTextHintClick, onResultMarketTokenClick = model::onResultMarketTokenClick, + onHistoryTokenClick = model::onHistoryTokenClick, ) } SearchContent( @@ -86,5 +89,6 @@ internal class DefaultSearchComponent( data class Params( val onBackClick: () -> Unit, + val onMarketTokenClick: ((TokenMarketParams, AppCurrency) -> Unit), ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt index 2273f437bb..f43e8c5766 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt @@ -1,19 +1,21 @@ package com.tangem.features.feed.model.search import arrow.core.getOrElse +import com.tangem.common.ui.charts.state.MarketChartData +import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter +import com.tangem.common.ui.charts.state.sorted import com.tangem.common.ui.markets.models.MarketsListItemUM 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.common.ui.charts.state.MarketChartData -import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter -import com.tangem.common.ui.charts.state.sorted +import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.markets.GetMarketsTokenListFlowUseCase import com.tangem.domain.markets.GetTokenPriceChartUseCase import com.tangem.domain.markets.PriceChangeInterval -import com.tangem.domain.models.account.AccountName +import com.tangem.domain.markets.TokenMarketParams +import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase import com.tangem.domain.search.usecase.GetSearchResultsUseCase import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase @@ -26,6 +28,7 @@ import com.tangem.features.feed.model.search.converter.MarketsListItemUMToRecent import com.tangem.features.feed.model.search.converter.MarketsListItemUMWithAppCurrency import com.tangem.features.feed.model.search.converter.RecentSearchTokenToMarketsListItemUMConverter import com.tangem.features.feed.model.search.converter.RecentSearchTokenWithAppCurrency +import com.tangem.features.feed.model.search.converter.UserAssetSearchItemConverter import com.tangem.features.feed.model.search.state.SearchStateController import com.tangem.features.feed.model.search.state.transformers.* import com.tangem.features.feed.ui.search.state.* @@ -35,13 +38,8 @@ import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.coroutines.launch import javax.inject.Inject private const val UPDATE_QUOTES_TIMER_MILLIS = 60000L @@ -54,6 +52,7 @@ internal class SearchModel @Inject constructor( paramsContainer: ParamsContainer, getMarketsTokenListFlowUseCase: GetMarketsTokenListFlowUseCase, getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, + getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getSearchResultsUseCase: GetSearchResultsUseCase, private val saveSearchQueryUseCase: SaveSearchQueryUseCase, private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase, @@ -77,6 +76,13 @@ internal class SearchModel @Inject constructor( initialValue = AppCurrency.Default, ) + private val isBalanceHidden = getBalanceHidingSettingsUseCase.isBalanceHidden() + .stateIn( + scope = modelScope, + started = SharingStarted.Eagerly, + initialValue = false, + ) + private val marketsListItemToRecentSearchTokenConverter by lazy { MarketsListItemUMToRecentSearchTokenConverter() } @@ -143,17 +149,38 @@ internal class SearchModel @Inject constructor( ) saveRecentSearchTokenUseCase(marketsListItemToRecentSearchTokenConverter.convert(input)) saveSearchQueryUseCase(stateController.value.searchBar.query) + withContext(dispatchers.mainImmediate) { + searchMarketsListManager.getTokenById(item.id)?.let { found -> + params.onMarketTokenClick(found.toSerializableParam(), appCurrency) + } + } } } + fun onHistoryTokenClick(item: MarketsListItemUM) { + val tokenMarketParams = TokenMarketParams( + id = item.id, + name = item.name, + symbol = item.currencySymbol, + tokenQuotes = TokenMarketParams.Quotes( + currentPrice = item.price.fiatPrice, + h24Percent = null, + weekPercent = null, + monthPercent = null, + ), + imageUrl = item.iconUrl, + ) + params.onMarketTokenClick(tokenMarketParams, currentAppCurrency.value) + } + private fun initCallbacks() { stateController.update(object : SearchUMTransformer { override fun transform(prevState: SearchUM): SearchUM { return prevState.copy( searchBar = prevState.searchBar.copy( onQueryChange = ::onQueryChange, - onActiveChange = ::onActiveChange, onClearClick = ::onClearClick, + onCancelClick = params.onBackClick, ), ) } @@ -164,10 +191,6 @@ internal class SearchModel @Inject constructor( stateController.update(UpdateSearchBarQueryTransformer(query)) } - private fun onActiveChange(isActive: Boolean) { - if (!isActive) params.onBackClick() - } - private fun onClearClick() { stateController.update(UpdateSearchBarQueryTransformer("")) } @@ -200,20 +223,19 @@ internal class SearchModel @Inject constructor( private fun subscribeToSearchResults(query: String) { modelScope.launch { - getSearchResultsUseCase(query = query).collectLatest { searchResult -> - val userAssets = searchResult.userAssets.map { entry -> - UserAssetItemUM( - id = "${entry.userWalletId.stringValue}_${entry.accountId.value}" + - "_${entry.currencyStatus.currency.id.value}", - tokenIconUrl = entry.currencyStatus.currency.iconUrl, - tokenName = entry.currencyStatus.currency.name, - tokenSymbol = entry.currencyStatus.currency.symbol, - accountName = entry.accountName.toDisplayString(), - onClick = { - // TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task. - }, - ) - }.toImmutableList() + combine( + getSearchResultsUseCase(query = query), + currentAppCurrency, + isBalanceHidden, + ) { searchResult, appCurrency, balanceHidden -> + val converter = UserAssetSearchItemConverter( + appCurrency = appCurrency, + isBalanceHidden = balanceHidden, + ) + searchResult.userAssets + .map(converter::convert) + .toImmutableList() + }.collectLatest { userAssets -> stateController.update(UpdateUserAssetsTransformer(userAssets)) } }.saveIn(searchResultsJob) @@ -334,13 +356,6 @@ internal class SearchModel @Inject constructor( } } - private fun AccountName.toDisplayString(): String { - return when (this) { - is AccountName.DefaultMain -> "Main" // TODO [REDACTED_TASK_KEY] localize - is AccountName.Custom -> value - } - } - private fun CoroutineScope.loadQuotesWithTimer(timeMillis: Long) { launch { while (true) { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt new file mode 100644 index 0000000000..4e742afdde --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt @@ -0,0 +1,92 @@ +package com.tangem.features.feed.model.search.converter + +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter +import com.tangem.core.ui.ds.image.TangemIconUM +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.search.model.UserAssetSearchEntry +import com.tangem.domain.search.model.UserAssetSearchItem +import com.tangem.features.feed.ui.search.state.UserAssetItemUM +import com.tangem.utils.StringsSigns +import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.toImmutableList +import java.math.BigDecimal + +internal class UserAssetSearchItemConverter( + private val appCurrency: AppCurrency, + private val isBalanceHidden: Boolean, +) : Converter { + + override fun convert(value: UserAssetSearchItem): UserAssetItemUM { + return when (value) { + is UserAssetSearchItem.Single -> convertSingle(value.entry) + is UserAssetSearchItem.Grouped -> convertGrouped(value) + } + } + + private fun convertSingle(entry: UserAssetSearchEntry): UserAssetItemUM.Single { + val currency = entry.currencyStatus.currency + val value = entry.currencyStatus.value + + return UserAssetItemUM.Single( + id = "${entry.userWalletId.stringValue}_${entry.accountId.value}_${currency.id.value}", + icon = TangemIconUM.Currency( + currencyIconState = CryptoCurrencyToIconStateConverter().convert(entry.currencyStatus), + ), + tokenName = currency.name, + tokenSymbol = currency.symbol, + fiatRate = value.fiatRate?.format { fiat(appCurrency.code, appCurrency.symbol) }, + cryptoBalance = formatCryptoAmount(value.amount, currency.symbol, currency.decimals), + fiatBalance = formatFiatAmount(value.fiatAmount), + isBalanceHidden = isBalanceHidden, + onClick = {}, + ) + } + + private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped { + val totalFiat = item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } + val totalCrypto = item.entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO } + + val firstCurrency = item.entries.first().currencyStatus.currency + val children = item.entries.map { entry -> + UserAssetItemUM.GroupedChild( + walletName = entry.userWalletName, + accountName = entry.accountName.toUM(), + accountIcon = entry.accountIcon.value, + accountColor = entry.accountIcon.color, + cryptoBalance = formatCryptoAmount( + entry.currencyStatus.value.amount, + entry.currencyStatus.currency.symbol, + entry.currencyStatus.currency.decimals, + ), + fiatBalance = formatFiatAmount(entry.currencyStatus.value.fiatAmount), + ) + }.toImmutableList() + + return UserAssetItemUM.Grouped( + id = "grouped_${item.tokenName}_${item.tokenSymbol}", + icon = TangemIconUM.Currency( + currencyIconState = CryptoCurrencyToIconStateConverter().convert(item.entries.first().currencyStatus), + ), + tokenName = item.tokenName, + tokenSymbol = item.tokenSymbol, + tokensCount = item.entries.size, + totalCryptoBalance = formatCryptoAmount(totalCrypto, firstCurrency.symbol, firstCurrency.decimals), + totalFiatBalance = formatFiatAmount(totalFiat), + isBalanceHidden = isBalanceHidden, + children = children, + onClick = {}, + ) + } + + private fun formatCryptoAmount(amount: BigDecimal?, symbol: String, decimals: Int): String { + return amount?.format { crypto(symbol, decimals) } ?: StringsSigns.DASH_SIGN + } + + private fun formatFiatAmount(fiatAmount: BigDecimal?): String { + return fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } ?: StringsSigns.DASH_SIGN + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt index 287b580b15..6d7118f812 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt @@ -3,20 +3,16 @@ package com.tangem.features.feed.ui.search import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListScope -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.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.draw.drawBehind import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity @@ -32,7 +28,6 @@ import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.list.InfiniteListHandler 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.clickableSingle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe @@ -54,6 +49,15 @@ internal fun SearchContent( val lazyListState = rememberLazyListState() val background = LocalMainBottomSheetColor.current.value + val contentStructureKey = when (content) { + is SearchContentUM.InitialEmpty -> "empty" + is SearchContentUM.History -> "history" + is SearchContentUM.Results -> "results_${content.userAssets.isNotEmpty()}" + } + LaunchedEffect(contentStructureKey) { + lazyListState.scrollToItem(0) + } + LazyColumn( state = lazyListState, modifier = modifier @@ -72,6 +76,7 @@ internal fun SearchContent( history = content, onClearAllClick = searchCallbacks.onClearHintsClick, onHintClick = searchCallbacks.onTextHintClick, + onHistoryTokenClick = searchCallbacks.onHistoryTokenClick, ) is SearchContentUM.Results -> searchResultsItems( results = content, @@ -98,6 +103,7 @@ private fun LazyListScope.searchHistoryItems( history: SearchContentUM.History, onClearAllClick: (() -> Unit), onHintClick: (String) -> Unit, + onHistoryTokenClick: (MarketsListItemUM) -> Unit, ) { if (!history.textHints.isEmpty() || !history.recentTokens.isEmpty()) { item(key = "recents") { @@ -107,15 +113,20 @@ private fun LazyListScope.searchHistoryItems( ) } } - items( + itemsIndexed( items = history.textHints, - key = { "hint_${it.text}" }, - ) { hint -> + key = { _, item -> "hint_${item.text}" }, + ) { index, hint -> TextHintItem(hint = hint, onHintClick = { onHintClick(hint.text) }) - HorizontalDivider( - modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2), - color = TangemTheme.colors2.border.neutral.primary, - ) + if (index < history.textHints.size - 1) { + HorizontalDivider( + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x2), + color = TangemTheme.colors2.border.neutral.primary, + ) + } + } + item { + SpacerH(TangemTheme.dimens2.x2) } items( items = history.recentTokens, @@ -129,7 +140,7 @@ private fun LazyListScope.searchHistoryItems( shape = RoundedCornerShape(TangemTheme.dimens2.x5), ), model = token, - onClick = {}, // TODO in [REDACTED_TASK_KEY] + onClick = { onHistoryTokenClick(token) }, ) } } @@ -264,9 +275,16 @@ private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) { } } -// TODO in [REDACTED_TASK_KEY] while just a stub item. Will be handled in next task. @Composable private fun UserAssetItem(asset: UserAssetItemUM) { + when (asset) { + is UserAssetItemUM.Single -> SingleUserAssetItem(asset) + is UserAssetItemUM.Grouped -> GroupedUserAssetItem(asset) + } +} + +@Composable +private fun SingleUserAssetItem(asset: UserAssetItemUM.Single) { Row( modifier = Modifier .fillMaxWidth() @@ -276,10 +294,8 @@ private fun UserAssetItem(asset: UserAssetItemUM) { horizontalArrangement = Arrangement.spacedBy(8.dp), ) { TangemIcon( - tangemIconUM = TangemIconUM.Url(asset.tokenIconUrl, fallbackRes = R.drawable.ic_custom_token_44), - modifier = Modifier - .size(40.dp) - .clip(CircleShape), + modifier = Modifier.size(40.dp), + tangemIconUM = asset.icon, ) Column(modifier = Modifier.weight(1f)) { Text( @@ -289,12 +305,79 @@ private fun UserAssetItem(asset: UserAssetItemUM) { maxLines = 1, ) Text( - text = "${asset.tokenSymbol} · ${asset.accountName}", + text = asset.tokenSymbol, style = TangemTheme.typography2.captionRegular13, color = TangemTheme.colors2.text.neutral.tertiary, maxLines = 1, ) } + if (!asset.isBalanceHidden) { + Column(horizontalAlignment = Alignment.End) { + Text( + text = asset.fiatBalance, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = asset.cryptoBalance, + style = TangemTheme.typography2.captionRegular13, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + ) + } + } + } +} + +// TODO [REDACTED_JIRA] update ui item to Portfolio block item +@Composable +private fun GroupedUserAssetItem(asset: UserAssetItemUM.Grouped) { + Column( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = asset.onClick) + .padding(horizontal = 12.dp, vertical = 14.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + TangemIcon( + modifier = Modifier.size(40.dp), + tangemIconUM = asset.icon, + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = asset.tokenName, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = "${asset.tokenSymbol} · ${asset.tokensCount}", + style = TangemTheme.typography2.captionRegular13, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + ) + } + if (!asset.isBalanceHidden) { + Column(horizontalAlignment = Alignment.End) { + Text( + text = asset.totalFiatBalance, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = asset.totalCryptoBalance, + style = TangemTheme.typography2.captionRegular13, + color = TangemTheme.colors2.text.neutral.tertiary, + maxLines = 1, + ) + } + } + } } } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt index bfb95e3819..e5cd68f79c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt @@ -2,11 +2,7 @@ package com.tangem.features.feed.ui.search.preview import android.content.res.Configuration import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -15,7 +11,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp 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.ds.image.TangemIconUM import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -206,18 +204,22 @@ internal object SearchContentPreviewFixtures { updateTimestamp = updateTimestamp, ) - private fun userAsset( - id: String, - name: String, - symbol: String, - accountName: String, - iconUrl: String? = null, - ): UserAssetItemUM = UserAssetItemUM( + private fun userAsset(id: String, name: String, symbol: String): UserAssetItemUM = UserAssetItemUM.Single( id = id, - tokenIconUrl = iconUrl, + icon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = com.tangem.core.ui.R.drawable.ic_ethereumpow_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ), tokenName = name, tokenSymbol = symbol, - accountName = accountName, + fiatRate = "$98,765.43", + cryptoBalance = "1.234 $symbol", + fiatBalance = "$121,876.50", + isBalanceHidden = false, onClick = {}, ) @@ -251,13 +253,8 @@ internal object SearchContentPreviewFixtures { ) private fun portfolioTwo(): ImmutableList = persistentListOf( - userAsset(id = "p1", name = "Ethereum", symbol = "ETH", accountName = "Main wallet"), - userAsset( - id = "p2", - name = "Polygon", - symbol = "POL", - accountName = "Account with a long label for preview", - ), + userAsset(id = "p1", name = "Ethereum", symbol = "ETH"), + userAsset(id = "p2", name = "Polygon", symbol = "POL"), ) private fun marketListShort(): ImmutableList = persistentListOf( @@ -390,6 +387,7 @@ private val SearchContentPreviewCallbacks = SearchCallbacks( onClearHintsClick = {}, onTextHintClick = { _ -> }, onResultMarketTokenClick = { _ -> }, + onHistoryTokenClick = { _ -> }, ) /** All [SearchContentPreviewScenario] values for the Preview Parameter dropdown in Android Studio. */ diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt index e5641b2bb8..9535331da7 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchCallbacks.kt @@ -7,4 +7,5 @@ internal data class SearchCallbacks( val onClearHintsClick: () -> Unit, val onTextHintClick: (hint: String) -> Unit, val onResultMarketTokenClick: (MarketsListItemUM) -> Unit, + val onHistoryTokenClick: (MarketsListItemUM) -> Unit, ) \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt index e4628ad30c..ede3decb1b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt @@ -1,8 +1,11 @@ package com.tangem.features.feed.ui.search.state import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.AccountNameUM import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.domain.models.account.CryptoPortfolioIcon import kotlinx.collections.immutable.ImmutableList data class SearchUM( @@ -42,11 +45,45 @@ sealed interface MarketSearchResultUM { data class TextHintItemUM(val text: String) -data class UserAssetItemUM( - val id: String, - val tokenIconUrl: String?, - val tokenName: String, - val tokenSymbol: String, - val accountName: String, - val onClick: () -> Unit, -) \ No newline at end of file +@Immutable +sealed interface UserAssetItemUM { + val id: String + val icon: TangemIconUM + val tokenName: String + val tokenSymbol: String + val onClick: () -> Unit + + data class Single( + override val id: String, + override val icon: TangemIconUM, + override val tokenName: String, + override val tokenSymbol: String, + val fiatRate: String?, + val cryptoBalance: String, + val fiatBalance: String, + val isBalanceHidden: Boolean, + override val onClick: () -> Unit, + ) : UserAssetItemUM + + data class Grouped( + override val id: String, + override val icon: TangemIconUM, + override val tokenName: String, + override val tokenSymbol: String, + val tokensCount: Int, + val totalCryptoBalance: String, + val totalFiatBalance: String, + val isBalanceHidden: Boolean, + val children: ImmutableList, + override val onClick: () -> Unit, + ) : UserAssetItemUM + + data class GroupedChild( + val walletName: String, + val accountName: AccountNameUM, + val accountIcon: CryptoPortfolioIcon.Icon, + val accountColor: CryptoPortfolioIcon.Color, + val cryptoBalance: String, + val fiatBalance: String, + ) +} \ No newline at end of file From 0896f80f5378dd8821ab03f9987f42c6ca3f47a4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Apr 2026 20:43:14 +0500 Subject: [PATCH 05/32] Updated on 2026-08-14 --- .../components/currency/icon/ContentIcon.kt | 2 + .../components/currency/icon/CurrencyIcon.kt | 1 + .../currency/icon/CurrencyIconState.kt | 7 + .../ui/components/tokenlist/TokenListItem.kt | 10 +- .../ui/ds/opportunities/OpportunitiesBG.kt | 1 + .../DefaultTangemPayCryptoCurrencyFactory.kt | 41 +-- .../PaymentAccountStatusValueDMConverter.kt | 19 +- .../tangem/data/pay/di/TangemPayDataModule.kt | 12 + .../pay/entity/TangemPayCurrencyFactory.kt | 49 ++++ .../DefaultPaymentAccountStatusFetcher.kt | 16 +- .../pay/store/PaymentAccountStatusesStore.kt | 5 +- .../account/PaymentAccountStatusValue.kt | 52 +++- .../pay/TangemPayCryptoCurrencyFactory.kt | 2 +- ...ymentAccountCryptoCurrencyStatusUseCase.kt | 34 +++ .../destination/model/SendDestinationModel.kt | 12 +- .../com/tangem/features/swap/SwapComponent.kt | 4 - .../SavedSwapTransactionListConverter.kt | 8 +- features/swap/domain/build.gradle.kts | 1 + .../feature/swap/domain/SwapInteractor.kt | 4 +- .../feature/swap/domain/SwapInteractorImpl.kt | 66 +++-- .../swap/domain/models/ui/SwapState.kt | 2 +- .../models/ui/TokensDataStateExpress.kt | 4 +- .../choosetoken/api/ChooseTokenComponent.kt | 13 +- .../impl/DefaultChooseTokenBridge.kt | 3 +- .../impl/model/ChooseTokenModel.kt | 11 +- .../converters/AccountTokenItemConverter.kt | 62 ++++- .../swap/converters/TokensDataConverter.kt | 9 +- .../tangem/feature/swap/model/SwapModel.kt | 262 +++++++++--------- .../swap/model/SwapProcessDataState.kt | 4 +- .../tangem/feature/swap/ui/StateBuilder.kt | 30 +- 30 files changed, 483 insertions(+), 263 deletions(-) create mode 100644 data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt index 27dceeec39..5927f3e422 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/ContentIcon.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.res.painterResource import com.tangem.core.ui.R import com.tangem.core.ui.components.account.AccountCharIcon import com.tangem.core.ui.components.account.AccountResIcon +import com.tangem.core.ui.components.account.PaymentAccountIcon import com.tangem.core.ui.components.currency.DefaultCurrencyIcon import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme @@ -61,6 +62,7 @@ internal fun ContentIcon( background = icon.background, alpha = alpha, ) + is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(modifier = modifier, size = icon.size) is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon( modifier = modifier, resId = icon.resId, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt index 9ec3a73b8d..f5cb062474 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIcon.kt @@ -62,6 +62,7 @@ fun CurrencyIcon( is CurrencyIconState.FiatIcon, is CurrencyIconState.CustomTokenIcon, is CurrencyIconState.TokenIcon, + is CurrencyIconState.PaymentAccount, is CurrencyIconState.CryptoPortfolio.Icon, is CurrencyIconState.CryptoPortfolio.Letter, -> { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt index 5bdd5f41f4..505e0d5b67 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/currency/icon/CurrencyIconState.kt @@ -88,6 +88,12 @@ sealed class CurrencyIconState { override val topBadgeIconResId: Int? = null } + data class PaymentAccount(val size: AccountIconSize = AccountIconSize.Default) : CurrencyIconState() { + override val isGrayscale: Boolean = false + override val shouldShowCustomBadge: Boolean = false + override val topBadgeIconResId: Int? = null + } + @Immutable sealed class CryptoPortfolio : CurrencyIconState() { override val shouldShowCustomBadge: Boolean = false @@ -155,6 +161,7 @@ sealed class CurrencyIconState { is CryptoPortfolio.Letter -> copy( isGrayscale = isGrayscale, ) + is PaymentAccount, is Loading, is Locked, is Empty, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt index 0c6291b831..7ae2238fb7 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/tokenlist/TokenListItem.kt @@ -2,7 +2,8 @@ package com.tangem.core.ui.components.tokenlist import androidx.compose.animation.* import androidx.compose.animation.SharedTransitionScope.ResizeMode.Companion.scaleToBounds -import androidx.compose.animation.core.* +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon @@ -24,6 +25,7 @@ import com.tangem.core.ui.components.TextShimmer import com.tangem.core.ui.components.account.AccountCharIcon import com.tangem.core.ui.components.account.AccountIconSize import com.tangem.core.ui.components.account.AccountResIcon +import com.tangem.core.ui.components.account.PaymentAccountIcon import com.tangem.core.ui.components.currency.icon.CurrencyIcon import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.fields.SearchBar @@ -98,6 +100,9 @@ fun PortfolioListItem(state: TokensListItemUM.Portfolio, isBalanceHidden: Boolea icon.copy( size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default, ) + is CurrencyIconState.PaymentAccount -> icon.copy( + size = if (isExpanded) AccountIconSize.ExtraSmall else AccountIconSize.Default, + ) else -> icon } @@ -183,7 +188,7 @@ fun PortfolioTokensListItem(state: PortfolioTokensListItemUM, isBalanceHidden: B } } -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun ExpandedPortfolioHeader( state: TokenItemState, @@ -209,6 +214,7 @@ fun ExpandedPortfolioHeader( composables.icon.invoke(Modifier) } else { when (val icon = state.iconState) { + is CurrencyIconState.PaymentAccount -> PaymentAccountIcon(size = icon.size) is CurrencyIconState.CryptoPortfolio.Icon -> AccountResIcon( resId = icon.resId, color = icon.color, 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 ac427c9961..e3750fd53e 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 @@ -169,6 +169,7 @@ private fun BoxScope.CurrencyIconBackgroundLayer(state: CurrencyIconState, blurR blurRadius = blurRadius, ) } + is CurrencyIconState.PaymentAccount -> Unit CurrencyIconState.Loading -> Unit CurrencyIconState.Locked -> Unit } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt index 48607769ea..ca7b011917 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/DefaultTangemPayCryptoCurrencyFactory.kt @@ -7,8 +7,8 @@ import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.core.error.UniversalError import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.network.NetworkFactory +import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.data.pay.util.TangemPayErrorConverter -import com.tangem.domain.card.common.visa.VisaUtilities import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory @@ -16,14 +16,8 @@ import com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TAG = "TangemPay: DefaultTangemPayCryptoCurrencyFactory" -/** - * Custom token parameters. Will be used only for F&F. - */ -private const val TOKEN_ID = "usd-coin" -private const val TOKEN_NAME = "USDC" -private const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" -private const val TOKEN_DECIMALS = 6 +@Deprecated("Use TangemPayCurrencyFactory instead") internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor( excludedBlockchains: ExcludedBlockchains, private val errorConverter: TangemPayErrorConverter, @@ -47,32 +41,11 @@ internal class DefaultTangemPayCryptoCurrencyFactory @Inject constructor( ) cryptoCurrencyFactory.createToken( network = requireNotNull(network), - rawId = CryptoCurrency.RawID(TOKEN_ID), - name = TOKEN_NAME, - symbol = TOKEN_NAME, - contractAddress = TOKEN_CONTRACT_ADDRESS, - decimals = TOKEN_DECIMALS, - ) - }.mapLeft { exception -> - TangemLogger.withTag(TAG).e("Error", exception) - errorConverter.convert(exception) - } - } - - override fun create(userWallet: UserWallet): Either { - return catch { - val network = networkFactory.create( - blockchain = VisaUtilities.visaBlockchain, - extraDerivationPath = null, - userWallet = userWallet, - ) - cryptoCurrencyFactory.createToken( - network = requireNotNull(network), - rawId = CryptoCurrency.RawID(TOKEN_ID), - name = TOKEN_NAME, - symbol = TOKEN_NAME, - contractAddress = TOKEN_CONTRACT_ADDRESS, - decimals = TOKEN_DECIMALS, + rawId = CryptoCurrency.RawID(TangemPayCurrencyFactory.TOKEN_ID), + name = TangemPayCurrencyFactory.TOKEN_NAME, + symbol = TangemPayCurrencyFactory.TOKEN_NAME, + contractAddress = TangemPayCurrencyFactory.TOKEN_CONTRACT_ADDRESS, + decimals = TangemPayCurrencyFactory.TOKEN_DECIMALS, ) }.mapLeft { exception -> TangemLogger.withTag(TAG).e("Error", exception) diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index a42852436a..57eb9b4402 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -1,11 +1,12 @@ package com.tangem.data.pay.converter -import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convert -import com.tangem.data.pay.converter.PaymentAccountStatusValueDMConverter.convertBack +import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.domain.models.StatusSource import com.tangem.domain.models.account.PaymentAccountStatusValue -import com.tangem.utils.converter.TwoWayConverter +import com.tangem.domain.models.wallet.UserWalletId +import javax.inject.Inject +import javax.inject.Singleton /** * Two-way converter between [PaymentAccountStatusValue] and [PaymentAccountStatusValueDM]. @@ -15,10 +16,12 @@ import com.tangem.utils.converter.TwoWayConverter * * [convertBack] maps data model → domain. All restored statuses have [StatusSource.CACHE] as source. */ -internal object PaymentAccountStatusValueDMConverter : - TwoWayConverter { +@Singleton +internal class PaymentAccountStatusValueDMConverter @Inject constructor( + private val tangemPayCurrencyFactory: TangemPayCurrencyFactory, +) { - override fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? { + fun convert(value: PaymentAccountStatusValue): PaymentAccountStatusValueDM? { return when (value) { is PaymentAccountStatusValue.NotCreated -> PaymentAccountStatusValueDM.NotCreated() is PaymentAccountStatusValue.UnderReview -> PaymentAccountStatusValueDM.UnderReview( @@ -60,7 +63,7 @@ internal object PaymentAccountStatusValueDMConverter : } } - override fun convertBack(value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue { + fun convertBack(userWalletId: UserWalletId, value: PaymentAccountStatusValueDM?): PaymentAccountStatusValue { return when (value) { is PaymentAccountStatusValueDM.NotCreated -> PaymentAccountStatusValue.NotCreated is PaymentAccountStatusValueDM.CardIssueFailed -> PaymentAccountStatusValue.Error.CardIssueFailed( @@ -80,6 +83,7 @@ internal object PaymentAccountStatusValueDMConverter : isPinSet = value.isPinSet, fiatBalance = value.fiatBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(), + cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), ) } else { PaymentAccountStatusValue.Loaded( @@ -92,6 +96,7 @@ internal object PaymentAccountStatusValueDMConverter : isPinSet = value.isPinSet, fiatBalance = value.fiatBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(), + cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), ) } is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( 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 3887f44e6f..d697221c1b 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 @@ -6,6 +6,7 @@ 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.converter.PaymentAccountStatusValueDMConverter import com.tangem.data.pay.flow.DefaultPaymentAccountStatusFetcher import com.tangem.data.pay.flow.DefaultPaymentAccountStatusProducer import com.tangem.data.pay.repository.* @@ -24,6 +25,7 @@ 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.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.pay.usecase.ProduceTangemPayInitialDataUseCase import com.tangem.domain.pay.usecase.TangemPayMainScreenCustomerInfoUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase @@ -112,6 +114,7 @@ internal interface TangemPayDataModule { @ApplicationContext context: Context, dispatchers: CoroutineDispatcherProvider, scope: AppCoroutineScope, + converter: PaymentAccountStatusValueDMConverter, ): PaymentAccountStatusesStore { return PaymentAccountStatusesStore( runtimeStore = RuntimeSharedStore(), @@ -124,6 +127,7 @@ internal interface TangemPayDataModule { produceFile = { context.dataStoreFile(fileName = "payment_account_statuses") }, scope = scope, ), + converter = converter, scope = scope, ) } @@ -139,6 +143,14 @@ internal interface TangemPayDataModule { ) {} } + @Provides + @Singleton + fun provideGetTangemPayCryptoCurrencyStatusUseCase( + paymentAccountStatusSupplier: PaymentAccountStatusSupplier, + ): GetPaymentAccountCryptoCurrencyStatusUseCase { + return GetPaymentAccountCryptoCurrencyStatusUseCase(paymentAccountStatusSupplier) + } + @Provides @Singleton fun provideTangemPayMainScreenCustomerInfoUseCase( diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt new file mode 100644 index 0000000000..ede6bba797 --- /dev/null +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/entity/TangemPayCurrencyFactory.kt @@ -0,0 +1,49 @@ +package com.tangem.data.pay.entity + +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.common.currency.CryptoCurrencyFactory +import com.tangem.data.common.network.NetworkFactory +import com.tangem.domain.card.common.visa.VisaUtilities +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.requireUserWalletsSync +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +internal class TangemPayCurrencyFactory @Inject constructor( + excludedBlockchains: ExcludedBlockchains, + private val userWalletsListRepository: UserWalletsListRepository, + private val networkFactory: NetworkFactory, +) { + private val cryptoCurrencyFactory by lazy(mode = LazyThreadSafetyMode.NONE) { + CryptoCurrencyFactory(excludedBlockchains) + } + + fun create(userWalletId: UserWalletId): CryptoCurrency.Token { + val userWallet = userWalletsListRepository.requireUserWalletsSync() + .firstOrNull { it.walletId == userWalletId } + ?: error("User wallet with id $userWalletId not found") + val network = networkFactory.create( + blockchain = VisaUtilities.visaBlockchain, + userWallet = userWallet, + extraDerivationPath = null, + ) + return cryptoCurrencyFactory.createToken( + network = requireNotNull(network), + rawId = CryptoCurrency.RawID(TOKEN_ID), + name = TOKEN_NAME, + symbol = TOKEN_NAME, + contractAddress = TOKEN_CONTRACT_ADDRESS, + decimals = TOKEN_DECIMALS, + ) + } + + companion object { + internal const val TOKEN_ID = "usd-coin" + internal const val TOKEN_NAME = "USDC" + internal const val TOKEN_CONTRACT_ADDRESS = "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359" + internal const val TOKEN_DECIMALS = 6 + } +} \ No newline at end of file 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 index cdd78706de..9e0ae38e91 100644 --- 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 @@ -1,6 +1,7 @@ package com.tangem.data.pay.flow import arrow.core.Either +import com.tangem.data.pay.entity.TangemPayCurrencyFactory import com.tangem.data.pay.store.PaymentAccountStatusesStore import com.tangem.domain.core.utils.catchOn import com.tangem.domain.models.StatusSource @@ -8,6 +9,7 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus +import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.flow.PaymentAccountStatusFetcher import com.tangem.domain.pay.model.CustomerInfo import com.tangem.domain.pay.model.OrderStatus @@ -29,6 +31,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val customerOrderRepository: CustomerOrderRepository, private val deviceSecurity: DeviceSecurityInfoProvider, private val dispatchers: CoroutineDispatcherProvider, + private val tangemPayCurrencyFactory: TangemPayCurrencyFactory, ) : PaymentAccountStatusFetcher { private val logger = TangemLogger.withTag(TAG) @@ -132,7 +135,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( }, ifRight = { customerInfo -> logger.i("proceedWithoutOrder data customerInfo ${account.userWalletId}") - val status = customerInfo.mapToPaymentAccountStatus() + val status = customerInfo.mapToPaymentAccountStatus(account.userWalletId) if (status is PaymentAccountStatusValue.IssuingCard && customerInfo.kycStatus == KycStatus.APPROVED) { // If order id wasn't saved -> start order creation and get customer info onboardingRepository.createOrder(account.userWalletId) @@ -167,7 +170,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( }, ifRight = { customerInfo -> if (customerInfo.kycStatus == KycStatus.REJECTED) { - customerInfo.mapToPaymentAccountStatus() + customerInfo.mapToPaymentAccountStatus(account.userWalletId) } else { PaymentAccountStatusValue.Error.CardIssueFailed( customerId = orderData.customerId, @@ -182,7 +185,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( onboardingRepository.getCustomerInfo(userWalletId = account.userWalletId) .fold( ifLeft = { it.mapToPaymentAccountStatus() }, - ifRight = { customerInfo -> customerInfo.mapToPaymentAccountStatus() }, + ifRight = { it.mapToPaymentAccountStatus(account.userWalletId) }, ) } OrderStatus.UNKNOWN -> PaymentAccountStatusValue.Error.Unavailable @@ -191,7 +194,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } - private fun CustomerInfo.mapToPaymentAccountStatus(): PaymentAccountStatusValue { + private fun CustomerInfo.mapToPaymentAccountStatus(userWalletId: UserWalletId): PaymentAccountStatusValue { val cardInfo = this.cardInfo val productInstance = this.productInstance return if (kycStatus != KycStatus.APPROVED && !customerId.isNullOrEmpty()) { @@ -202,6 +205,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } else if (cardInfo != null && productInstance != null && !customerId.isNullOrEmpty()) { convertToContentState( + userWalletId = userWalletId, productInstance = productInstance, cardInfo = cardInfo, customerId = requireNotNull(customerId) { "CustomerId must not be null" }, @@ -212,10 +216,12 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( } private fun convertToContentState( + userWalletId: UserWalletId, productInstance: CustomerInfo.ProductInstance, cardInfo: CustomerInfo.CardInfo, customerId: String, ): PaymentAccountStatusValue { + val cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId) return when (productInstance.frozenState) { TangemPayCardFrozenState.Frozen -> PaymentAccountStatusValue.Locked( source = StatusSource.ACTUAL, @@ -227,6 +233,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( isPinSet = cardInfo.isPinSet, fiatBalance = cardInfo.fiatBalance, cryptoBalance = cardInfo.cryptoBalance, + cryptoCurrency = cryptoCurrency, ) else -> PaymentAccountStatusValue.Loaded( source = StatusSource.ACTUAL, @@ -238,6 +245,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( isPinSet = cardInfo.isPinSet, fiatBalance = cardInfo.fiatBalance, cryptoBalance = cardInfo.cryptoBalance, + cryptoCurrency = cryptoCurrency, ) } } 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 ec8a48d880..62accbccf7 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 @@ -29,6 +29,7 @@ internal typealias WalletIdWithPaymentStatusDM = Map, private val persistenceDataStore: DataStore, + private val converter: PaymentAccountStatusValueDMConverter, scope: AppCoroutineScope, ) { @@ -39,7 +40,7 @@ internal class PaymentAccountStatusesStore( runtimeStore.store( value = cachedStatuses.mapValues { (rawUserWalletId, statusDM) -> val account = Account.Payment(userWalletId = UserWalletId(rawUserWalletId)) - val statusValue = PaymentAccountStatusValueDMConverter.convertBack(value = statusDM) + val statusValue = converter.convertBack(userWalletId = account.userWalletId, value = statusDM) AccountStatus.Payment(account = account, value = statusValue) }, ) @@ -87,7 +88,7 @@ internal class PaymentAccountStatusesStore( } private suspend fun storeInPersistence(userWalletId: UserWalletId, status: PaymentAccountStatusValue) { - val statusDM = PaymentAccountStatusValueDMConverter.convert(value = status) ?: return + val statusDM = converter.convert(value = status) ?: return persistenceDataStore.updateData { storedStatuses -> storedStatuses.toMutableMap().apply { put(key = userWalletId.stringValue, value = statusDM) diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index fc44ccc105..53beaf23be 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -2,9 +2,13 @@ package com.tangem.domain.models.account 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.kyc.KycStatus +import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.serialization.SerializedBigDecimal import kotlinx.serialization.Serializable +import java.math.BigDecimal /** * Represents the various states a payment account can have, encapsulating different information based on the state. @@ -104,7 +108,29 @@ sealed class PaymentAccountStatusValue { val isPinSet: Boolean, val fiatBalance: FiatBalance, val cryptoBalance: CryptoBalance, - ) : PaymentAccountStatusValue() + val cryptoCurrency: CryptoCurrency.Token, + ) : PaymentAccountStatusValue() { + val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( + currency = cryptoCurrency, + value = CryptoCurrencyStatus.Loaded( + amount = cryptoBalance.balance, + fiatAmount = fiatBalance.availableBalance, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + type = NetworkAddress.Address.Type.Primary, + value = cryptoBalance.depositAddress, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + pendingTransactions = emptySet(), + stakingBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + ), + ) + } /** * Represents a state where the payment account is successfully loaded with complete information. @@ -130,7 +156,29 @@ sealed class PaymentAccountStatusValue { val isPinSet: Boolean, val fiatBalance: FiatBalance, val cryptoBalance: CryptoBalance, - ) : PaymentAccountStatusValue() + val cryptoCurrency: CryptoCurrency.Token, + ) : PaymentAccountStatusValue() { + val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( + currency = cryptoCurrency, + value = CryptoCurrencyStatus.Loaded( + amount = cryptoBalance.balance, + fiatAmount = fiatBalance.availableBalance, + fiatRate = BigDecimal.ONE, + priceChange = BigDecimal.ZERO, + networkAddress = NetworkAddress.Single( + defaultAddress = NetworkAddress.Address( + type = NetworkAddress.Address.Type.Primary, + value = cryptoBalance.depositAddress, + ), + ), + sources = CryptoCurrencyStatus.Sources(), + pendingTransactions = emptySet(), + stakingBalance = null, + yieldSupplyStatus = null, + hasCurrentNetworkTransactions = false, + ), + ) + } /** Represents an error state for the payment account status. */ @Serializable diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt index 1004e00447..31a2537914 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/TangemPayCryptoCurrencyFactory.kt @@ -5,8 +5,8 @@ import com.tangem.core.error.UniversalError import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet +@Deprecated("TangemPayCurrencyFactory") interface TangemPayCryptoCurrencyFactory { fun create(userWallet: UserWallet, chainId: Int): Either - fun create(userWallet: UserWallet): Either } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt new file mode 100644 index 0000000000..fe66dc36f7 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/GetPaymentAccountCryptoCurrencyStatusUseCase.kt @@ -0,0 +1,34 @@ +package com.tangem.domain.pay.usecase + +import arrow.core.Option +import arrow.core.none +import arrow.core.some +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier +import kotlinx.coroutines.flow.firstOrNull + +class GetPaymentAccountCryptoCurrencyStatusUseCase( + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, +) { + + suspend operator fun invoke( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Option> { + val accountStatus = paymentAccountStatusSupplier.invoke(userWalletId).firstOrNull() ?: return none() + val cryptoCurrencyStatus = when (val statusValue = accountStatus.value) { + is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus + is PaymentAccountStatusValue.Locked -> statusValue.cryptoCurrencyStatus + else -> return none() + } + return if (cryptoCurrencyStatus.currency == cryptoCurrency) { + (accountStatus.account to cryptoCurrencyStatus).some() + } else { + none() + } + } +} \ No newline at end of file diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt index 18ff42ff9e..df6dc83590 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/destination/model/SendDestinationModel.kt @@ -19,7 +19,6 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.CryptoCurrencyAddress import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.isLocked -import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase @@ -69,7 +68,6 @@ internal class SendDestinationModel @Inject constructor( private val listenToQrScanningUseCase: ListenToQrScanningUseCase, private val parseQrCodeUseCase: ParseQrCodeUseCase, private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, - private val tangemPayCryptoCurrencyFactory: TangemPayCryptoCurrencyFactory, private val analyticsEventHandler: AnalyticsEventHandler, private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier, ) : Model(), SendDestinationClickIntents { @@ -260,16 +258,16 @@ internal class SendDestinationModel @Inject constructor( private fun AccountStatus.Payment.getDestinationWalletUM(wallet: UserWallet): DestinationWalletUM? { val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: return null - val address = when (val status = this.value) { - is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress - is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress + val (paymentAccountAddress, currency) = when (val status = this.value) { + is PaymentAccountStatusValue.Loaded -> status.cryptoBalance.depositAddress to status.cryptoCurrency + is PaymentAccountStatusValue.Locked -> status.cryptoBalance.depositAddress to status.cryptoCurrency else -> return null } - val currency = tangemPayCryptoCurrencyFactory.create(wallet).getOrNull() ?: return null + return if (contractAddress.equals(currency.contractAddress, true)) { DestinationWalletUM( name = wallet.name, - address = address, + address = paymentAccountAddress, cryptoCurrency = currency, userWalletId = wallet.walletId, account = account, diff --git a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt index ed9144cac3..2d0f54f7b2 100644 --- a/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt +++ b/features/swap/api/src/main/kotlin/com/tangem/features/swap/SwapComponent.kt @@ -2,9 +2,7 @@ package com.tangem.features.swap import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent -import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWalletId import java.math.BigDecimal @@ -17,8 +15,6 @@ interface SwapComponent : ComposableContentComponent { val isInitialReverseOrder: Boolean = false, val screenSource: String, val tangemPayInput: TangemPayInput? = null, - val preselectedToToken: CryptoCurrencyStatus? = null, - val preselectedAccount: Account? = null, ) { data class TangemPayInput( val cryptoAmount: BigDecimal, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt index 2b50f90d14..dba1d47550 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SavedSwapTransactionListConverter.kt @@ -162,8 +162,12 @@ internal class SavedSwapTransactionListConverter( } private fun findAccountByDerivationIndex(accountList: AccountList?, derivationIndex: DerivationIndex?): Account? { - return accountList?.accounts?.asSequence()?.filterIsInstance() - ?.firstOrNull { it.derivationIndex == derivationIndex } + val accounts = accountList?.accounts ?: return null + + return accounts.asSequence() + .filterIsInstance() + .firstOrNull { it.derivationIndex == derivationIndex } + ?: accounts.firstOrNull { it is Account.Payment }.takeIf { derivationIndex == null } } private fun UserTokensResponse.Token.getDerivationIndex(): DerivationIndex? { diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index d0baa14afb..b76d4c6e4c 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(projects.domain.express.models) implementation(projects.domain.account) implementation(projects.domain.account.status) + implementation(projects.domain.visa) implementation(projects.domain.visa.models) implementation(projects.features.swap.domain.api) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 98165323aa..ae3c82c978 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -49,9 +49,9 @@ interface SwapInteractor { @Throws(IllegalStateException::class) suspend fun findBestQuote( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, providers: List, amountToSwap: String, reduceBalanceBy: BigDecimal, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index e44392c352..18dc50d0b6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -31,7 +31,7 @@ import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.express.models.ExpressOperationType 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.account.PaymentAccountStatusValue import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network @@ -134,13 +134,13 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun getAccountCurrencyTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { val walletAccountCurrencyStatuses = singleAccountStatusListSupplier.getSyncOrNull( SingleAccountStatusListProducer.Params(userWalletId), - )?.accountStatuses.orEmpty().filterCryptoPortfolio() + )?.accountStatuses.orEmpty() val walletAccountCurrencyStatusesExceptInitial: Map> = walletAccountCurrencyStatuses.mapNotNull { accountStatus -> val filteredCurrencies = when (accountStatus) { is AccountStatus.CryptoPortfolio -> accountStatus.flattenCurrencies().filterCurrencies(currency) - is AccountStatus.Payment -> TODO("[REDACTED_JIRA]") + is AccountStatus.Payment -> getPaymentAccountCurrencies(accountStatus) } if (filteredCurrencies.isNotEmpty()) { @@ -184,6 +184,16 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + private fun getPaymentAccountCurrencies(accountStatus: AccountStatus.Payment): List { + val currencyStatus = when (val statusValue = accountStatus.value) { + is PaymentAccountStatusValue.Loaded -> statusValue.cryptoCurrencyStatus + is PaymentAccountStatusValue.Locked -> statusValue.cryptoCurrencyStatus + else -> return emptyList() + } + + return listOf(currencyStatus) + } + private fun List.filterCurrencies(currency: CryptoCurrency) = this.filter { status -> val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || status.currency.getContractAddress() != currency.getContractAddress() @@ -207,11 +217,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( tokenInfoForFilter(pair).network == currency.network.backendId } - val accountCurrencyList = cryptoCurrenciesList.mapNotNull { (accountEntry, currencyStatusList) -> - val cryptoPortfolio = accountEntry as? Account.CryptoPortfolio ?: return@mapNotNull null - + val accountCurrencyList = cryptoCurrenciesList.map { (accountEntry, currencyStatusList) -> AccountSwapAvailability( - account = cryptoPortfolio, + account = accountEntry, currencyList = currencyStatusList.map { currencyStatus -> val providers = findProvidersForPair( cryptoCurrencyStatuses = currencyStatus, @@ -314,11 +322,12 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } + @Suppress("LongMethod") override suspend fun findBestQuote( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, providers: List, amountToSwap: String, reduceBalanceBy: BigDecimal, @@ -343,7 +352,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( return providers.associateWith { createEmptyAmountState() } } val amount = SwapAmount(amountDecimal, fromToken.currency.decimals) - val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) + val isBalanceWithoutFeeEnough = when (fromAccount) { + is Account.Payment -> true + else -> isBalanceEnough(fromToken, amount, null) + } val networkId = fromToken.currency.network.backendId when (provider.type) { ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { @@ -397,9 +409,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun manageDex( networkId: String, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, provider: SwapProvider, txFeeSealedState: TxFeeSealedState, amount: SwapAmount, @@ -480,9 +492,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun manageDexSolana( networkId: String, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, provider: SwapProvider, txFeeSealedState: TxFeeSealedState, amount: SwapAmount, @@ -535,9 +547,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun manageCex( networkId: String, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, provider: SwapProvider, amount: SwapAmount, reduceBalanceBy: BigDecimal, @@ -1292,9 +1304,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( amount: SwapAmount, reduceBalanceBy: BigDecimal, fromTokenStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toTokenStatus: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, provider: SwapProvider, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, @@ -1386,9 +1398,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( quoteDataModel: Either, amount: SwapAmount, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, networkId: String, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, @@ -1500,7 +1512,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun createSwapErrorWith( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, amount: SwapAmount, includeFeeInAmount: IncludeFeeInAmount, expressDataError: ExpressDataError, @@ -1690,9 +1702,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( provider: SwapProvider, networkId: String, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: SwapAmount, txFeeSealedState: TxFeeSealedState, expressOperationType: ExpressOperationType, @@ -1834,7 +1846,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun produceDexSwapDataError( error: ExpressDataError, fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, amount: SwapAmount, ): SwapState.SwapError { val rates = getQuotes(fromToken.currency.id) @@ -1931,9 +1943,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun updateBalances( provider: SwapProvider, fromTokenStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toTokenStatus: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, fromTokenAmount: SwapAmount, toTokenAmount: SwapAmount, swapData: SwapDataModel?, @@ -1999,7 +2011,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( private suspend fun updatePermissionState( networkId: String, fromTokenStatus: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, swapAmount: SwapAmount, quotesLoadedState: SwapState.QuotesLoadedState, spenderAddress: String?, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 89f79b2d72..d538156362 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -101,7 +101,7 @@ data class TokenSwapInfo( val tokenAmount: SwapAmount, val amountFiat: BigDecimal, val cryptoCurrencyStatus: CryptoCurrencyStatus, - val account: Account.CryptoPortfolio?, + val account: Account?, ) data class RequestApproveStateData( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index f0587337d6..d2da881490 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -45,13 +45,13 @@ data class CurrenciesGroup( ) data class AccountSwapAvailability( - val account: Account.CryptoPortfolio, + val account: Account, val currencyList: List, ) data class AccountSwapCurrency( val isAvailable: Boolean, - val account: Account.CryptoPortfolio, + val account: Account, val cryptoCurrencyStatus: CryptoCurrencyStatus, val providers: List, ) \ No newline at end of file diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt index e87799d55e..c421ec5874 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/api/ChooseTokenComponent.kt @@ -4,6 +4,7 @@ import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference +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 @@ -23,14 +24,14 @@ internal interface ChooseTokenBridge { val onClose: Channel // todo swap legacy api, remove - val onTokenSelected: Channel> + val onTokenSelected: Channel val onNewTokenAdded: Channel> val searchQueryState: StateFlow val currenciesGroup: Flow - fun onTokenSelected(tokenId: Pair) { - onTokenSelected.trySend(tokenId) + fun onTokenSelected(result: ChooseTokenResultOld) { + onTokenSelected.trySend(result) onSearchQuery("") } @@ -57,6 +58,12 @@ internal interface ChooseTokenBridge { } } +data class ChooseTokenResultOld( + val cryptoCurrencyStatus: CryptoCurrencyStatus, + val account: Account, + val isSearched: Boolean, +) + data class ChooseTokenResult( val currency: CryptoCurrencyStatus, val account: AccountStatus, diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt index 870a5a7abe..a8b7cb6142 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenBridge.kt @@ -4,6 +4,7 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.feature.swap.choosetoken.api.ChooseTokenAnalyticsPayload import com.tangem.feature.swap.choosetoken.api.ChooseTokenBridge import com.tangem.feature.swap.choosetoken.api.ChooseTokenResult +import com.tangem.feature.swap.choosetoken.api.ChooseTokenResultOld import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel.Companion.DEBOUNCE_SEARCH_DELAY import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import dagger.assisted.Assisted @@ -19,7 +20,7 @@ internal class DefaultChooseTokenBridge @AssistedInject constructor( override val onCurrencyChosen: Channel = Channel() - override val onTokenSelected: Channel> = Channel() + override val onTokenSelected: Channel = Channel() override val onNewTokenAdded: Channel> = Channel() override val onClose: Channel = Channel() diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt index ca32af6c5e..0766482b98 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -116,10 +116,13 @@ internal class ChooseTokenModel @Inject constructor( TokensDataConverter( onSearchEntered = { query -> bridge.onSearchQuery(query) }, - onTokenClick = { tokenId -> - val selected = tokenId to ChooseTokenAnalyticsPayload - .IsSearched(isSearchingState) - bridge.onTokenSelected(selected) + onTokenClick = { account, cryptoCurrencyStatus -> + val result = ChooseTokenResultOld( + account = account, + cryptoCurrencyStatus = cryptoCurrencyStatus, + isSearched = searchQueryState.value.isNotEmpty(), + ) + bridge.onTokenSelected(result) }, onAccountClick = { account -> expandedAccountsFlow.update { expandedList -> diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt index 0bb771929f..333bb5087c 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/AccountTokenItemConverter.kt @@ -2,15 +2,21 @@ package com.tangem.feature.swap.converters import com.tangem.common.getTotalCryptoAmount import com.tangem.common.getTotalFiatAmount +import com.tangem.common.ui.R import com.tangem.common.ui.account.AccountCryptoPortfolioItemStateConverter import com.tangem.common.ui.account.TokensListPortfolioItemConverter +import com.tangem.common.ui.account.toUM import com.tangem.common.ui.tokens.TokenItemStateConverter import com.tangem.common.ui.tokens.TokenItemStateConverter.Companion.isFlickering +import com.tangem.core.ui.components.currency.icon.CurrencyIconState import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter import com.tangem.core.ui.components.token.state.TokenItemState +import com.tangem.core.ui.components.token.state.TokenItemState.FiatAmountState import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.pluralReference 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 @@ -29,34 +35,68 @@ internal class AccountTokenItemConverter( private val appCurrency: AppCurrency, private val unavailableErrorText: TextReference, private val expandedAccounts: Map, - private val onTokenItemClick: (String) -> Unit, - private val onAccountItemClick: (Account.CryptoPortfolio) -> Unit, + private val onTokenItemClick: (Account, CryptoCurrencyStatus) -> Unit, + private val onAccountItemClick: (Account) -> Unit, ) : Converter { override fun convert(value: AccountSwapAvailability): TokensListItemUM.Portfolio { - return TokensListPortfolioItemConverter( - tokenItemUM = AccountCryptoPortfolioItemStateConverter( + val headerTokenItemState = when (val account = value.account) { + is Account.CryptoPortfolio -> AccountCryptoPortfolioItemStateConverter( appCurrency = appCurrency, - account = value.account.copy( - cryptoCurrencies = value.currencyList.map { it.cryptoCurrencyStatus.currency }, - ), + account = account.copy(cryptoCurrencies = value.currencyList.map { it.cryptoCurrencyStatus.currency }), onItemClick = onAccountItemClick, ).convert( TotalFiatBalance.Loaded( amount = value.currencyList.sumOf { it.cryptoCurrencyStatus.value.fiatAmount.orZero() }, source = StatusSource.ONLY_CACHE, ), - ), + ) + is Account.Payment -> createPaymentAccountHeaderState(value) + } + return TokensListPortfolioItemConverter( + tokenItemUM = headerTokenItemState, isExpanded = expandedAccounts[value.account.accountId] != false, isCollapsable = true, tokens = value.currencyList.map { accountSwapCurrency -> - createAvailableItemConverter() + createAvailableItemConverter(value.account) .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList(), ).convert(Unit) } - fun createAvailableItemConverter(): TokenItemStateConverter { + private fun createPaymentAccountHeaderState(accountSwapAvailability: AccountSwapAvailability): TokenItemState { + val account = accountSwapAvailability.account + val tokensCount = accountSwapAvailability.currencyList.size + val fiatBalance = + accountSwapAvailability.currencyList.sumOf { it.cryptoCurrencyStatus.value.fiatAmount.orZero() } + return TokenItemState.Content( + id = account.accountId.value, + iconState = CurrencyIconState.PaymentAccount(), + titleState = TokenItemState.TitleState.Content(text = account.accountName.toUM().value), + subtitleState = TokenItemState.SubtitleState.TextContent( + value = pluralReference( + R.plurals.common_tokens_count, + count = tokensCount, + formatArgs = wrappedList(tokensCount), + ), + isAvailable = false, + ), + onItemClick = { onAccountItemClick(account) }, + fiatAmountState = FiatAmountState.Content( + text = fiatBalance.format { + fiat( + fiatCurrencyCode = appCurrency.code, + fiatCurrencySymbol = appCurrency.symbol, + ) + }, + isFlickering = false, + ), + subtitle2State = null, + onItemLongClick = null, + ) + } + + fun createAvailableItemConverter(account: Account): TokenItemStateConverter { return TokenItemStateConverter( appCurrency = appCurrency, subtitleStateProvider = { status -> @@ -70,7 +110,7 @@ internal class AccountTokenItemConverter( fiatAmountStateProvider = { createFiatAmountStateProvider(status = it, appCurrency = appCurrency, isAvailable = true) }, - onItemClick = { account, currencyStatus -> onTokenItemClick(currencyStatus.currency.id.value) }, + onItemClick = { _, currencyStatus -> onTokenItemClick(account, currencyStatus) }, ) } diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 64ba7cf5b0..d06953f1a3 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -5,6 +5,7 @@ import com.tangem.core.ui.extensions.resourceReference import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenListUMData @@ -16,10 +17,10 @@ import kotlinx.collections.immutable.toPersistentList @Suppress("LongParameterList") internal class TokensDataConverter( - private val onSearchEntered: (String) -> Unit, - onTokenClick: (String) -> Unit, - onAccountClick: (Account.CryptoPortfolio) -> Unit, + onTokenClick: (Account, CryptoCurrencyStatus) -> Unit, + onAccountClick: (Account) -> Unit, private val expandedAccounts: Map, + private val onSearchEntered: (String) -> Unit, private val tokensDataState: CurrenciesGroup, private val isBalanceHidden: Boolean, private val isAccountsMode: Boolean, @@ -52,7 +53,7 @@ internal class TokensDataConverter( } else { val tokensList = accountList.flatMap { (_, currencyList) -> currencyList.asSequence().map { accountSwapCurrency -> - accountListItemConverter.createAvailableItemConverter() + accountListItemConverter.createAvailableItemConverter(accountSwapCurrency.account) .convert(accountSwapCurrency.cryptoCurrencyStatus) }.map(TokensListItemUM::Token).toPersistentList() }.toPersistentList() 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 dcdc0a63a5..41203dc1f7 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 @@ -33,7 +33,6 @@ import com.tangem.core.ui.message.EventMessageAction import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.datasource.local.appsflyer.AppsFlyerStore -import com.tangem.domain.account.status.model.AccountCryptoCurrencyStatus import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase @@ -49,12 +48,14 @@ import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.derivationIndex import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.pay.WithdrawalResult +import com.tangem.domain.pay.usecase.GetPaymentAccountCryptoCurrencyStatusUseCase import com.tangem.domain.promo.ShouldShowStoriesUseCase import com.tangem.domain.promo.models.StoryContentIds import com.tangem.domain.settings.usercountry.GetUserCountryUseCase @@ -144,6 +145,7 @@ internal class SwapModel @Inject constructor( private val appsFlyerStore: AppsFlyerStore, private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, private val messageSender: UiMessageSender, + private val paymentAccountCryptoCurrencyStatusUseCase: GetPaymentAccountCryptoCurrencyStatusUseCase, chooseTokenBridgeFactory: ChooseTokenBridge.Factory, giveApprovalFeatureToggles: GiveApprovalFeatureToggles, ) : Model() { @@ -219,8 +221,10 @@ internal class SwapModel @Inject constructor( private val swapRouter: SwapRouter = SwapRouter(router = router) private var userCountry: UserCountry? = null - private var fromAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null - private var toAccountCurrencyStatus: AccountCryptoCurrencyStatus? = null + private var fromAccount: Account? = null + private var toAccount: Account? = null + private var fromAccountStatus: CryptoCurrencyStatus? = null + private var toAccountStatus: CryptoCurrencyStatus? = null /** * If user came from Tangem Pay -> fromAccountCurrencyStatus == null @@ -286,8 +290,12 @@ internal class SwapModel @Inject constructor( .launchIn(modelScope) chooseTokenBridge.onTokenSelected.receiveAsFlow() - .onEach { (addedToken, isSearched) -> - onTokenSelect(addedToken, isSearched.value) + .onEach { result -> + onTokenSelect( + account = result.account, + cryptoCurrencyStatus = result.cryptoCurrencyStatus, + isSearched = result.isSearched, + ) } .launchIn(modelScope) @@ -322,22 +330,31 @@ internal class SwapModel @Inject constructor( userWalletId = userWalletId, currency = initialCurrencyFrom, ).getOrNull() + val fromPaymentAccountStatus = + paymentAccountCryptoCurrencyStatusUseCase(userWalletId, initialCurrencyFrom).getOrNull() val toAccountStatus = initialCurrencyTo?.let { currencyTo -> getAccountCurrencyStatusUseCase.invokeSync( userWalletId = userWalletId, currency = currencyTo, ).getOrNull() } + val toPaymentAccountStatus = initialCurrencyTo?.let { currencyTo -> + paymentAccountCryptoCurrencyStatusUseCase(userWalletId, currencyTo).getOrNull() + } + val fromAccount = fromAccountStatus?.account ?: fromPaymentAccountStatus?.first + val fromStatus = fromAccountStatus?.status ?: fromPaymentAccountStatus?.second - if (fromAccountStatus == null) { + if (fromAccount != null && fromStatus != null) { + this@SwapModel.fromAccount = fromAccount + this@SwapModel.fromAccountStatus = fromStatus + this@SwapModel.toAccount = toAccountStatus?.account ?: toPaymentAccountStatus?.first + this@SwapModel.toAccountStatus = toAccountStatus?.status ?: toPaymentAccountStatus?.second + this@SwapModel.initialFromStatus = fromStatus + this@SwapModel.initialToStatus = toAccountStatus?.status ?: toPaymentAccountStatus?.second + initTokens(isInitiallyReversed) + } else { showAlert() swapRouter.back() - } else { - fromAccountCurrencyStatus = fromAccountStatus - toAccountCurrencyStatus = toAccountStatus - initialFromStatus = fromAccountStatus.status - initialToStatus = toAccountStatus?.status - initTokens(isInitiallyReversed) } } else { val fromStatus = getFromStatus() @@ -405,7 +422,7 @@ internal class SwapModel @Inject constructor( updateTokensState(state) val (selectedCurrency, selectedAccount) = run { - var selectedAccountCurrency = toAccountCurrencyStatus + var selectedAccountCurrency = toAccountStatus if (selectedAccountCurrency == null) { val amountSwapCurrency = swapInteractor.getInitialCurrencyToSwap( @@ -415,14 +432,11 @@ internal class SwapModel @Inject constructor( ) if (amountSwapCurrency != null) { - selectedAccountCurrency = AccountCryptoCurrencyStatus( - account = amountSwapCurrency.account, - status = amountSwapCurrency.cryptoCurrencyStatus, - ) + selectedAccountCurrency = amountSwapCurrency.cryptoCurrencyStatus } } - selectedAccountCurrency?.status to selectedAccountCurrency?.account + selectedAccountCurrency to toAccount } val isApplied = applyInitialTokenChoice( @@ -534,7 +548,7 @@ internal class SwapModel @Inject constructor( private fun applyInitialTokenChoice( state: TokensDataStateExpress, selectedCurrency: CryptoCurrencyStatus?, - selectedAccount: Account.CryptoPortfolio?, + selectedAccount: Account?, isReverseFromTo: Boolean, ): Boolean { // exceptional case @@ -555,9 +569,9 @@ internal class SwapModel @Inject constructor( } val (fromAccount, toAccount) = if (canUseFromAccountCurrencyStatus) { if (isOrderReversed.value) { - selectedAccount to requireNotNull(fromAccountCurrencyStatus).account + selectedAccount to fromAccount } else { - requireNotNull(fromAccountCurrencyStatus).account to selectedAccount + fromAccount to selectedAccount } } else { null to null @@ -600,9 +614,9 @@ internal class SwapModel @Inject constructor( private fun startLoadingQuotes( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, toProvidersList: List, @@ -673,9 +687,9 @@ internal class SwapModel @Inject constructor( private fun loadQuotesTask( fromToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, toToken: CryptoCurrencyStatus, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, amount: String, reduceBalanceBy: BigDecimal, toProvidersList: List, @@ -777,7 +791,7 @@ internal class SwapModel @Inject constructor( selectedFeeType = (getSelectedFee() as? TxFee.Legacy)?.feeType ?: FeeType.NORMAL, isReverseSwapPossible = isReverseSwapPossible(), needApplyFCARestrictions = userCountry.needApplyFCARestrictions(), - hideFee = tangemPayInput?.isWithdrawal == true, + hideFee = isTangemPayWithdrawal(), ) } @@ -971,8 +985,9 @@ internal class SwapModel @Inject constructor( } val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) val fee = getSelectedFee() + val isTangemPayWithdrawal = isTangemPayWithdrawal() - if (fee == null && tangemPayInput?.isWithdrawal != true) { + if (fee == null && !isTangemPayWithdrawal) { TangemLogger.e("onSwapClick: fee is null and isWithdrawal is ${tangemPayInput?.isWithdrawal}") showAlert(resourceReference(R.string.swapping_fee_estimation_error_text)) modelScope.launch { @@ -994,7 +1009,7 @@ internal class SwapModel @Inject constructor( includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount, fee = fee, expressOperationType = ExpressOperationType.SWAP, - isTangemPayWithdrawal = tangemPayInput?.isWithdrawal == true, + isTangemPayWithdrawal = isTangemPayWithdrawal, ) }.onSuccess { swapTransactionState -> when (swapTransactionState) { @@ -1252,11 +1267,12 @@ internal class SwapModel @Inject constructor( } @Suppress("LongMethod") - private fun onTokenSelect(id: String, isSearched: Boolean) { + private fun onTokenSelect(account: Account, cryptoCurrencyStatus: CryptoCurrencyStatus, isSearched: Boolean) { val tokens = dataState.tokensDataState ?: return - val (foundToken, foundAccount) = getSelectedTokenAndAccount(tokens, id) + val foundToken = cryptoCurrencyStatus + val foundAccount = account - foundToken?.currency?.symbol?.let { symbol -> + foundToken.currency.symbol.let { symbol -> analyticsEventHandler.send( SwapEvents.ChooseTokenScreenResult(isTokenChosen = true, token = symbol), ) @@ -1270,108 +1286,90 @@ internal class SwapModel @Inject constructor( ) } - if (foundToken != null) { - val fromToken: CryptoCurrencyStatus - val fromAccount: Account.CryptoPortfolio? - val toToken: CryptoCurrencyStatus - val toAccount: Account.CryptoPortfolio? - if (isOrderReversed.value) { - fromToken = foundToken - fromAccount = foundAccount - toToken = initialFromStatus - toAccount = fromAccountCurrencyStatus?.account + val fromToken: CryptoCurrencyStatus + val fromAccount: Account? + val toToken: CryptoCurrencyStatus + val toAccount: Account? + if (isOrderReversed.value) { + fromToken = foundToken + fromAccount = foundAccount + toToken = initialFromStatus + toAccount = this.fromAccount - val newToken = fromToken.currency as? CryptoCurrency.Coin - if (newToken != null) { - subscribeToCoinBalanceUpdates( - userWalletId = userWalletId, - coin = newToken, - isFromCurrency = true, - ) - } else { - fromTokenBalanceJobHolder.cancel() - } + val newToken = fromToken.currency as? CryptoCurrency.Coin + if (newToken != null) { + subscribeToCoinBalanceUpdates( + userWalletId = userWalletId, + coin = newToken, + isFromCurrency = true, + ) } else { - fromToken = initialFromStatus - fromAccount = fromAccountCurrencyStatus?.account - toToken = foundToken - toAccount = foundAccount - - val newToken = toToken.currency as? CryptoCurrency.Coin - if (newToken != null) { - subscribeToCoinBalanceUpdates( - userWalletId = userWalletId, - coin = newToken, - isFromCurrency = false, - ) - } else { - toTokenBalanceJobHolder.cancel() - } + fromTokenBalanceJobHolder.cancel() } - - if (dataState.fromCryptoCurrency != null && dataState.tokensDataState != null) { - isAmountChangedByUser = true - } - - dataState = dataState.copy( - fromCryptoCurrency = fromToken, - fromAccount = fromAccount, - toCryptoCurrency = toToken, - toAccount = toAccount, - selectedProvider = null, - ) - swapRouter.openScreen(SwapNavScreen.Main) - if (handleSwapNotSupported( - state = tokens, - fromToken = fromToken, - toToken = toToken, - fromAccount = fromAccount, - toAccount = toAccount, - ) - ) { - return - } - modelScope.launch { - TangemLogger.i( - "updateFeePaidCryptoCurrencyFor: id = ${fromToken.currency.id}, " + - "isOrderReversed: ${isOrderReversed.value}", - ) - if ((uiState.sendCardData as? SwapCardState.SwapCardData)?.type is TransactionCardType.ReadOnly) { - uiState = stateBuilder.createInitialLoadingState( - initialCurrencyFrom = fromToken.currency, - initialCurrencyTo = toToken.currency, - fromNetworkInfo = fromToken.currency.getNetworkInfo(), - ) - } - updateFeePaidCryptoCurrencyFor(fromToken) - startLoadingQuotes( - fromToken = fromToken, - fromAccount = fromAccount, - toToken = toToken, - toAccount = toAccount, - amount = lastAmount.value, - reduceBalanceBy = lastReducedBalanceBy.value, - toProvidersList = findSwapProviders(fromToken, toToken), - ) - } - updateTokensState(tokens) - } - } - - private fun getSelectedTokenAndAccount( - tokens: TokensDataStateExpress, - id: String, - ): Pair { - val accountCryptoCurrencyStatus = if (isOrderReversed.value) { - tokens.fromGroup } else { - tokens.toGroup - }.accountCurrencyList.firstNotNullOfOrNull { accountSwapAvailability -> - accountSwapAvailability.currencyList.firstOrNull { accountSwapCurrency -> - accountSwapCurrency.cryptoCurrencyStatus.currency.id.value == id + fromToken = initialFromStatus + fromAccount = this.fromAccount + toToken = foundToken + toAccount = foundAccount + + val newToken = toToken.currency as? CryptoCurrency.Coin + if (newToken != null) { + subscribeToCoinBalanceUpdates( + userWalletId = userWalletId, + coin = newToken, + isFromCurrency = false, + ) + } else { + toTokenBalanceJobHolder.cancel() } } - return accountCryptoCurrencyStatus?.cryptoCurrencyStatus to accountCryptoCurrencyStatus?.account + + if (dataState.fromCryptoCurrency != null && dataState.tokensDataState != null) { + isAmountChangedByUser = true + } + + dataState = dataState.copy( + fromCryptoCurrency = fromToken, + fromAccount = fromAccount, + toCryptoCurrency = toToken, + toAccount = toAccount, + selectedProvider = null, + ) + swapRouter.openScreen(SwapNavScreen.Main) + if (handleSwapNotSupported( + state = tokens, + fromToken = fromToken, + toToken = toToken, + fromAccount = fromAccount, + toAccount = toAccount, + ) + ) { + return + } + modelScope.launch { + TangemLogger.i( + "updateFeePaidCryptoCurrencyFor: id = ${fromToken.currency.id}, " + + "isOrderReversed: ${isOrderReversed.value}", + ) + if ((uiState.sendCardData as? SwapCardState.SwapCardData)?.type is TransactionCardType.ReadOnly) { + uiState = stateBuilder.createInitialLoadingState( + initialCurrencyFrom = fromToken.currency, + initialCurrencyTo = toToken.currency, + fromNetworkInfo = fromToken.currency.getNetworkInfo(), + ) + } + updateFeePaidCryptoCurrencyFor(fromToken) + startLoadingQuotes( + fromToken = fromToken, + fromAccount = fromAccount, + toToken = toToken, + toAccount = toAccount, + amount = lastAmount.value, + reduceBalanceBy = lastReducedBalanceBy.value, + toProvidersList = findSwapProviders(fromToken, toToken), + ) + } + updateTokensState(tokens) } @Suppress("LongMethod", "CyclomaticComplexMethod") @@ -1923,8 +1921,8 @@ internal class SwapModel @Inject constructor( state: TokensDataStateExpress, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, ): Boolean { val selectedCurrency = if (isOrderReversed.value) fromToken else toToken if (isTokenAvailableForSwap(state, selectedCurrency, isOrderReversed.value)) return false @@ -1972,8 +1970,12 @@ internal class SwapModel @Inject constructor( } } + private fun isTangemPayWithdrawal(): Boolean { + return tangemPayInput?.isWithdrawal == true || dataState.fromAccount is Account.Payment + } + private fun List.filterForTangemPayWithdrawal(): List { - return if (tangemPayInput?.isWithdrawal == true) { + return if (isTangemPayWithdrawal()) { filter { it.type == ExchangeProviderType.CEX } } else { this diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt index f352427c56..1c031215dd 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/model/SwapProcessDataState.kt @@ -15,8 +15,8 @@ data class SwapProcessDataState( val fromCryptoCurrency: CryptoCurrencyStatus? = null, val toCryptoCurrency: CryptoCurrencyStatus? = null, val feePaidCryptoCurrency: CryptoCurrencyStatus? = null, - val fromAccount: Account.CryptoPortfolio? = null, - val toAccount: Account.CryptoPortfolio? = null, + val fromAccount: Account? = null, + val toAccount: Account? = null, // Amount from input val amount: String? = null, val reduceBalanceBy: BigDecimal = BigDecimal.ZERO, 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 6f704854d9..0ebbb39c1e 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 @@ -3,6 +3,7 @@ package com.tangem.feature.swap.ui import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.AccountTitleUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter import com.tangem.common.ui.account.toUM @@ -177,8 +178,8 @@ internal class StateBuilder( uiStateHolder: SwapStateHolder, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, mainTokenId: String, ): SwapStateHolder { val canSelectSendToken = mainTokenId != fromToken.currency.id.value @@ -241,8 +242,8 @@ internal class StateBuilder( fromToken: CryptoCurrency, toToken: CryptoCurrency, mainTokenId: String, - fromAccount: Account.CryptoPortfolio?, - toAccount: Account.CryptoPortfolio?, + fromAccount: Account?, + toAccount: Account?, ): SwapStateHolder { val canSelectSendToken = mainTokenId != fromToken.id.value val canSelectReceiveToken = mainTokenId != toToken.id.value @@ -501,7 +502,7 @@ internal class StateBuilder( swapProvider: SwapProvider, fromToken: TokenSwapInfo, toToken: CryptoCurrencyStatus?, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, includeFeeInAmount: IncludeFeeInAmount, expressDataError: ExpressDataError, isReverseSwapPossible: Boolean, @@ -618,7 +619,7 @@ internal class StateBuilder( emptyAmountState: SwapState.EmptyAmountState, fromTokenStatus: CryptoCurrencyStatus, toTokenStatus: CryptoCurrencyStatus?, - toAccount: Account.CryptoPortfolio?, + toAccount: Account?, isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -693,7 +694,7 @@ internal class StateBuilder( amountFormatted: String, amountRaw: String, fromToken: CryptoCurrency, - fromAccount: Account.CryptoPortfolio?, + fromAccount: Account?, minTxAmount: BigDecimal?, ): SwapStateHolder { if (uiState.sendCardData !is SwapCardState.SwapCardData) return uiState @@ -1327,30 +1328,37 @@ internal class StateBuilder( return FCA_RESTRICTED_PROVIDER_IDS.contains(providerId) } - private fun getFromCardAccountTitle(fromAccount: Account.CryptoPortfolio?): AccountTitleUM { + private fun getFromCardAccountTitle(fromAccount: Account?): AccountTitleUM { return if (fromAccount != null && isAccountsModeProvider()) { AccountTitleUM.Account( prefixText = resourceReference(R.string.common_from), name = fromAccount.accountName.toUM().value, - icon = CryptoPortfolioIconConverter.convert(fromAccount.icon), + icon = fromAccount.toIconUM(), ) } else { AccountTitleUM.Text(resourceReference(R.string.swapping_from_title)) } } - private fun getToCardAccountTitle(toAccount: Account.CryptoPortfolio?): AccountTitleUM { + private fun getToCardAccountTitle(toAccount: Account?): AccountTitleUM { return if (toAccount != null && isAccountsModeProvider()) { AccountTitleUM.Account( prefixText = resourceReference(R.string.common_to), name = toAccount.accountName.toUM().value, - icon = CryptoPortfolioIconConverter.convert(toAccount.icon), + icon = toAccount.toIconUM(), ) } else { AccountTitleUM.Text(resourceReference(R.string.swapping_to_title)) } } + private fun Account.toIconUM(): AccountIconUM { + return when (this) { + is Account.CryptoPortfolio -> CryptoPortfolioIconConverter.convert(icon) + is Account.Payment -> AccountIconUM.Payment + } + } + private fun getChangeCardsButtonState(isReverseSwapPossible: Boolean) = if (isReverseSwapPossible) { ChangeCardsButtonState.ENABLED } else { From f57a7ecac9b5c470067a0e7aa41a57bef1eed024 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 30 Mar 2026 15:11:36 +0400 Subject: [PATCH 06/32] Updated on 2026-08-14 --- .../com/tangem/tap/common/redux/AppState.kt | 4 - .../tap/common/redux/global/GlobalAction.kt | 6 -- .../common/redux/global/GlobalMiddleware.kt | 43 ---------- .../tap/common/redux/global/GlobalReducer.kt | 7 -- .../tap/common/redux/global/GlobalState.kt | 2 - .../common/redux/legacy/LegacyMiddleware.kt | 79 ------------------- .../features/details/redux/DetailsAction.kt | 8 -- .../details/redux/DetailsMiddleware.kt | 6 +- .../features/details/redux/DetailsReducer.kt | 16 +--- .../features/details/redux/DetailsState.kt | 3 - .../ui/appsettings/model/AppSettingsModel.kt | 6 +- .../com/tangem/domain/redux/LegacyAction.kt | 8 -- .../features/details/model/DetailsModel.kt | 10 --- 13 files changed, 6 insertions(+), 192 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt delete mode 100644 domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.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 89851458ac..a6275955a1 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 @@ -1,8 +1,6 @@ package com.tangem.tap.common.redux -import com.tangem.tap.common.redux.global.GlobalMiddleware import com.tangem.tap.common.redux.global.GlobalState -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.proxy.redux.DaggerGraphMiddleware @@ -20,12 +18,10 @@ data class AppState( fun getMiddleware(): List> { return listOf( logMiddleware, - GlobalMiddleware.handler, DetailsMiddleware().detailsMiddleware, LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, - LegacyMiddleware.legacyMiddleware, ) } } diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt index a36bd807b6..c446ff5206 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.redux.global -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse import org.rekotlin.Action @@ -8,10 +7,5 @@ sealed class GlobalAction : Action { data class SaveScanResponse(val scanResponse: ScanResponse) : GlobalAction() - data class ChangeAppCurrency(val appCurrency: AppCurrency) : GlobalAction() - object RestoreAppCurrency : GlobalAction() { - data class Success(val appCurrency: AppCurrency) : GlobalAction() - } - data class IsSignWithRing(val isSignWithRing: Boolean) : GlobalAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt deleted file mode 100644 index f2df3df6e9..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.tangem.tap.common.redux.global - -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware - -object GlobalMiddleware { - val handler = globalMiddlewareHandler -} - -private val globalMiddlewareHandler: Middleware = { _, _ -> - { nextDispatch -> - { action -> - handleAction(action) - nextDispatch(action) - } - } -} - -private fun handleAction(action: Action) { - when (action) { - is GlobalAction.RestoreAppCurrency -> restoreAppCurrency() - } -} - -private fun restoreAppCurrency() { - scope.launch { - val currency = store.inject(DaggerGraphState::appCurrencyRepository) - .getSelectedAppCurrency() - .firstOrNull() - ?: AppCurrency.Default - - store.dispatchWithMain(GlobalAction.RestoreAppCurrency.Success(currency)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt index e46630172e..32164696dc 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalReducer.kt @@ -13,13 +13,6 @@ fun globalReducer(action: Action, state: AppState): GlobalState { is GlobalAction.SaveScanResponse -> { globalState.copy(scanResponse = action.scanResponse) } - is GlobalAction.ChangeAppCurrency -> { - globalState.copy(appCurrency = action.appCurrency) - } - is GlobalAction.RestoreAppCurrency.Success -> { - globalState.copy(appCurrency = action.appCurrency) - } is GlobalAction.IsSignWithRing -> globalState.copy(isLastSignWithRing = action.isSignWithRing) - else -> globalState } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt index 01782e2e9a..843d9f2977 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt @@ -1,6 +1,5 @@ package com.tangem.tap.common.redux.global -import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.models.scan.ScanResponse import com.tangem.tap.domain.TapWalletManager import org.rekotlin.StateType @@ -9,7 +8,6 @@ data class GlobalState( @Deprecated("Use scan response from selected user wallet") val scanResponse: ScanResponse? = null, val tapWalletManager: TapWalletManager = TapWalletManager(), - val appCurrency: AppCurrency = AppCurrency.Default, val isLastSignWithRing: Boolean = false, ) : StateType diff --git a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt deleted file mode 100644 index aee323386c..0000000000 --- a/app/src/main/java/com/tangem/tap/common/redux/legacy/LegacyMiddleware.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.tangem.tap.common.redux.legacy - -import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.redux.LegacyAction -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.details.redux.AppSettingsState -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.* -import org.rekotlin.Middleware - -@Suppress("MemberNameEqualsClassName") -internal object LegacyMiddleware { - private val prepareDetailsScreenJobHolder = JobHolder() - - val legacyMiddleware: Middleware = { _, _ -> - { next -> - { action -> - when (action) { - is LegacyAction.PrepareDetailsScreen -> { - selectedUserWallet() - .distinctUntilChanged { old, new -> - if (old is UserWallet.Cold && new is UserWallet.Cold) { - old.walletId == new.walletId && - old.scanResponse == new.scanResponse - } else { - old.walletId == new.walletId - } - } - .onEach { selectedUserWallet -> - val initializedAppSettingsStateContent = initializeAppSettingsState() - store.dispatchWithMain( - DetailsAction.PrepareScreen( - scanResponse = (selectedUserWallet as? UserWallet.Cold)?.scanResponse, - initializedAppSettingsState = initializedAppSettingsStateContent, - ), - ) - } - .flowOn(Dispatchers.IO) - .launchIn(scope) - .saveIn(prepareDetailsScreenJobHolder) - } - } - next(action) - } - } - } - - private fun selectedUserWallet(): Flow { - return store.inject(DaggerGraphState::userWalletsListRepository).selectedUserWallet.filterNotNull() - } - - /** - * LEGACY: We need to initialize [AppSettingsState] async to avoid drawing blocking - * previously it was initialized in runBlocking and blocked details screen - */ - private suspend fun initializeAppSettingsState(): AppSettingsState { - return AppSettingsState( - selectedAppCurrency = store.state.globalState.appCurrency, - selectedThemeMode = store.inject(DaggerGraphState::appThemeModeRepository).getAppThemeMode().firstOrNull() - ?: AppThemeMode.DEFAULT, - requireAccessCode = store.inject(DaggerGraphState::walletsRepository).requireAccessCode(), - useBiometricAuthentication = store.inject(DaggerGraphState::walletsRepository).useBiometricAuthentication(), - isHidingEnabled = store.inject(DaggerGraphState::balanceHidingRepository) - .getBalanceHidingSettings().isHidingEnabledInSettings, - needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, - hasSecuredWallets = store.inject(DaggerGraphState::userWalletsListRepository).hasSecuredWallets(), - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt index 0c647de6c4..072d738717 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt @@ -2,18 +2,12 @@ package com.tangem.tap.features.details.redux import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.models.scan.ScanResponse import kotlinx.coroutines.CoroutineScope import org.rekotlin.Action @Suppress("BooleanPropertyNaming") sealed class DetailsAction : Action { - data class PrepareScreen( - val scanResponse: ScanResponse?, - val initializedAppSettingsState: AppSettingsState, - ) : DetailsAction() - sealed class AppSettings : DetailsAction() { data class SwitchPrivacySetting( val enable: Boolean, @@ -50,6 +44,4 @@ sealed class DetailsAction : Action { data class Prepare(val state: AppSettingsState) : AppSettings() } - - data class ChangeAppCurrency(val currency: AppCurrency) : DetailsAction() } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt index 724134783f..fa438bf186 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt @@ -12,7 +12,6 @@ import com.tangem.tap.common.analytics.events.Settings import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.inject import com.tangem.tap.common.redux.AppState -import com.tangem.tap.common.redux.global.GlobalAction import com.tangem.tap.features.demo.DemoHelper import com.tangem.tap.proxy.redux.DaggerGraphState import com.tangem.tap.scope @@ -78,10 +77,7 @@ class DetailsMiddleware { is DetailsAction.AppSettings.ChangeBalanceHiding -> { changeBalanceHiding(action.shouldHideBalance) } - is DetailsAction.AppSettings.ChangeAppCurrency -> { - store.dispatch(GlobalAction.ChangeAppCurrency(action.currency)) - store.dispatch(DetailsAction.ChangeAppCurrency(action.currency)) - } + is DetailsAction.AppSettings.ChangeAppCurrency, is DetailsAction.AppSettings.SwitchPrivacySetting.Success, is DetailsAction.AppSettings.SwitchPrivacySetting.Failure, is DetailsAction.AppSettings.BiometricsStatusChanged, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt index a01428dbfd..40549d69f4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt @@ -12,27 +12,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState { if (action !is DetailsAction) return state.detailsState val detailsState = state.detailsState return when (action) { - is DetailsAction.PrepareScreen -> { - handlePrepareScreen(action) - } is DetailsAction.AppSettings -> { handlePrivacyAction(action, detailsState) } - is DetailsAction.ChangeAppCurrency -> detailsState.copy( - appSettingsState = detailsState.appSettingsState.copy( - selectedAppCurrency = action.currency, - ), - ) } } -private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState { - return DetailsState( - scanResponse = action.scanResponse, - appSettingsState = action.initializedAppSettingsState, - ) -} - @Suppress("LongMethod", "CyclomaticComplexMethod") private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState { return when (action) { @@ -94,6 +79,7 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail useBiometricAuthentication = action.state.useBiometricAuthentication, requireAccessCode = action.state.requireAccessCode, hasSecuredWallets = action.state.hasSecuredWallets, + needEnrollBiometrics = action.state.needEnrollBiometrics, ), ) is DetailsAction.AppSettings.EnrollBiometrics, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt index 83cf685304..e209c707e4 100644 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt @@ -2,12 +2,9 @@ package com.tangem.tap.features.details.redux import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.models.scan.ScanResponse import org.rekotlin.StateType data class DetailsState( - @Deprecated("Delete after onboarding refactoring") - val scanResponse: ScanResponse? = null, val appSettingsState: AppSettingsState = AppSettingsState(), ) : StateType diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index 3f7613c86c..e65c6560da 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -14,6 +14,7 @@ import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.sdk.api.TangemSdkManager import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings @@ -54,6 +55,7 @@ internal class AppSettingsModel @Inject constructor( private val analyticsEventHandler: AnalyticsEventHandler, private val appThemeModeRepository: AppThemeModeRepository, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, + private val tangemSdkManager: TangemSdkManager, private val uiMessageSender: UiMessageSender, ) : Model(), StoreSubscriber { @@ -251,9 +253,8 @@ internal class AppSettingsModel @Inject constructor( private fun bootstrapAppCurrencyUpdates() { appCurrencyRepository .getSelectedAppCurrency() + .distinctUntilChanged() .onEach { appCurrency -> - if (appCurrency.code == store.state.globalState.appCurrency.code) return@onEach - store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(appCurrency)) } .launchIn(scope) @@ -267,6 +268,7 @@ internal class AppSettingsModel @Inject constructor( isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, + needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(), ) diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt deleted file mode 100644 index a9d9660b18..0000000000 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/LegacyAction.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.domain.redux - -import org.rekotlin.Action - -sealed interface LegacyAction : Action { - - data object PrepareDetailsScreen : LegacyAction -} \ No newline at end of file diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt index a4d4c58b0a..e501fd3751 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/model/DetailsModel.kt @@ -22,8 +22,6 @@ import com.tangem.domain.feedback.repository.FeedbackFeatureToggles import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.pay.TangemPayEligibilityManager import com.tangem.domain.pay.model.TangemPayEntryPoint -import com.tangem.domain.redux.LegacyAction -import com.tangem.domain.redux.ReduxStateHolder import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase @@ -61,7 +59,6 @@ internal class DetailsModel @Inject constructor( private val router: Router, private val urlOpener: UrlOpener, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, - private val appStateHolder: ReduxStateHolder, private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase, private val getTangemPayCustomerIdUseCase: GetTangemPayCustomerIdUseCase, private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase, @@ -79,9 +76,6 @@ internal class DetailsModel @Inject constructor( val state: MutableStateFlow init { - // Use to save compatibility with screens that using Redux states - bootstrapScreenState() - val isWalletConnectAvailable = runBlocking { // danger region, this works immediately, but will be refactored later with WC checkIsWalletConnectAvailableUseCase(params.userWalletId).getOrElse { throwable -> @@ -122,10 +116,6 @@ internal class DetailsModel @Inject constructor( .launchIn(modelScope) } - private fun bootstrapScreenState() { - appStateHolder.dispatch(LegacyAction.PrepareDetailsScreen) - } - private fun sendFeedback() { modelScope.launch { val userWallets = getWalletsUseCase.invokeSync() From f6b0bf8dd57d06e5d1f02f73b524bf13da366c46 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Apr 2026 19:51:04 +0400 Subject: [PATCH 07/32] Updated on 2026-08-14 --- .../NavigationButtonsState.kt | 2 ++ .../di/StakingBalanceSupplierModule.kt | 10 ++++---- domain/staking/detekt-baseline-debug.xml | 13 ---------- .../FetchStakingYieldBalanceUseCase.kt | 6 ++--- ...GetConstructedStakingTransactionUseCase.kt | 7 +++++- .../InvalidatePendingTransactionsUseCase.kt | 10 ++++---- .../analytics/StakingAnalyticsEvent.kt | 2 +- .../multi/MultiStakingBalanceSupplier.kt | 2 +- .../single/SingleStakingBalanceSupplier.kt | 2 +- .../staking/impl/detekt-baseline-debug.xml | 25 ------------------- .../deeplink/DefaultStakingDeepLinkHandler.kt | 10 ++++---- .../presentation/model/StakingClickIntents.kt | 2 ++ .../state/StakingStateController.kt | 8 +++--- .../impl/presentation/state/StakingUiState.kt | 19 +++----------- .../previewdata/InitialStakingStatePreview.kt | 2 +- .../SetButtonsStateTransformer.kt | 4 +-- .../SetInitialDataStateTransformer.kt | 2 +- .../AmountCurrencyChangeStateTransformer.kt | 4 +-- ...pprovalBottomSheetInProgressTransformer.kt | 2 +- ...pprovalBottomSheetTypeChangeTransformer.kt | 2 +- .../AddStakingNotificationsTransformer.kt | 16 ++++++------ .../StakingInfoNotificationsFactory.kt | 8 +++--- .../ui/StakingInitialInfoContent.kt | 24 ++++++++---------- .../presentation/ui/block/StakingFeeBlock.kt | 22 ++++++++-------- 24 files changed, 80 insertions(+), 124 deletions(-) delete mode 100644 domain/staking/detekt-baseline-debug.xml delete mode 100644 features/staking/impl/detekt-baseline-debug.xml diff --git a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt index 8b1bebbcd4..182696bedf 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/navigationButtons/NavigationButtonsState.kt @@ -1,8 +1,10 @@ package com.tangem.common.ui.navigationButtons import androidx.annotation.DrawableRes +import androidx.compose.runtime.Immutable import com.tangem.core.ui.extensions.TextReference +@Immutable sealed class NavigationButtonsState { data object Empty : NavigationButtonsState() diff --git a/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceSupplierModule.kt b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceSupplierModule.kt index 4ddab5988c..8937c4ec40 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceSupplierModule.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/di/StakingBalanceSupplierModule.kt @@ -8,11 +8,11 @@ import com.tangem.data.staking.store.StakeKitBalancesStore import com.tangem.datasource.api.ethpool.models.response.P2PEthPoolAccountResponse import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeSharedStore -import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.staking.multi.MultiStakingBalanceProducer import com.tangem.domain.staking.multi.MultiStakingBalanceSupplier import com.tangem.domain.staking.single.SingleStakingBalanceProducer import com.tangem.domain.staking.single.SingleStakingBalanceSupplier +import com.tangem.utils.coroutines.AppCoroutineScope import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -54,7 +54,7 @@ internal object StakingBalanceSupplierModule { fun provideSingleStakingBalanceSupplier( factory: SingleStakingBalanceProducer.Factory, ): SingleStakingBalanceSupplier { - return object : SingleStakingBalanceSupplier( + return SingleStakingBalanceSupplier( factory = factory, keyCreator = { params -> listOf( @@ -65,15 +65,15 @@ internal object StakingBalanceSupplierModule { ) .joinToString(separator = "_") }, - ) {} + ) } @Provides @Singleton fun provideMultiStakingBalanceSupplier(factory: MultiStakingBalanceProducer.Factory): MultiStakingBalanceSupplier { - return object : MultiStakingBalanceSupplier( + return MultiStakingBalanceSupplier( factory = factory, keyCreator = { "multi_staking_balances_${it.userWalletId.stringValue}" }, - ) {} + ) } } \ No newline at end of file diff --git a/domain/staking/detekt-baseline-debug.xml b/domain/staking/detekt-baseline-debug.xml deleted file mode 100644 index f52428665c..0000000000 --- a/domain/staking/detekt-baseline-debug.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - MultilineLambdaItParameter:FetchStakingYieldBalanceUseCase.kt$FetchStakingYieldBalanceUseCase${ when (it) { is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } return@either } - MultilineLambdaItParameter:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase${ !it.isPending && action.amount < it.amount && it.type == BalanceType.STAKED && it.validatorAddress == action.validatorAddress } - NamedArguments:GetConstructedStakingTransactionUseCase.kt$GetConstructedStakingTransactionUseCase$constructTransaction(networkId, fee, amount, transactionId) - UnnecessaryAbstractClass:MultiStakingBalanceSupplier.kt$MultiStakingBalanceSupplier$MultiStakingBalanceSupplier - UnnecessaryAbstractClass:SingleStakingBalanceSupplier.kt$SingleStakingBalanceSupplier$SingleStakingBalanceSupplier - UseEmptyCounterpart:StakingAnalyticsEvent.kt$StakingAnalyticsEvent$mapOf() - UseOrEmpty:InvalidatePendingTransactionsUseCase.kt$InvalidatePendingTransactionsUseCase$action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "" - - diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt index a438d88cef..2e4836733e 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/FetchStakingYieldBalanceUseCase.kt @@ -23,9 +23,9 @@ class FetchStakingYieldBalanceUseCase( currencyId = cryptoCurrency.id, network = cryptoCurrency.network, ) - .getOrElse { - when (it) { - is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$it")) + .getOrElse { error -> + when (error) { + is StakingIdFactory.Error.UnableToGetAddress -> raise(StakingError.DomainError("$error")) StakingIdFactory.Error.UnsupportedCurrency -> Unit.right() } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt index 3402b6bb03..7242d70f51 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt @@ -20,7 +20,12 @@ class GetConstructedStakingTransactionUseCase( amount: Amount, transactionId: String, ): Either> = Either.catch { - stakeKitRepository.constructTransaction(networkId, fee, amount, transactionId) + stakeKitRepository.constructTransaction( + networkId = networkId, + fee = fee, + amount = amount, + transactionId = transactionId, + ) }.mapLeft { stakingErrorResolver.resolve(it) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt index f24cac1f74..a58ad4c346 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/InvalidatePendingTransactionsUseCase.kt @@ -100,7 +100,7 @@ class InvalidatePendingTransactionsUseCase( type = BalanceType.STAKED, amount = action.amount, rawCurrencyId = null, - validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0) ?: "", + validatorAddress = action.validatorAddress ?: action.validatorAddresses?.getOrNull(0).orEmpty(), date = null, pendingActions = emptyList(), pendingActionsConstraints = emptyList(), @@ -149,10 +149,10 @@ class InvalidatePendingTransactionsUseCase( } private fun findPartialUnstake(balances: MutableList, action: StakingAction): Pair { - val index = balances.indexOfFirst { - !it.isPending && action.amount < it.amount && - it.type == BalanceType.STAKED && - it.validatorAddress == action.validatorAddress + val index = balances.indexOfFirst { balance -> + !balance.isPending && action.amount < balance.amount && + balance.type == BalanceType.STAKED && + balance.validatorAddress == action.validatorAddress } return index to action.amount } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt index cdf1a16bad..3b67c72c6f 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/analytics/StakingAnalyticsEvent.kt @@ -8,7 +8,7 @@ import com.tangem.domain.models.staking.action.StakingActionType sealed class StakingAnalyticsEvent( event: String, - params: Map = mapOf(), + params: Map = emptyMap(), ) : AnalyticsEvent( category = "Staking", event = event, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt index 106e390f01..7932714729 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/multi/MultiStakingBalanceSupplier.kt @@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance * [REDACTED_AUTHOR] */ -abstract class MultiStakingBalanceSupplier( +open class MultiStakingBalanceSupplier( override val factory: FlowProducer.Factory, override val keyCreator: (MultiStakingBalanceProducer.Params) -> String, ) : FlowCachingSupplier>() \ No newline at end of file diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt index 9474e0e172..a9050acd21 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/single/SingleStakingBalanceSupplier.kt @@ -12,7 +12,7 @@ import com.tangem.domain.models.staking.StakingBalance * [REDACTED_AUTHOR] */ -abstract class SingleStakingBalanceSupplier( +open class SingleStakingBalanceSupplier( override val factory: FlowProducer.Factory, override val keyCreator: (SingleStakingBalanceProducer.Params) -> String, ) : FlowCachingSupplier() \ No newline at end of file diff --git a/features/staking/impl/detekt-baseline-debug.xml b/features/staking/impl/detekt-baseline-debug.xml deleted file mode 100644 index 8cd2dca55b..0000000000 --- a/features/staking/impl/detekt-baseline-debug.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - BooleanPropertyNaming:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer$val showNotification = sendingAmount + feeAmount > balance - BooleanPropertyNaming:AmountCurrencyChangeStateTransformer.kt$AmountCurrencyChangeStateTransformer$private val value: Boolean - BooleanPropertyNaming:StakingUiState.kt$StakingStates.InitialInfoState.Data$val showBanner: Boolean - BooleanPropertyNaming:StakingUiState.kt$StakingUiState$val showColdWalletInteractionIcon: Boolean - CastNullableToNonNullableType:SetApprovalBottomSheetInProgressTransformer.kt$SetApprovalBottomSheetInProgressTransformer$as - CastNullableToNonNullableType:SetApprovalBottomSheetTypeChangeTransformer.kt$SetApprovalBottomSheetTypeChangeTransformer$as - MultilineLambdaItParameter:AddStakingNotificationsTransformer.kt$AddStakingNotificationsTransformer${ it is StakingNotification.Error || it is NotificationUM.Error || it is NotificationUM.Warning.NetworkFeeUnreachable || it is StakingNotification.Warning.TransactionInProgress || it is StakingNotification.Warning.InitializeTonAccount } - MultilineLambdaItParameter:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler${ val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true isNetwork && isCurrency } - MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Error) { Text( text = DASH_SIGN, color = TangemTheme.colors.text.primary1, style = TangemTheme.typography.body1, ) } } - MultilineLambdaItParameter:StakingFeeBlock.kt${ if (it == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( height = TangemTheme.dimens.size24, width = TangemTheme.dimens.size90, ), ) } } - MultilineLambdaItParameter:StakingInfoNotificationsFactory.kt$StakingInfoNotificationsFactory${ it.type == BalanceType.PREPARING || it.type == BalanceType.STAKED || it.type == BalanceType.LOCKED } - MultilineLambdaItParameter:StakingStateController.kt$StakingStateController${ it.copy( showColdWalletInteractionIcon = userWallet is UserWallet.Cold, ) } - NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$networkId - NullableToStringCall:DefaultStakingDeepLinkHandler.kt$DefaultStakingDeepLinkHandler$$tokenId - PropertyUsedBeforeDeclaration:StakingFeeBlock.kt$FeeBlockPreviewProvider$contentState - PropertyUsedBeforeDeclaration:StakingStateController.kt$StakingStateController$uiState - UnnecessaryEventHandlerParameter:StakingInitialInfoContent.kt$onClick: (BalanceState) -> Unit - UnnecessaryLet:StakingTosText.kt$let { onTextClick(PRIVACY_POLICY_URL) } - UnnecessaryLet:StakingTosText.kt$let { onTextClick(TERMS_OF_USE_URL) } - - diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt index c861dfb887..a12c16b790 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -53,9 +53,9 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( params = MultiWalletCryptoCurrenciesProducer.Params(selectedUserWalletId), ) .orEmpty() - .firstOrNull { - val isNetwork = it.network.backendId.equals(networkId, ignoreCase = true) - val isCurrency = it.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true + .firstOrNull { currency -> + val isNetwork = currency.network.backendId.equals(networkId, ignoreCase = true) + val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true isNetwork && isCurrency } @@ -63,8 +63,8 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( TangemLogger.e( """ Could not get crypto currency for - |- $NETWORK_ID_KEY: $networkId - |- $TOKEN_ID_KEY: $tokenId + |- $NETWORK_ID_KEY: ${networkId.orEmpty()} + |- $TOKEN_ID_KEY: ${tokenId.orEmpty()} """.trimIndent(), ) return@launch diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingClickIntents.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingClickIntents.kt index 66b3ccedc6..9ae89b73e3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingClickIntents.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/model/StakingClickIntents.kt @@ -1,5 +1,6 @@ package com.tangem.features.staking.impl.presentation.model +import androidx.compose.runtime.Immutable import com.tangem.common.ui.amountScreen.AmountScreenClickIntents import com.tangem.common.ui.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.notifications.NotificationUM @@ -11,6 +12,7 @@ import java.math.BigDecimal // TODO split this interface to click intents and other interaction events @Suppress("TooManyFunctions") +@Immutable internal interface StakingClickIntents : AmountScreenClickIntents { fun onBackClick() 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 7a02bd36ef..32844ee371 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 @@ -26,19 +26,19 @@ internal class StakingStateController @Inject constructor( private val holdToConfirmButtonFeatureToggles: HoldToConfirmButtonFeatureToggles, ) { - val value: StakingUiState get() = uiState.value - private val mutableUiState: MutableStateFlow = MutableStateFlow(value = getInitialState()) val uiState: StateFlow get() = mutableUiState.asStateFlow() + val value: StakingUiState get() = uiState.value + private val buttonsTransformer = SetButtonsStateTransformer(urlOpener) private val titleTransformer = SetTitleTransformer fun initializeWithUserWallet(userWallet: UserWallet) { mutableUiState.update { state -> state.copy( - showColdWalletInteractionIcon = userWallet.isColdWallet, + isColdWalletInteractionIconVisible = userWallet.isColdWallet, shouldShowHoldToConfirmButton = holdToConfirmButtonFeatureToggles.isHoldToConfirmEnabled && userWallet.isHotWallet, ) @@ -89,7 +89,7 @@ internal class StakingStateController @Inject constructor( actionType = StakingActionCommonType.Enter(skipEnterAmount = false), buttonsState = NavigationButtonsState.Empty, balanceState = null, - showColdWalletInteractionIcon = true, + isColdWalletInteractionIconVisible = true, shouldShowHoldToConfirmButton = false, ) } 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 8624ce9a72..f9a8b1cf23 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 @@ -39,22 +39,9 @@ internal data class StakingUiState( val actionType: StakingActionCommonType, val buttonsState: NavigationButtonsState, val balanceState: BalanceState?, - val showColdWalletInteractionIcon: Boolean, + val isColdWalletInteractionIconVisible: Boolean, val shouldShowHoldToConfirmButton: Boolean, -) { - - fun copyWrapped( - initialInfoState: StakingStates.InitialInfoState = this.initialInfoState, - amountState: AmountState = this.amountState, - confirmationState: StakingStates.ConfirmationState = this.confirmationState, - validatorState: StakingStates.ValidatorState = this.validatorState, - ): StakingUiState = copy( - initialInfoState = initialInfoState, - amountState = amountState, - confirmationState = confirmationState, - validatorState = validatorState, - ) -} +) internal sealed class StakingStates { @@ -64,7 +51,7 @@ internal sealed class StakingStates { sealed class InitialInfoState : StakingStates() { data class Data( override val isPrimaryButtonEnabled: Boolean, - val showBanner: Boolean, + val isBannerVisible: Boolean, val infoItems: ImmutableList, val onInfoClick: (InfoType) -> Unit, val yieldBalance: InnerYieldBalanceState, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt index c54d001d68..ba083ad159 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt @@ -18,7 +18,7 @@ import kotlinx.collections.immutable.persistentListOf internal object InitialStakingStatePreview { val defaultState = StakingStates.InitialInfoState.Data( isPrimaryButtonEnabled = true, - showBanner = true, + isBannerVisible = true, infoItems = persistentListOf( RoundedListWithDividersItemData( id = R.string.staking_details_available, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt index 725c5869e8..b67b009796 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetButtonsStateTransformer.kt @@ -37,7 +37,7 @@ internal class SetButtonsStateTransformer( return prevState.copy(buttonsState = buttonsState) } - private fun getPrimaryButton(prevState: StakingUiState): NavigationButton? { + private fun getPrimaryButton(prevState: StakingUiState): NavigationButton { val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data val innerConfirmState = confirmState?.innerState @@ -52,7 +52,7 @@ internal class SetButtonsStateTransformer( val isPrimaryButtonDisabled = prevState.isPrimaryButtonDisabled() return NavigationButton( textReference = prevState.getButtonText(), - iconRes = R.drawable.ic_tangem_24.takeIf { prevState.showColdWalletInteractionIcon }, + iconRes = R.drawable.ic_tangem_24.takeIf { prevState.isColdWalletInteractionIconVisible }, isDimmed = isPrimaryButtonDisabled, isIconVisible = isIconVisible, shouldShowProgress = isInProgress, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt index 1dbb49e096..c6c30d4bcb 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt @@ -96,7 +96,7 @@ internal class SetInitialDataStateTransformer( isPrimaryButtonEnabled = with(status) { !amount.isNullOrZero() && sources.stakingBalanceSource.isActual() && sources.networkSource.isActual() }, - showBanner = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty, + isBannerVisible = !isAnyTokenStaked && yieldBalance == InnerYieldBalanceState.Empty, infoItems = getInfoItems(), onInfoClick = clickIntents::onInfoClick, yieldBalance = yieldBalance, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt index 9631543416..5c121f20e1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/amount/AmountCurrencyChangeStateTransformer.kt @@ -7,11 +7,11 @@ import com.tangem.utils.transformer.Transformer internal class AmountCurrencyChangeStateTransformer( private val cryptoCurrencyStatus: CryptoCurrencyStatus, - private val value: Boolean, + private val isFiatValue: Boolean, ) : Transformer { override fun transform(prevState: StakingUiState): StakingUiState { return prevState.copy( - amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState), + amountState = AmountCurrencyTransformer(cryptoCurrencyStatus, isFiatValue).transform(prevState.amountState), ) } } \ No newline at end of file diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt index 5221aba998..997a7e6743 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetInProgressTransformer.kt @@ -27,7 +27,7 @@ internal class SetApprovalBottomSheetInProgressTransformer( ), onCancel = onDismiss, ) - } as TangemBottomSheetConfigContent, + } as? TangemBottomSheetConfigContent ?: return prevState, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetTypeChangeTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetTypeChangeTransformer.kt index 0e58d0c7eb..099b77c51a 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetTypeChangeTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/approval/SetApprovalBottomSheetTypeChangeTransformer.kt @@ -16,7 +16,7 @@ internal class SetApprovalBottomSheetTypeChangeTransformer( bottomSheetConfig = prevState.bottomSheetConfig?.copy( content = approvalBottomSheetConfig?.copy( data = approvalBottomSheetConfig.data.copy(approveType = approveType), - ) as TangemBottomSheetConfigContent, + ) as? TangemBottomSheetConfigContent ?: return prevState, ), ) } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 7bb3f0630d..8849435172 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -177,12 +177,12 @@ internal class AddStakingNotificationsTransformer( } private fun isPrimaryButtonEnabled(notifications: ImmutableList, isActualSources: Boolean) = - notifications.none { - it is StakingNotification.Error || - it is NotificationUM.Error || - it is NotificationUM.Warning.NetworkFeeUnreachable || - it is StakingNotification.Warning.TransactionInProgress || - it is StakingNotification.Warning.InitializeTonAccount + notifications.none { notification -> + notification is StakingNotification.Error || + notification is NotificationUM.Error || + notification is NotificationUM.Warning.NetworkFeeUnreachable || + notification is StakingNotification.Warning.TransactionInProgress || + notification is StakingNotification.Warning.InitializeTonAccount } && isActualSources private fun MutableList.addStakingErrorNotifications( @@ -302,8 +302,8 @@ internal class AddStakingNotificationsTransformer( val balance = cryptoCurrencyStatus.value.amount.orZero() if (!isSubtractionAvailable) return - val showNotification = sendingAmount + feeAmount > balance - if (showNotification) { + val isExceedsBalance = sendingAmount + feeAmount > balance + if (isExceedsBalance) { onNotEnoughFeeNotificationShow() val notification = if (actionType is StakingActionCommonType.Enter) { NotificationUM.Error.TotalExceedsBalance diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt index 8628070ef4..02a41fe4a3 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/StakingInfoNotificationsFactory.kt @@ -150,10 +150,10 @@ internal class StakingInfoNotificationsFactory( val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() val isTron = isTron(cryptoCurrencyStatus.currency.network.rawId) val hasStakedBalance = (cryptoCurrencyStatus.value.stakingBalance as? StakingBalance.Data.StakeKit)?.balance - ?.items?.any { - it.type == BalanceType.PREPARING || - it.type == BalanceType.STAKED || - it.type == BalanceType.LOCKED + ?.items?.any { item -> + item.type == BalanceType.PREPARING || + item.type == BalanceType.STAKED || + item.type == BalanceType.LOCKED } == true if (isTron && hasStakedBalance) { add( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt index eb514ea0ba..66cb40cb89 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt @@ -87,7 +87,7 @@ internal fun StakingInitialInfoContent( .background(TangemTheme.colors.background.secondary) .padding(horizontal = TangemTheme.dimens.spacing16), ) { - if (state.showBanner) { + if (state.isBannerVisible) { item(key = BANNER_BLOCK_KEY) { Column( modifier = Modifier.animateItem(), @@ -175,7 +175,7 @@ private fun LazyListScope.activeStakingBlock( ActiveStakingBlock( balance = balance, isBalanceHidden = isBalanceHidden, - onClick = clickIntents::onActiveStake, + onClick = { clickIntents.onActiveStake(balance) }, onAnalytic = clickIntents::onActiveStakeAnalytic, modifier = Modifier .animateItem() @@ -288,7 +288,7 @@ private fun StakingRewardBlock( private fun ActiveStakingBlock( balance: BalanceState, isBalanceHidden: Boolean, - onClick: (BalanceState) -> Unit, + onClick: () -> Unit, onAnalytic: () -> Unit, modifier: Modifier = Modifier, ) { @@ -304,7 +304,7 @@ private fun ActiveStakingBlock( enabled = balance.isClickable, onClick = { onAnalytic() - onClick(balance) + onClick() }, ) .padding(TangemTheme.dimens.spacing12), @@ -351,20 +351,18 @@ private fun ActiveStakingBlock( style = TangemTheme.typography.body1, color = TangemTheme.colors.text.primary1, ) - if (balance.formattedCryptoAmount != null) { - Text( - text = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden).resolveReference(), - style = TangemTheme.typography.caption2, - color = TangemTheme.colors.text.tertiary, - modifier = Modifier.padding(top = TangemTheme.dimens.spacing2), - ) - } + Text( + text = balance.formattedCryptoAmount.orMaskWithStars(isBalanceHidden).resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(top = TangemTheme.dimens.spacing2), + ) } } } @Composable -private fun RowScope.StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) { +private fun StakingBalanceIcon(balance: BalanceState, icon: Int?, iconTint: Color) { if (balance.hasImage() || icon != null) { StakingTargetIcon( image = if (balance.hasImage()) balance.target?.image.toImageReference() else null, diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt index 60ba58a048..8ad250c6d1 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/block/StakingFeeBlock.kt @@ -109,8 +109,8 @@ private fun BoxScope.FeeLoading(feeState: FeeState) { targetState = feeState, label = "Fee Loading State Change", modifier = Modifier.align(Alignment.CenterEnd), - ) { - if (it == FeeState.Loading) { + ) { state -> + if (state == FeeState.Loading) { RectangleShimmer( radius = TangemTheme.dimens.radius3, modifier = Modifier.size( @@ -128,8 +128,8 @@ private fun BoxScope.FeeError(feeState: FeeState) { targetState = feeState, label = "Fee Error State Change", modifier = Modifier.align(Alignment.CenterEnd), - ) { - if (it == FeeState.Error) { + ) { state -> + if (state == FeeState.Error) { Text( text = DASH_SIGN, color = TangemTheme.colors.text.primary1, @@ -151,13 +151,6 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va private class FeeBlockPreviewProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - contentState, - FeeState.Loading, - FeeState.Error, - ) - private val fee = Fee.Common( amount = Amount( currencySymbol = "MATIC", @@ -174,6 +167,13 @@ private class FeeBlockPreviewProvider : PreviewParameterProvider { isFeeApproximate = false, isFeeConvertibleToFiat = true, ) + + override val values: Sequence + get() = sequenceOf( + contentState, + FeeState.Loading, + FeeState.Error, + ) } // endregion \ No newline at end of file From 0b52308ff130ff045b1c3705298b75e90217497e Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 8 Apr 2026 22:03:44 +0300 Subject: [PATCH 08/32] Updated on 2026-08-14 --- .../storybook/entity/StoryBookPage.kt | 21 +- .../storybook/page/badge/TangemBadgeStory.kt | 27 +- .../storybook/page/buttons/ButtonsStory.kt | 140 ++++++++-- .../storybook/page/deviceicon/Build.kt | 9 + .../page/deviceicon/DeviceIconStory.kt | 184 +++++++++++++ .../page/headerrow/TangemHeaderRowStory.kt | 23 ++ .../storybook/page/pagerindicator/Build.kt | 9 + .../TangemPagerIndicatorStory.kt | 142 ++++++++++ .../storybook/page/placeholder/Build.kt | 9 + .../page/placeholder/PlaceholderStory.kt | 254 +++++++++++++++++ .../storybook/page/progress/Build.kt | 9 + .../page/progress/ProgressIndicatorStory.kt | 132 +++++++++ .../presentation/storybook/page/tab/Build.kt | 19 ++ .../storybook/page/tab/TangemTabStory.kt | 127 +++++++++ .../storybook/page/topbar/Build.kt | 20 ++ .../page/topbar/TangemTopBarStory.kt | 259 ++++++++++++++++++ .../storybook/ui/StoryBookListScreen.kt | 12 + .../storybook/ui/StoryBookScreen.kt | 19 ++ 18 files changed, 1394 insertions(+), 21 deletions(-) create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/deviceicon/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/deviceicon/DeviceIconStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/pagerindicator/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/pagerindicator/TangemPagerIndicatorStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/placeholder/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/placeholder/PlaceholderStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/progress/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/progress/ProgressIndicatorStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tab/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tab/TangemTabStory.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/topbar/Build.kt create mode 100644 features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/topbar/TangemTopBarStory.kt 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 bf95156d7d..5bd49223ea 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 @@ -3,6 +3,7 @@ package com.tangem.feature.tester.presentation.storybook.entity import com.tangem.core.ui.ds.badge.TangemBadgeColor import com.tangem.core.ui.ds.field.search.TangemFieldShape import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.ds.topbar.TangemTopBarType internal sealed interface StoryBookPage @@ -64,4 +65,22 @@ internal data class TangemSearchFieldStory( internal data class TypographyStory( val isFontScaleDefault: Boolean, val onFontScaleToggle: () -> Unit, -) : StoryBookPage \ No newline at end of file +) : StoryBookPage + +internal data class TangemTopBarStory( + val selectedType: TangemTopBarType, + val onTypeChange: (TangemTopBarType) -> Unit, +) : StoryBookPage + +internal data class TangemTabStory( + val checkedIndex: Int, + val onCheckedIndexChange: (Int) -> Unit, +) : StoryBookPage + +internal data object TangemPagerIndicatorStory : StoryBookPage + +internal data object PlaceholderStory : StoryBookPage + +internal data object ProgressIndicatorStory : StoryBookPage + +internal data object DeviceIconStory : StoryBookPage \ 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 index c3c6cea2a1..c158b5f7d1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/badge/TangemBadgeStory.kt @@ -139,12 +139,18 @@ private fun BadgeShapeGroup(size: TangemBadgeSize, shape: TangemBadgeShape, colo @Composable private fun ColumnHeaderRow() { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, ) { Spacer(Modifier.width(STATE_LABEL_WIDTH.dp)) Text( - text = "Text + Icon", + text = "Icon Start", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.weight(1f), + ) + Text( + text = "Icon End", style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.weight(1f), @@ -172,7 +178,7 @@ private fun BadgeTypeRow( type: TangemBadgeType, ) { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -196,6 +202,21 @@ private fun BadgeTypeRow( onClick = {}, ) } + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + TangemBadge( + text = stringReference("New"), + tangemIconUM = TangemIconUM.Icon(iconRes = R.drawable.ic_information_24), + size = size, + shape = shape, + color = color, + type = type, + iconPosition = TangemBadgeIconPosition.End, + onClick = {}, + ) + } Box( contentAlignment = Alignment.CenterStart, modifier = Modifier.weight(1f), 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 d1bd85a0e6..f4c2e96d65 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 @@ -32,7 +32,7 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { .background(TangemTheme.colors2.surface.level1), ) { item("primary") { - ButtonSection(title = "Primary") { isEnabled, text, shape -> + ButtonSection(title = "Primary") { isEnabled, isLoading, text, shape, iconPosition -> PrimaryTangemButton( onClick = {}, text = if (text) stringReference("Continue") else null, @@ -46,14 +46,16 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { } }, ), + iconPosition = iconPosition, size = TangemButtonSize.X10, isEnabled = isEnabled, + isLoading = isLoading, shape = shape, ) } } item("secondary") { - ButtonSection(title = "Secondary") { isEnabled, text, shape -> + ButtonSection(title = "Secondary") { isEnabled, isLoading, text, shape, iconPosition -> SecondaryTangemButton( onClick = {}, text = if (text) stringReference("Continue") else null, @@ -67,8 +69,10 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { } }, ), + iconPosition = iconPosition, size = TangemButtonSize.X10, isEnabled = isEnabled, + isLoading = isLoading, shape = shape, ) } @@ -77,7 +81,7 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { ButtonSection( title = "PrimaryInverse", background = TangemTheme.colors2.surface.level2, - ) { isEnabled, text, shape -> + ) { isEnabled, isLoading, text, shape, iconPosition -> PrimaryInverseTangemButton( onClick = {}, text = if (text) stringReference("Continue") else null, @@ -91,14 +95,16 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { } }, ), + iconPosition = iconPosition, size = TangemButtonSize.X10, isEnabled = isEnabled, + isLoading = isLoading, shape = shape, ) } } item("outline") { - ButtonSection(title = "Outline") { isEnabled, text, shape -> + ButtonSection(title = "Outline") { isEnabled, isLoading, text, shape, iconPosition -> OutlineTangemButton( onClick = {}, text = if (text) stringReference("Continue") else null, @@ -112,14 +118,16 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { } }, ), + iconPosition = iconPosition, size = TangemButtonSize.X10, isEnabled = isEnabled, + isLoading = isLoading, shape = shape, ) } } item("accent") { - ButtonSection(title = "Accent") { isEnabled, text, shape -> + ButtonSection(title = "Accent") { isEnabled, isLoading, text, shape, iconPosition -> StatusTangemButton( onClick = {}, text = if (text) stringReference("Continue") else null, @@ -133,14 +141,16 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { } }, ), + iconPosition = iconPosition, size = TangemButtonSize.X10, isEnabled = isEnabled, + isLoading = isLoading, shape = shape, ) } } item("positive") { - ButtonSection(title = "Positive") { isEnabled, text, shape -> + ButtonSection(title = "Positive") { isEnabled, isLoading, text, shape, iconPosition -> StatusTangemButton( onClick = {}, text = if (text) stringReference("Continue") else null, @@ -154,15 +164,17 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { } }, ), + iconPosition = iconPosition, size = TangemButtonSize.X10, type = TangemButtonType.Positive, isEnabled = isEnabled, + isLoading = isLoading, shape = shape, ) } } item("ghost") { - ButtonSection(title = "Ghost") { isEnabled, text, shape -> + ButtonSection(title = "Ghost") { isEnabled, isLoading, text, shape, iconPosition -> GhostTangemButton( onClick = {}, text = if (text) stringReference("Continue") else null, @@ -176,21 +188,74 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) { } }, ), + iconPosition = iconPosition, size = TangemButtonSize.X10, isEnabled = isEnabled, + isLoading = isLoading, shape = shape, ) } } + item("sizes") { + SizeShowcase() + } } } +@Composable +private fun SizeShowcase() { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = "Sizes", + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + TangemButtonSize.entries.forEach { size -> + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = size.name, + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.width(STATE_LABEL_WIDTH.dp), + ) + PrimaryTangemButton( + onClick = {}, + text = stringReference("Button"), + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_tangem_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primaryInverted }, + ), + size = size, + ) + } + } + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + @Composable private fun ButtonSection( title: String, background: Color = TangemTheme.colors2.surface.level1, shapes: List = TangemButtonShape.entries, - button: @Composable (isEnabled: Boolean, text: Boolean, shape: TangemButtonShape) -> Unit, + button: @Composable ( + isEnabled: Boolean, + isLoading: Boolean, + text: Boolean, + shape: TangemButtonShape, + iconPosition: TangemButtonIconPosition, + ) -> Unit, ) { Column( verticalArrangement = Arrangement.spacedBy(8.dp), @@ -205,7 +270,9 @@ private fun ButtonSection( color = TangemTheme.colors.text.primary1, ) shapes.forEach { shape -> - ShapeGroup(shape = shape, button = button) + TangemButtonIconPosition.entries.forEach { iconPosition -> + ShapeGroup(shape = shape, iconPosition = iconPosition, button = button) + } } } HorizontalDivider( @@ -217,17 +284,46 @@ private fun ButtonSection( @Composable private fun ShapeGroup( shape: TangemButtonShape, - button: @Composable (isEnabled: Boolean, text: Boolean, shape: TangemButtonShape) -> Unit, + iconPosition: TangemButtonIconPosition, + button: @Composable ( + isEnabled: Boolean, + isLoading: Boolean, + text: Boolean, + shape: TangemButtonShape, + iconPosition: TangemButtonIconPosition, + ) -> Unit, ) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text( - text = shape.name, + text = "${shape.name} / Icon ${iconPosition.name}", style = TangemTheme.typography.body2, color = TangemTheme.colors.text.secondary, ) ColumnHeaderRow() - StateRow(isEnabled = true, shape = shape, button = button) - StateRow(isEnabled = false, shape = shape, button = button) + StateRow( + label = "Enabled", + isEnabled = true, + isLoading = false, + shape = shape, + iconPosition = iconPosition, + button = button, + ) + StateRow( + label = "Disabled", + isEnabled = false, + isLoading = false, + shape = shape, + iconPosition = iconPosition, + button = button, + ) + StateRow( + label = "Loading", + isEnabled = true, + isLoading = true, + shape = shape, + iconPosition = iconPosition, + button = button, + ) } } @@ -253,27 +349,37 @@ private fun ColumnHeaderRow() { } } +@Suppress("LongParameterList") @Composable private fun StateRow( + label: String, isEnabled: Boolean, + isLoading: Boolean, shape: TangemButtonShape, - button: @Composable (isEnabled: Boolean, text: Boolean, shape: TangemButtonShape) -> Unit, + iconPosition: TangemButtonIconPosition, + button: @Composable ( + isEnabled: Boolean, + isLoading: Boolean, + text: Boolean, + shape: TangemButtonShape, + iconPosition: TangemButtonIconPosition, + ) -> Unit, ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( - text = if (isEnabled) "Enabled" else "Disabled", + text = label, style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.width(STATE_LABEL_WIDTH.dp), ) Box(modifier = Modifier.weight(1f)) { - button(isEnabled, true, shape) + button(isEnabled, isLoading, true, shape, iconPosition) } Box(modifier = Modifier.weight(1f)) { - button(isEnabled, false, shape) + button(isEnabled, isLoading, false, shape, iconPosition) } } } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/deviceicon/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/deviceicon/Build.kt new file mode 100644 index 0000000000..aaa3b8fc51 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/deviceicon/Build.kt @@ -0,0 +1,9 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.deviceicon + +import com.tangem.feature.tester.presentation.storybook.entity.DeviceIconStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal val deviceIconStoryFactory: StoryPageFactory = + StoryPageFactory { DeviceIconStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/deviceicon/DeviceIconStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/deviceicon/DeviceIconStory.kt new file mode 100644 index 0000000000..0c3708b35d --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/deviceicon/DeviceIconStory.kt @@ -0,0 +1,184 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.deviceicon + +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.ds.image.DeviceIconUM +import com.tangem.core.ui.ds.image.TangemDeviceIcon +import com.tangem.core.ui.res.TangemTheme + +private val CardBlue = Color(0xFF1C5FBF) +private val CardGold = Color(0xFFD4A017) +private val CardPurple = Color(0xFF7B2FBE) +private val RingGreen = Color(0xFF2ECC71) + +@Composable +internal fun DeviceIconStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("section_cards") { + SectionTitle(text = "Cards") + } + + item("card_1") { + DeviceIconRow( + label = "Single card", + state = DeviceIconUM.Card(mainColor = CardBlue, secondColor = null), + ) + } + + item("card_2") { + DeviceIconRow( + label = "Two cards", + state = DeviceIconUM.Card(mainColor = CardBlue, secondColor = CardGold), + ) + } + + item("card_3") { + DeviceIconRow( + label = "Three cards", + state = DeviceIconUM.Card( + mainColor = CardBlue, + secondColor = CardGold, + thirdColor = CardPurple, + ), + ) + } + + item("section_rings") { + SectionTitle(text = "Rings") + } + + item("ring_solo") { + DeviceIconRow( + label = "Ring only", + state = DeviceIconUM.Ring(mainColor = RingGreen), + ) + } + + item("ring_card") { + DeviceIconRow( + label = "Ring + card", + state = DeviceIconUM.Ring(mainColor = RingGreen, cardColor = CardBlue), + ) + } + + item("ring_two_cards") { + DeviceIconRow( + label = "Ring + 2 cards", + state = DeviceIconUM.Ring( + mainColor = RingGreen, + cardColor = CardBlue, + secondCardColor = CardGold, + ), + ) + } + + item("section_stubs") { + SectionTitle(text = "Stubs") + } + + repeat(3) { count -> + item("stub_$count") { + DeviceIconRow( + label = "Stub ($count card${if (count > 0) "s" else ""})", + state = DeviceIconUM.Stub(cardsCount = count), + ) + } + } + + item("section_mobile") { + SectionTitle(text = "Mobile") + } + + item("mobile") { + DeviceIconRow( + label = "Mobile wallet", + state = DeviceIconUM.Mobile, + ) + } + + item("section_sizes") { + SectionTitle(text = "Sizes") + } + + item("sizes") { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Bottom, + modifier = Modifier.padding(horizontal = 16.dp), + ) { + listOf(24, 32, 40, 48).forEach { size -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + TangemDeviceIcon( + state = DeviceIconUM.Card( + mainColor = CardBlue, + secondColor = CardGold, + thirdColor = CardPurple, + ), + modifier = Modifier.size(size.dp), + ) + Text( + text = "${size}dp", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } + } + } + } +} + +@Composable +private fun SectionTitle(text: String) { + Text( + text = text, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun DeviceIconRow(label: String, state: DeviceIconUM) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + ) { + TangemDeviceIcon( + state = state, + modifier = Modifier.size(40.dp), + ) + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/TangemHeaderRowStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/TangemHeaderRowStory.kt index 95a5c349d7..f034c7b02f 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/TangemHeaderRowStory.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/headerrow/TangemHeaderRowStory.kt @@ -88,6 +88,29 @@ private fun buildSampleRows(): List = listOf( title = stringReference("Account"), subtitle = stringReference("\$ 42,900.17"), ), + TangemHeaderRowUM( + id = "tail_text", + startIconUM = TangemIconUM.Currency(currencyIconState = CurrencyIconState.Locked), + tailUM = TangemRowTailUM.Text(text = stringReference("12 tokens")), + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "tail_draggable", + startIconUM = TangemIconUM.Currency(currencyIconState = CurrencyIconState.Locked), + tailUM = TangemRowTailUM.Draggable(iconRes = R.drawable.ic_drag_24), + title = stringReference("Account"), + subtitle = stringReference("\$ 42,900.17"), + ), + TangemHeaderRowUM( + id = "clickable", + startIconUM = TangemIconUM.Currency(currencyIconState = CurrencyIconState.Locked), + tailUM = TangemRowTailUM.Icon(R.drawable.ic_arrow_collapse_24), + title = stringReference("Clickable row"), + subtitle = stringReference("\$ 42,900.17"), + isEnabled = true, + onItemClick = {}, + ), TangemHeaderRowUM( id = "title_only", title = stringReference("Account"), diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/pagerindicator/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/pagerindicator/Build.kt new file mode 100644 index 0000000000..22832e4de4 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/pagerindicator/Build.kt @@ -0,0 +1,9 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.pagerindicator + +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory +import com.tangem.feature.tester.presentation.storybook.entity.TangemPagerIndicatorStory + +internal val tangemPagerIndicatorStoryFactory: StoryPageFactory = + StoryPageFactory { TangemPagerIndicatorStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/pagerindicator/TangemPagerIndicatorStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/pagerindicator/TangemPagerIndicatorStory.kt new file mode 100644 index 0000000000..fc770c4015 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/pagerindicator/TangemPagerIndicatorStory.kt @@ -0,0 +1,142 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.pagerindicator + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.pager.rememberPagerState +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.TangemPagerIndicator +import com.tangem.core.ui.ds.TangemPagerIndicatorColors +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun TangemPagerIndicatorStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("section_page_counts") { + SectionTitle(text = "Page counts") + } + + listOf(1, 2, 3, 4, 5).forEach { pageCount -> + item("count_$pageCount") { + IndicatorRow(label = "$pageCount page(s)", pageCount = pageCount, currentPage = 0) + } + } + + item("section_many_pages") { + SectionTitle(text = "Many pages (>5)") + } + + item("many_start") { + IndicatorRow(label = "7 pages, at start", pageCount = 7, currentPage = 0) + } + + item("many_middle") { + IndicatorRow(label = "7 pages, at middle", pageCount = 7, currentPage = 3) + } + + item("many_end") { + IndicatorRow(label = "7 pages, at end", pageCount = 7, currentPage = 6) + } + + item("ten_start") { + IndicatorRow(label = "10 pages, at start", pageCount = 10, currentPage = 0) + } + + item("ten_middle") { + IndicatorRow(label = "10 pages, at middle", pageCount = 10, currentPage = 5) + } + + item("ten_end") { + IndicatorRow(label = "10 pages, at end", pageCount = 10, currentPage = 9) + } + + item("section_active_positions") { + SectionTitle(text = "Active dot positions (5 pages)") + } + + repeat(4) { page -> + item("active_$page") { + IndicatorRow(label = "Active: page ${page + 1}", pageCount = 5, currentPage = page) + } + } + + item("section_overlay") { + SectionTitle(text = "With overlay background") + } + + item("overlay") { + IndicatorRowWithOverlay(pageCount = 5, currentPage = 2) + } + } +} + +@Composable +private fun SectionTitle(text: String) { + Text( + text = text, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun IndicatorRow(label: String, pageCount: Int, currentPage: Int) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + TangemPagerIndicator( + pagerState = rememberPagerState(currentPage) { pageCount }, + ) + } +} + +@Composable +private fun IndicatorRowWithOverlay(pageCount: Int, currentPage: Int) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + ) { + Text( + text = "With overlay", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + TangemPagerIndicator( + pagerState = rememberPagerState(currentPage) { pageCount }, + colors = TangemPagerIndicatorColors.copy( + overlay = TangemTheme.colors2.tabs.backgroundSecondary, + ), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/placeholder/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/placeholder/Build.kt new file mode 100644 index 0000000000..947666e5a7 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/placeholder/Build.kt @@ -0,0 +1,9 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.placeholder + +import com.tangem.feature.tester.presentation.storybook.entity.PlaceholderStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal val placeholderStoryFactory: StoryPageFactory = + StoryPageFactory { PlaceholderStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/placeholder/PlaceholderStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/placeholder/PlaceholderStory.kt new file mode 100644 index 0000000000..5eb7aa7d41 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/placeholder/PlaceholderStory.kt @@ -0,0 +1,254 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.placeholder + +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.unit.dp +import com.tangem.core.ui.components.ChipShimmer +import com.tangem.core.ui.components.CircleShimmer +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.res.TangemTheme + +@Composable +internal fun PlaceholderStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("section_rectangle") { + SectionTitle(text = "RectangleShimmer") + } + + item("rect_default") { + ShimmerRow(label = "Default") { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(24.dp), + ) + } + } + + item("rect_narrow") { + ShimmerRow(label = "Narrow (40%)") { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth(fraction = 0.4f) + .height(16.dp), + ) + } + } + + item("rect_tall") { + ShimmerRow(label = "Tall (48dp)") { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + ) + } + } + + item("rect_custom_radius") { + ShimmerRow(label = "Custom radius (16dp)") { + RectangleShimmer( + modifier = Modifier + .fillMaxWidth() + .height(24.dp), + radius = 16.dp, + ) + } + } + + item("section_circle") { + SectionTitle(text = "CircleShimmer") + } + + item("circle_sizes") { + ShimmerRow(label = "Sizes: 24, 32, 40, 48") { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + CircleShimmer(modifier = Modifier.size(24.dp)) + CircleShimmer(modifier = Modifier.size(32.dp)) + CircleShimmer(modifier = Modifier.size(40.dp)) + CircleShimmer(modifier = Modifier.size(48.dp)) + } + } + } + + item("section_text") { + SectionTitle(text = "TextShimmer") + } + + item("text_title") { + ShimmerRow(label = "titleRegular44") { + TextShimmer( + style = TangemTheme.typography2.titleRegular44, + modifier = Modifier.fillMaxWidth(fraction = 0.5f), + ) + } + } + + item("text_heading") { + ShimmerRow(label = "headingSemibold22") { + TextShimmer( + style = TangemTheme.typography2.headingSemibold22, + modifier = Modifier.fillMaxWidth(fraction = 0.6f), + ) + } + } + + item("text_body") { + ShimmerRow(label = "bodyRegular16") { + TextShimmer( + style = TangemTheme.typography2.bodyRegular16, + modifier = Modifier.fillMaxWidth(fraction = 0.7f), + ) + } + } + + item("text_caption") { + ShimmerRow(label = "captionRegular12") { + TextShimmer( + style = TangemTheme.typography2.captionRegular12, + modifier = Modifier.fillMaxWidth(fraction = 0.4f), + ) + } + } + + item("text_size_height") { + ShimmerRow(label = "bodyRegular16 (textSizeHeight)") { + TextShimmer( + style = TangemTheme.typography2.bodyRegular16, + textSizeHeight = true, + modifier = Modifier.fillMaxWidth(fraction = 0.5f), + ) + } + } + + item("section_button_chip") { + SectionTitle(text = "SmallButtonShimmer & ChipShimmer") + } + + item("small_button") { + ShimmerRow(label = "SmallButtonShimmer") { + SmallButtonShimmer() + } + } + + item("small_button_icon") { + ShimmerRow(label = "SmallButtonShimmer (with icon)") { + SmallButtonShimmer(withIcon = true) + } + } + + item("chip") { + ShimmerRow(label = "ChipShimmer") { + ChipShimmer() + } + } + + item("section_composition") { + SectionTitle(text = "Skeleton composition") + } + + item("card_skeleton") { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = 16.dp), + ) { + Text( + text = "Typical card loading state", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + CircleShimmer(modifier = Modifier.size(40.dp)) + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + TextShimmer( + style = TangemTheme.typography2.headingSemibold17, + modifier = Modifier.width(120.dp), + ) + TextShimmer( + style = TangemTheme.typography2.captionRegular13, + modifier = Modifier.width(80.dp), + ) + } + } + } + } + + item("list_skeleton") { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(horizontal = 16.dp), + ) { + Text( + text = "List loading state", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + repeat(3) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + RectangleShimmer(modifier = Modifier.size(40.dp)) + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.weight(1f), + ) { + TextShimmer( + style = TangemTheme.typography2.bodyMedium16, + modifier = Modifier.fillMaxWidth(fraction = 0.6f), + ) + TextShimmer( + style = TangemTheme.typography2.captionRegular13, + modifier = Modifier.fillMaxWidth(fraction = 0.4f), + ) + } + } + } + } + } + } +} + +@Composable +private fun SectionTitle(text: String) { + Text( + text = text, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun ShimmerRow(label: String, content: @Composable () -> Unit) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.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/progress/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/progress/Build.kt new file mode 100644 index 0000000000..9855967b04 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/progress/Build.kt @@ -0,0 +1,9 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.progress + +import com.tangem.feature.tester.presentation.storybook.entity.ProgressIndicatorStory +import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory + +internal val progressIndicatorStoryFactory: StoryPageFactory = + StoryPageFactory { ProgressIndicatorStory } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/progress/ProgressIndicatorStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/progress/ProgressIndicatorStory.kt new file mode 100644 index 0000000000..85ef01a6ea --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/progress/ProgressIndicatorStory.kt @@ -0,0 +1,132 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.progress + +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.unit.dp +import com.tangem.core.ui.ds.progress.TangemLinearProgressIndicatorWithDot +import com.tangem.core.ui.res.TangemTheme + +@Composable +internal fun ProgressIndicatorStory(modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("section_progress") { + SectionTitle(text = "Progress values") + } + + listOf(0f, 0.25f, 0.5f, 0.75f, 1f).forEach { progress -> + item("progress_$progress") { + ProgressRow(label = "${(progress * 100).toInt()}%", progress = progress) + } + } + + item("section_heights") { + SectionTitle(text = "Track heights") + } + + listOf(4, 6, 8).forEach { height -> + item("height_$height") { + ProgressRow(label = "${height}dp track", progress = 0.5f, height = height) + } + } + + item("section_colors") { + SectionTitle(text = "Color variants") + } + + item("accent") { + ProgressRowWithColors( + label = "Accent", + progress = 0.6f, + dotColor = TangemTheme.colors2.fill.status.accent, + bgColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant, + ) + } + + item("warning") { + ProgressRowWithColors( + label = "Warning", + progress = 0.4f, + dotColor = TangemTheme.colors2.fill.status.warning, + bgColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant, + ) + } + + item("attention") { + ProgressRowWithColors( + label = "Attention", + progress = 0.8f, + dotColor = TangemTheme.colors2.fill.status.attention, + bgColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant, + ) + } + } +} + +@Composable +private fun SectionTitle(text: String) { + Text( + text = text, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp), + ) + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} + +@Composable +private fun ProgressRow(label: String, progress: Float, height: Int = 6) { + ProgressRowWithColors( + label = label, + progress = progress, + height = height, + dotColor = TangemTheme.colors2.fill.status.accent, + bgColor = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant, + ) +} + +@Composable +private fun ProgressRowWithColors( + label: String, + progress: Float, + dotColor: androidx.compose.ui.graphics.Color, + bgColor: androidx.compose.ui.graphics.Color, + height: Int = 6, +) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + TangemLinearProgressIndicatorWithDot( + progress = { progress }, + dotColor = dotColor, + backgroundColor = bgColor, + modifier = Modifier + .fillMaxWidth() + .height(height.dp), + ) + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tab/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tab/Build.kt new file mode 100644 index 0000000000..21b9a31e14 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tab/Build.kt @@ -0,0 +1,19 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.tab + +import com.tangem.feature.tester.presentation.storybook.entity.TangemTabStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemTabStory { + return TangemTabStory( + checkedIndex = 0, + onCheckedIndexChange = { index -> + updateStory { it.copy(checkedIndex = index) } + }, + ) +} + +internal val tangemTabStoryFactory + 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/tab/TangemTabStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tab/TangemTabStory.kt new file mode 100644 index 0000000000..f734c1027c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/tab/TangemTabStory.kt @@ -0,0 +1,127 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.tab + +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.unit.dp +import com.tangem.core.ui.ds.tabs.TangemTab +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemTabStory + +@Composable +internal fun TangemTabStory(state: TangemTabStory, modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + item("interactive") { + TabSection(title = "Interactive") { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + listOf("Markets", "Portfolio", "Activity").forEachIndexed { index, label -> + TangemTab( + text = stringReference(label), + isChecked = state.checkedIndex == index, + onCheckedChange = { if (it) state.onCheckedIndexChange(index) }, + ) + } + } + } + } + + item("checked") { + TabSection(title = "Checked") { + TangemTab( + text = stringReference("Markets"), + isChecked = true, + onCheckedChange = {}, + ) + } + } + + item("unchecked") { + TabSection(title = "Unchecked") { + TangemTab( + text = stringReference("Markets"), + isChecked = false, + onCheckedChange = {}, + ) + } + } + + item("disabled_checked") { + TabSection(title = "Disabled (Checked)") { + TangemTab( + text = stringReference("Markets"), + isChecked = true, + onCheckedChange = {}, + isEnabled = false, + ) + } + } + + item("disabled_unchecked") { + TabSection(title = "Disabled (Unchecked)") { + TangemTab( + text = stringReference("Markets"), + isChecked = false, + onCheckedChange = {}, + isEnabled = false, + ) + } + } + + item("multiple_tabs") { + TabSection(title = "Multiple tabs row") { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + TangemTab( + text = stringReference("All"), + isChecked = true, + onCheckedChange = {}, + ) + TangemTab( + text = stringReference("Gainers"), + isChecked = false, + onCheckedChange = {}, + ) + TangemTab( + text = stringReference("Losers"), + isChecked = false, + onCheckedChange = {}, + ) + } + } + } + } +} + +@Composable +private fun TabSection(title: String, content: @Composable () -> Unit) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + ) { + Text( + text = title, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + content() + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/topbar/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/topbar/Build.kt new file mode 100644 index 0000000000..494ab36f6b --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/topbar/Build.kt @@ -0,0 +1,20 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.topbar + +import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.feature.tester.presentation.storybook.entity.TangemTopBarStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemTopBarStory { + return TangemTopBarStory( + selectedType = TangemTopBarType.Default, + onTypeChange = { type -> + updateStory { it.copy(selectedType = type) } + }, + ) +} + +internal val tangemTopBarStoryFactory + 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/topbar/TangemTopBarStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/topbar/TangemTopBarStory.kt new file mode 100644 index 0000000000..4a0d026c91 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/topbar/TangemTopBarStory.kt @@ -0,0 +1,259 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.topbar + +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.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM +import com.tangem.core.ui.ds.topbar.TangemTopBarType +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemTopBarStory +import kotlinx.collections.immutable.persistentListOf + +@Composable +internal fun TangemTopBarStory(state: TangemTopBarStory, modifier: Modifier = Modifier) { + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("type_toggle") { + TypeToggle( + selected = state.selectedType, + onSelect = state.onTypeChange, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + + item("title_only") { + TopBarVariant(label = "Title only") { + TangemTopBar( + title = stringReference("Wallet"), + type = state.selectedType, + startAction = null, + ) + } + } + + item("title_subtitle") { + TopBarVariant(label = "Title + Subtitle") { + TangemTopBar( + title = stringReference("Wallet"), + subtitle = stringReference("3 cards"), + type = state.selectedType, + startAction = null, + ) + } + } + + item("back_action") { + TopBarVariant(label = "Back action + Title") { + TangemTopBar( + title = stringReference("Send"), + type = state.selectedType, + startAction = TangemTopBarActionUM( + iconRes = R.drawable.ic_back_24, + onClick = {}, + ), + ) + } + } + + item("back_and_end") { + TopBarVariant(label = "Back + Title + End action") { + TangemTopBar( + title = stringReference("Token Details"), + type = state.selectedType, + startAction = TangemTopBarActionUM( + iconRes = R.drawable.ic_back_24, + onClick = {}, + ), + endActions = persistentListOf( + TangemTopBarActionUM( + iconRes = R.drawable.ic_more_default_24, + onClick = {}, + ), + ), + ) + } + } + + item("back_and_two_end") { + TopBarVariant(label = "Back + Title + 2 End actions") { + TangemTopBar( + title = stringReference("Settings"), + type = state.selectedType, + startAction = TangemTopBarActionUM( + iconRes = R.drawable.ic_back_24, + onClick = {}, + ), + endActions = persistentListOf( + TangemTopBarActionUM( + iconRes = R.drawable.ic_information_24, + onClick = {}, + ), + TangemTopBarActionUM( + iconRes = R.drawable.ic_close_24, + onClick = {}, + ), + ), + ) + } + } + + item("ghost_actions") { + TopBarVariant(label = "Ghost mode actions (progress=1)") { + TangemTopBar( + title = stringReference("Portfolio"), + type = state.selectedType, + startAction = TangemTopBarActionUM( + iconRes = R.drawable.ic_tangem_24, + onClick = {}, + ghostModeProgress = 1f, + ), + endActions = persistentListOf( + TangemTopBarActionUM( + iconRes = R.drawable.ic_more_default_24, + onClick = {}, + ghostModeProgress = 1f, + ), + ), + ) + } + } + + item("non_actionable") { + TopBarVariant(label = "Non-actionable icons") { + TangemTopBar( + title = stringReference("Details"), + type = state.selectedType, + startAction = TangemTopBarActionUM( + iconRes = R.drawable.ic_tangem_24, + isActionable = false, + ), + endActions = persistentListOf( + TangemTopBarActionUM( + iconRes = R.drawable.ic_more_default_24, + isActionable = false, + ), + ), + ) + } + } + + item("title_icon") { + TopBarVariant(label = "Title with icon") { + TangemTopBar( + title = stringReference("Wallet"), + titleIconRes = R.drawable.ic_tangem_24, + type = state.selectedType, + startAction = TangemTopBarActionUM( + iconRes = R.drawable.ic_back_24, + onClick = {}, + ), + ) + } + } + + item("no_title") { + TopBarVariant(label = "No title (end action only)") { + TangemTopBar( + type = state.selectedType, + startAction = null, + endActions = persistentListOf( + TangemTopBarActionUM( + iconRes = R.drawable.ic_close_24, + onClick = {}, + ), + ), + ) + } + } + } +} + +@Composable +private fun TypeToggle( + selected: TangemTopBarType, + onSelect: (TangemTopBarType) -> 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), + ) { + TangemTopBarType.entries.forEach { type -> + TypeChip( + label = type.name, + selected = type == selected, + onClick = { onSelect(type) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun TypeChip(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 TopBarVariant(label: String, content: @Composable () -> Unit) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + content() + } + HorizontalDivider( + color = TangemTheme.colors2.border.neutral.secondary, + modifier = Modifier.padding(horizontal = 16.dp), + ) +} \ 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 3a8b75942d..26d7f561c6 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 @@ -20,12 +20,18 @@ import com.tangem.feature.tester.presentation.storybook.page.badge.tangemBadgeSt import com.tangem.feature.tester.presentation.storybook.page.buttons.buttonsStoryFactory import com.tangem.feature.tester.presentation.storybook.page.checkbox.tangemCheckboxStoryFactory import com.tangem.feature.tester.presentation.storybook.page.contextmenu.tangemContextMenuStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.deviceicon.deviceIconStoryFactory import com.tangem.feature.tester.presentation.storybook.page.headerrow.tangemHeaderRowStoryFactory import com.tangem.feature.tester.presentation.storybook.page.message.tangemMessageStoryFactory import com.tangem.feature.tester.presentation.storybook.page.opportunities.opportunitiesBGStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.pagerindicator.tangemPagerIndicatorStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.placeholder.placeholderStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.progress.progressIndicatorStoryFactory import com.tangem.feature.tester.presentation.storybook.page.searchfield.tangemSearchFieldStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.tab.tangemTabStoryFactory import com.tangem.feature.tester.presentation.storybook.page.tabs.tangemSegmentedPickerStoryFactory import com.tangem.feature.tester.presentation.storybook.page.tokenrow.tangemTokenRowStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.topbar.tangemTopBarStoryFactory import com.tangem.feature.tester.presentation.storybook.page.typography.typographyStoryFactory private data class StoryItem(val title: String, val factory: StoryPageFactory) @@ -43,6 +49,12 @@ private fun buildStories() = listOf( StoryItem(title = "📋 Context Menu", factory = tangemContextMenuStoryFactory), StoryItem(title = "🔍 Search Field", factory = tangemSearchFieldStoryFactory), StoryItem(title = "🔤 Typography", factory = typographyStoryFactory), + StoryItem(title = "🧭 Top Bar", factory = tangemTopBarStoryFactory), + StoryItem(title = "🔀 Tab", factory = tangemTabStoryFactory), + StoryItem(title = "⚫ Pager Indicator", factory = tangemPagerIndicatorStoryFactory), + StoryItem(title = "💀 Placeholder", factory = placeholderStoryFactory), + StoryItem(title = "⏳ Progress Indicator", factory = progressIndicatorStoryFactory), + StoryItem(title = "💳 Device Icon", factory = deviceIconStoryFactory), ) @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 e8e17c0645..7bce52bb28 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 @@ -5,8 +5,11 @@ 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.DeviceIconStory 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.PlaceholderStory +import com.tangem.feature.tester.presentation.storybook.entity.ProgressIndicatorStory 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 @@ -14,23 +17,33 @@ import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxSto import com.tangem.feature.tester.presentation.storybook.entity.TangemHeaderRowStory import com.tangem.feature.tester.presentation.storybook.entity.TangemContextMenuStory import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemPagerIndicatorStory import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchFieldStory import com.tangem.feature.tester.presentation.storybook.entity.TangemSegmentedPickerStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemTabStory import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemTopBarStory import com.tangem.feature.tester.presentation.storybook.entity.TypographyStory 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.deviceicon.DeviceIconStory 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.pagerindicator.TangemPagerIndicatorStory +import com.tangem.feature.tester.presentation.storybook.page.placeholder.PlaceholderStory +import com.tangem.feature.tester.presentation.storybook.page.progress.ProgressIndicatorStory +import com.tangem.feature.tester.presentation.storybook.page.tab.TangemTabStory import com.tangem.feature.tester.presentation.storybook.page.tabs.TangemSegmentedPickerStory import com.tangem.feature.tester.presentation.storybook.page.tokenrow.TangemTokenRowStory +import com.tangem.feature.tester.presentation.storybook.page.topbar.TangemTopBarStory import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory import com.tangem.feature.tester.presentation.storybook.page.contextmenu.TangemContextMenuStory import com.tangem.feature.tester.presentation.storybook.page.searchfield.TangemSearchFieldStory import com.tangem.feature.tester.presentation.storybook.page.typography.TypographyStory +@Suppress("CyclomaticComplexMethod") @Composable internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) { BackHandler(onBack = state.onBackClick) @@ -54,6 +67,12 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) is TangemContextMenuStory -> TangemContextMenuStory(state = storyState) is TangemSearchFieldStory -> TangemSearchFieldStory(state = storyState) is TypographyStory -> TypographyStory(state = storyState) + is TangemTopBarStory -> TangemTopBarStory(state = storyState) + is TangemTabStory -> TangemTabStory(state = storyState) + TangemPagerIndicatorStory -> TangemPagerIndicatorStory() + PlaceholderStory -> PlaceholderStory() + ProgressIndicatorStory -> ProgressIndicatorStory() + DeviceIconStory -> DeviceIconStory() } } } \ No newline at end of file From a47b0447f65f0a733a26347566f88583235ebd17 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Apr 2026 10:25:49 +0000 Subject: [PATCH 09/32] 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 278d64c66f..165d042410 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.37-1485" +tangemBlockchainSdk = "develop-1482" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.37-603" +tangemCardSdk = "develop-602" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From bd411ae949fb3194047370ce4b6cd27d8aa7d0ee Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Apr 2026 15:58:34 +0500 Subject: [PATCH 10/32] Updated on 2026-08-14 --- .../com/tangem/scenarios/BaseScenarios.kt | 9 +++-- .../domain/sdk/impl/MockTangemSdkManager.kt | 14 ++++++++ .../domain/sdk/mocks/MockCardPickerDialog.kt | 33 +++++++++++++++++++ .../tap/domain/sdk/mocks/MockProvider.kt | 28 ++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 5 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockCardPickerDialog.kt diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 85e746d709..2d11046a5c 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -15,11 +15,10 @@ fun BaseTestCase.scanCard( mockContent: MockContent? = null, isTwinsCard: Boolean = false, ) { - if (productType != null) { - MockProvider.setMocks(productType) - } - if (mockContent != null) { - MockProvider.setMocks(mockContent) + when { + mockContent != null -> MockProvider.setMocks(mockContent) + productType != null -> MockProvider.setMocks(productType) + else -> MockProvider.setMocks(ProductType.Wallet) } step("Click on 'Accept' button") { onDisclaimerScreen { acceptButton.clickWithAssertion() } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index c70dc421f4..bc15466208 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -10,6 +10,7 @@ import com.tangem.common.KeyPair import com.tangem.common.SuccessResponse import com.tangem.common.authentication.keystore.DummyKeystoreManager import com.tangem.common.core.CardSessionRunnable +import com.tangem.common.core.TangemSdkError import com.tangem.common.core.UserCodeRequestPolicy import com.tangem.common.extensions.ByteArrayKey import com.tangem.common.services.InMemoryStorage @@ -32,6 +33,8 @@ import com.tangem.sdk.api.TangemSdkManager import com.tangem.sdk.api.visa.VisaCardActivationResponse import com.tangem.sdk.api.visa.VisaCardActivationTaskMode import com.tangem.tap.domain.sdk.mocks.MockProvider +import com.tangem.tap.domain.sdk.mocks.showMockCardPicker +import com.tangem.tap.foregroundActivityObserver @Suppress("TooManyFunctions") class MockTangemSdkManager( @@ -61,6 +64,17 @@ class MockTangemSdkManager( allowsRequestAccessCodeFromRepository: Boolean, shouldCheckIsAlreadyActivated: Boolean, ): CompletionResult { + if (!MockProvider.isPreset) { + val activity = foregroundActivityObserver.foregroundActivity + if (activity != null) { + val selectedMock = showMockCardPicker(activity) + if (selectedMock != null) { + MockProvider.setMocksWithoutPresetFlag(selectedMock) + } else { + return CompletionResult.Failure(TangemSdkError.UserCancelled()) + } + } + } return MockProvider.getScanResponse() } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockCardPickerDialog.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockCardPickerDialog.kt new file mode 100644 index 0000000000..301a9214d2 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockCardPickerDialog.kt @@ -0,0 +1,33 @@ +package com.tangem.tap.domain.sdk.mocks + +import androidx.appcompat.app.AlertDialog +import com.tangem.wallet.R +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import kotlin.coroutines.resume + +internal suspend fun showMockCardPicker(activity: AppCompatActivity): MockContent? = withContext(Dispatchers.Main) { + suspendCancellableCoroutine { continuation -> + val mocks = MockProvider.availableMocks + val names = mocks.map { it.first }.toTypedArray() + + val dialog = AlertDialog.Builder(activity) + .setTitle(R.string.mock_card_picker_title) + .setItems(names) { _, which -> + if (continuation.isActive) { + continuation.resume(mocks[which].second) + } + } + .setOnCancelListener { + if (continuation.isActive) { + continuation.resume(null) + } + } + .create() + + continuation.invokeOnCancellation { activity.runOnUiThread { dialog.dismiss() } } + dialog.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt index 704c10dd50..cc16fe4302 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/mocks/MockProvider.kt @@ -11,10 +11,32 @@ object MockProvider { private var content: MockContent = getMockContent(ProductType.Wallet) + var isPreset: Boolean = false + private set + private var isEmulatingError: Boolean = false private var emulatedError: TangemError = TangemSdkError.TagLost() + val availableMocks: List> = listOf( + "Wallet" to WalletMockContent, + "Note" to NoteMockContent, + "Twins" to TwinsMockContent, + "Ring" to RingMockContent, + "Wallet 2" to Wallet2MockContent, + "Wallet 2 (No Backup)" to Wallet2NoBackupMockContent, + "Wallet 2 (No Backup, No Wallets)" to Wallet2NoBackupNoWalletsMockContent, + "Wallet 2 (Seed Phrase)" to Wallet2WithSeedPhraseMockContent, + "Shiba" to ShibaMockContent, + "Shiba (No Backup)" to ShibaNoBackupMockContent, + "Shiba (No Backup, No Wallets)" to ShibaNoBackupNoWalletsMockContent, + "Ed25519 Curve" to EdCurveMockContent, + "Secp256k1 Curve" to Secpk1CurveMockContent, + "Backup Wallet" to BackupWalletMockContent, + "Dev Wallet" to DevWalletMockContent, + "Firmware 4.12" to Firmware412MockContent, + ) + fun setEmulateError(error: TangemError? = null) { isEmulatingError = true error?.let { @@ -28,10 +50,16 @@ object MockProvider { fun setMocks(productType: ProductType) { content = getMockContent(productType) + isPreset = true } fun setMocks(mockContent: MockContent) { content = mockContent + isPreset = true + } + + fun setMocksWithoutPresetFlag(mockContent: MockContent) { + content = mockContent } fun getSuccessResponse() = CompletionResult.Success(content.successResponse).orFailure() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 250461aa6b..e1c13baa62 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,5 +2,6 @@ Tangem + Select Mock Card From 49127d64dd97f29919b609ea11aaa7f3f33d12b5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Apr 2026 04:24:13 -0700 Subject: [PATCH 11/32] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../components/TangemPayCardPageComponent.kt | 14 ++ .../DefaultTangemPayCardPageComponent.kt | 96 +++++++++ .../TangemPayCardPageScreenComponent.kt | 80 ++++++++ .../di/TangemPayDetailsFeatureModule.kt | 8 + .../tangempay/di/TangemPayModelModule.kt | 6 + .../tangempay/entity/TangemPayCardPageUM.kt | 39 ++++ .../tangempay/model/TangemPayCardPageModel.kt | 177 +++++++++++++++++ .../tangempay/ui/TangemPayCardPageScreen.kt | 182 ++++++++++++++++++ 9 files changed, 603 insertions(+) create mode 100644 features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 5b19f4c6fd..75603cdf2e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1612,6 +1612,7 @@ Withdraw unavailable now You can\'t initiate swap or new withdrawal till the current one is finished Withdrawal in progress + Card settings Change PIN-code Come back to the app if you forget it. I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery diff --git a/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageComponent.kt b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageComponent.kt new file mode 100644 index 0000000000..9ec8ddecd1 --- /dev/null +++ b/features/tangempay/details/api/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageComponent.kt @@ -0,0 +1,14 @@ +package com.tangem.features.tangempay.components + +import com.tangem.core.decompose.factory.ComponentFactory +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.TangemPayDetailsConfig + +interface TangemPayCardPageComponent : ComposableContentComponent { + data class Params( + val userWalletId: UserWalletId, + val config: TangemPayDetailsConfig, + ) + interface Factory : ComponentFactory +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt new file mode 100644 index 0000000000..a69dbe675f --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt @@ -0,0 +1,96 @@ +package com.tangem.features.tangempay.components + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.stack.Children +import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.stack.StackNavigation +import com.arkivanov.decompose.router.stack.childStack +import com.arkivanov.decompose.router.stack.pop +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.childByContext +import com.tangem.core.decompose.navigation.inner.InnerRouter +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +internal class DefaultTangemPayCardPageComponent @AssistedInject constructor( + @Assisted private val appComponentContext: AppComponentContext, + @Assisted private val params: TangemPayCardPageComponent.Params, +) : AppComponentContext by appComponentContext, TangemPayCardPageComponent { + + private val stackNavigation = StackNavigation() + + private val innerRouter = InnerRouter( + stackNavigation = stackNavigation, + popCallback = { onChildBack() }, + ) + + private val childStack = childStack( + key = "tangemPayCardPageInnerStack", + source = stackNavigation, + serializer = TangemPayDetailsInnerRoute.serializer(), + initialConfiguration = TangemPayDetailsInnerRoute.Details, + childFactory = ::screenChild, + ) + + @Suppress("ReusedModifierInstance") + @Composable + override fun Content(modifier: Modifier) { + val childStack by childStack.subscribeAsState() + Children( + stack = childStack, + animation = stackAnimation(), + ) { child -> + child.instance.Content(modifier = modifier) + } + } + + private fun screenChild( + config: TangemPayDetailsInnerRoute, + componentContext: ComponentContext, + ): ComposableContentComponent = when (config) { + TangemPayDetailsInnerRoute.Details -> TangemPayCardPageScreenComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = params, + ) + TangemPayDetailsInnerRoute.ChangePIN -> TangemPayChangePinComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = TangemPayDetailsContainerComponent.Params( + userWalletId = params.userWalletId, + config = params.config, + ), + ) + TangemPayDetailsInnerRoute.ChangePINSuccess -> TangemPayChangePinSuccessComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + ) + TangemPayDetailsInnerRoute.AddToWallet -> TangemPayAddToWalletComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = TangemPayDetailsContainerComponent.Params( + userWalletId = params.userWalletId, + config = params.config, + ), + ) + } + + private fun onChildBack() { + if (childStack.value.backStack.isEmpty()) { + router.pop() + } else { + stackNavigation.pop() + } + } + + @AssistedFactory + interface Factory : TangemPayCardPageComponent.Factory { + override fun create( + context: AppComponentContext, + params: TangemPayCardPageComponent.Params, + ): DefaultTangemPayCardPageComponent + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt new file mode 100644 index 0000000000..975ce82cf3 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt @@ -0,0 +1,80 @@ +package com.tangem.features.tangempay.components + +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.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.NavigationBar3ButtonsScrim +import com.tangem.core.ui.decompose.ComposableBottomSheetComponent +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCardDetailsBlockComponent +import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent +import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation +import com.tangem.features.tangempay.model.TangemPayCardPageModel +import com.tangem.features.tangempay.ui.TangemPayCardPageScreen + +internal class TangemPayCardPageScreenComponent( + private val appComponentContext: AppComponentContext, + private val params: TangemPayCardPageComponent.Params, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TangemPayCardPageModel = getOrCreateModel(params = params) + + private val containerParams = TangemPayDetailsContainerComponent.Params( + userWalletId = params.userWalletId, + config = params.config, + ) + + private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent( + appComponentContext = child("cardDetailsBlockComponent"), + params = TangemPayCardDetailsBlockComponent.Params(params = containerParams), + ) + + private val bottomSheetSlot = childSlot( + source = model.bottomSheetNavigation, + serializer = TangemPayDetailsNavigation.serializer(), + handleBackButton = false, + childFactory = ::bottomSheetChild, + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle() + val bottomSheet by bottomSheetSlot.subscribeAsState() + + NavigationBar3ButtonsScrim() + TangemPayCardPageScreen( + state = state, + cardDetailsBlockComponent = cardDetailsBlockComponent, + cardDetailsState = cardDetailsState, + modifier = modifier, + ) + bottomSheet.child?.instance?.BottomSheet() + } + + private fun bottomSheetChild( + navigation: TangemPayDetailsNavigation, + componentContext: ComponentContext, + ): ComposableBottomSheetComponent { + val context = childByContext(componentContext) + return when (navigation) { + is TangemPayDetailsNavigation.ViewPinCode -> TangemPayViewPinComponent( + appComponentContext = context, + params = TangemPayViewPinComponent.Params( + walletId = navigation.userWalletId, + cardId = navigation.cardId, + listener = model, + ), + ) + else -> error("Unsupported bottom sheet navigation: $navigation") + } + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt index de293a174d..df71114f7c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayDetailsFeatureModule.kt @@ -1,6 +1,8 @@ package com.tangem.features.tangempay.di +import com.tangem.features.tangempay.components.DefaultTangemPayCardPageComponent import com.tangem.features.tangempay.components.DefaultTangemPayDetailsContainerComponent +import com.tangem.features.tangempay.components.TangemPayCardPageComponent import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent import com.tangem.features.tangempay.model.listener.CardDetailsEventListener import com.tangem.features.tangempay.model.listener.DefaultCardDetailsEventListener @@ -20,6 +22,12 @@ internal interface TangemPayDetailsFeatureModule { factory: DefaultTangemPayDetailsContainerComponent.Factory, ): TangemPayDetailsContainerComponent.Factory + @Binds + @Singleton + fun bindTangemPayCardPageComponentFactory( + factory: DefaultTangemPayCardPageComponent.Factory, + ): TangemPayCardPageComponent.Factory + @Binds @Singleton fun bindCardDetailsEventListener(impl: DefaultCardDetailsEventListener): CardDetailsEventListener diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt index 2150e54279..d533bce2b9 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -5,6 +5,7 @@ import com.tangem.core.decompose.model.Model import com.tangem.features.tangempay.model.TangemPayAddFundsModel import com.tangem.features.tangempay.model.TangemPayAddToWalletModel import com.tangem.features.tangempay.model.TangemPayCardDetailsBlockModel +import com.tangem.features.tangempay.model.TangemPayCardPageModel import com.tangem.features.tangempay.model.TangemPayChangePinModel import com.tangem.features.tangempay.model.TangemPayDetailsModel import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel @@ -59,4 +60,9 @@ internal interface TangemPayModelModule { @IntoMap @ClassKey(TangemPayViewPinModel::class) fun bindTangemPayViewPinModel(model: TangemPayViewPinModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayCardPageModel::class) + fun bindTangemPayCardPageModel(model: TangemPayCardPageModel): Model } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt new file mode 100644 index 0000000000..8a16bf6d94 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardPageUM.kt @@ -0,0 +1,39 @@ +package com.tangem.features.tangempay.entity + +import androidx.compose.runtime.Immutable +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Immutable +internal data class TangemPayCardPageUM( + val addToWalletBlockState: AddToWalletBlockState? = null, + val settings: ImmutableList = persistentListOf( + TangemPayCardPageSetting.ChangePIN, + TangemPayCardPageSetting.FreezeCard, + ), + val onBackClick: () -> Unit, + val onSettingClick: (TangemPayCardPageSetting) -> Unit, +) { + companion object { + fun stub( + addToWalletBlockState: AddToWalletBlockState? = AddToWalletBlockState(onClick = {}, onClickClose = {}), + settings: ImmutableList = persistentListOf( + TangemPayCardPageSetting.ChangePIN, + TangemPayCardPageSetting.FreezeCard, + TangemPayCardPageSetting.ReplaceCard, + ), + ) = TangemPayCardPageUM( + addToWalletBlockState = addToWalletBlockState, + settings = settings, + onBackClick = {}, + onSettingClick = {}, + ) + } +} + +@Immutable +internal sealed class TangemPayCardPageSetting { + data object ChangePIN : TangemPayCardPageSetting() + data object FreezeCard : TangemPayCardPageSetting() + data object ReplaceCard : TangemPayCardPageSetting() +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt new file mode 100644 index 0000000000..b08ca54fbb --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardPageModel.kt @@ -0,0 +1,177 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.runtime.Stable +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.features.tangempay.components.TangemPayCardPageComponent +import com.tangem.features.tangempay.components.ViewPinListener +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.AddToWalletBlockState +import com.tangem.features.tangempay.entity.TangemPayCardPageSetting +import com.tangem.features.tangempay.entity.TangemPayCardPageUM +import com.tangem.features.tangempay.entity.TangemPayDetailsNavigation +import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute +import com.tangem.features.tangempay.utils.TangemPayMessagesFactory +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.JobHolder +import com.tangem.utils.coroutines.saveIn +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayCardPageModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val uiMessageSender: UiMessageSender, +) : Model(), ViewPinListener { + + private val params: TangemPayCardPageComponent.Params = paramsContainer.require() + + private var currentFrozenState: TangemPayCardFrozenState = params.config.cardFrozenState + + private val addToWalletBannerJobHolder = JobHolder() + + val uiState: StateFlow + field = MutableStateFlow( + TangemPayCardPageUM( + onBackClick = router::pop, + onSettingClick = ::onSettingClick, + ), + ) + + val bottomSheetNavigation: SlotNavigation = SlotNavigation() + + init { + fetchAddToWalletBanner() + subscribeToCardFrozenState() + } + + private fun onSettingClick(setting: TangemPayCardPageSetting) = when (setting) { + TangemPayCardPageSetting.ChangePIN -> onClickChangePIN() + TangemPayCardPageSetting.FreezeCard -> onClickFreezeOrUnfreezeCard() + TangemPayCardPageSetting.ReplaceCard -> Unit // TODO v_rodionov #[REDACTED_TASK_KEY] + } + + private fun onClickChangePIN() { + if (!params.config.isPinSet) { + router.push(TangemPayDetailsInnerRoute.ChangePIN) + } else { + bottomSheetNavigation.activate( + TangemPayDetailsNavigation.ViewPinCode( + userWalletId = params.userWalletId, + cardId = params.config.cardId, + ), + ) + } + } + + private fun onClickFreezeOrUnfreezeCard() { + when (currentFrozenState) { + TangemPayCardFrozenState.Frozen -> uiMessageSender.send( + TangemPayMessagesFactory.createUnfreezeCardMessage(onUnfreezeClicked = ::unfreezeCard), + ) + else -> uiMessageSender.send( + TangemPayMessagesFactory.createFreezeCardMessage(onFreezeClicked = ::freezeCard), + ) + } + } + + private fun freezeCard() { + modelScope.launch { + cardDetailsRepository.freezeCard( + userWalletId = params.userWalletId, + cardId = params.config.cardId, + ).onLeft { + val message = SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed)) + uiMessageSender.send(message) + }.onRight { state -> + val message = if (state == TangemPayCardFrozenState.Frozen) { + SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_success)) + } else { + SnackbarMessage(resourceReference(R.string.tangem_pay_freeze_card_failed)) + } + uiMessageSender.send(message) + } + } + } + + private fun unfreezeCard() { + modelScope.launch { + cardDetailsRepository.unfreezeCard( + userWalletId = params.userWalletId, + cardId = params.config.cardId, + ).onLeft { + val message = SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed)) + uiMessageSender.send(message) + }.onRight { state -> + val message = if (state == TangemPayCardFrozenState.Unfrozen) { + SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_success)) + } else { + SnackbarMessage(resourceReference(R.string.tangem_pay_unfreeze_card_failed)) + } + uiMessageSender.send(message) + } + } + } + + private fun subscribeToCardFrozenState() { + cardDetailsRepository + .cardFrozenState(params.config.cardId) + .onEach { state -> currentFrozenState = state } + .launchIn(modelScope) + } + + private fun fetchAddToWalletBanner() { + modelScope.launch { + val isDone = cardDetailsRepository.isAddToWalletDone(params.userWalletId).getOrNull() == true + if (!isDone) { + uiState.update { state -> + state.copy( + addToWalletBlockState = AddToWalletBlockState( + onClick = ::onClickAddToWallet, + onClickClose = ::onClickCloseBanner, + ), + ) + } + } + }.saveIn(addToWalletBannerJobHolder) + } + + private fun onClickAddToWallet() { + router.push(TangemPayDetailsInnerRoute.AddToWallet) + } + + private fun onClickCloseBanner() { + modelScope.launch { + cardDetailsRepository.setAddToWalletAsDone(params.userWalletId) + uiState.update { it.copy(addToWalletBlockState = null) } + }.saveIn(addToWalletBannerJobHolder) + } + + override fun onClickChangePin() { + bottomSheetNavigation.dismiss() + router.push(TangemPayDetailsInnerRoute.ChangePIN) + } + + override fun onDismissViewPin() { + bottomSheetNavigation.dismiss() + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt new file mode 100644 index 0000000000..20a8d78463 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt @@ -0,0 +1,182 @@ +package com.tangem.features.tangempay.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.WindowInsets +import androidx.compose.foundation.layout.exclude +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults +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.tooling.preview.Preview +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.components.appbar.AppBarWithBackButton +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.domain.visa.model.TangemPayCardFrozenState +import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent +import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM +import com.tangem.features.tangempay.entity.TangemPayCardPageSetting +import com.tangem.features.tangempay.entity.TangemPayCardPageUM +import kotlinx.collections.immutable.ImmutableList + +@Composable +internal fun TangemPayCardPageScreen( + state: TangemPayCardPageUM, + cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent, + cardDetailsState: TangemPayCardDetailsUM, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + AppBarWithBackButton( + modifier = Modifier.statusBarsPadding(), + onBackClick = state.onBackClick, + ) + }, + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.exclude(WindowInsets.navigationBars), + containerColor = TangemTheme.colors.background.secondary, + ) { scaffoldPaddings -> + val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(scaffoldPaddings), + contentPadding = PaddingValues( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, + ), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + item(key = "Card") { + cardDetailsBlockComponent.CardDetailsBlockContent( + modifier = Modifier.padding(top = TangemTheme.dimens.spacing8), + state = cardDetailsState, + ) + } + if (state.addToWalletBlockState != null) { + item(key = "GooglePay") { + TangemPayAddToWalletBlock( + state = state.addToWalletBlockState, + ) + } + } + item(key = "Settings") { + TangemPayCardPageSettingsBlock( + settings = state.settings, + onSettingClick = state.onSettingClick, + ) + } + } + } +} + +@Composable +private fun TangemPayCardPageSettingsBlock( + settings: ImmutableList, + onSettingClick: (TangemPayCardPageSetting) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.primary, + shape = TangemTheme.shapes.roundedCornersMedium, + ), + ) { + Text( + modifier = Modifier.padding( + start = TangemTheme.dimens.spacing12, + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing4, + ), + text = stringResourceSafe(R.string.tangempay_card_page_settings_title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.tertiary, + ) + settings.fastForEach { item -> + TangemPayCardPageSettingRow( + item = item, + onClick = { onSettingClick(item) }, + ) + } + } +} + +@Composable +private fun TangemPayCardPageSettingRow( + item: TangemPayCardPageSetting, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(TangemTheme.dimens.spacing12), + contentAlignment = Alignment.CenterStart, + ) { + Text( + text = stringResourceSafe(item.titleRes), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + } +} + +private val TangemPayCardPageSetting.titleRes + get() = when (this) { + TangemPayCardPageSetting.ChangePIN -> R.string.tangempay_card_details_change_pin + TangemPayCardPageSetting.FreezeCard -> R.string.tangempay_card_details_freeze_card + TangemPayCardPageSetting.ReplaceCard -> R.string.common_error // TODO v_rodionov #[REDACTED_TASK_KEY] + } + +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun preview() = TangemThemePreview { + TangemPayCardPageScreen( + state = TangemPayCardPageUM.stub(), + cardDetailsBlockComponent = PreviewTangemPayCardDetailsBlockComponent( + TangemPayCardDetailsUM( + number = "•••• •••• •••• 1245", + numberShort = "··1245", + expiry = "••/••", + cvv = "•••", + onCopy = { _, _ -> }, + onClick = {}, + cardFrozenState = TangemPayCardFrozenState.Unfrozen, + ), + ), + cardDetailsState = TangemPayCardDetailsUM( + number = "•••• •••• •••• 1245", + numberShort = "··1245", + expiry = "••/••", + cvv = "•••", + onCopy = { _, _ -> }, + onClick = {}, + cardFrozenState = TangemPayCardFrozenState.Unfrozen, + ), + ) +} \ No newline at end of file From b58be5612edf400089a07dc0585926f6bedc43d4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 6 Apr 2026 12:35:57 +0300 Subject: [PATCH 12/32] Updated on 2026-08-14 --- core/res/src/main/res/values-de/strings.xml | 41 +- core/res/src/main/res/values-es/strings.xml | 105 ++- core/res/src/main/res/values-fr/strings.xml | 8 + core/res/src/main/res/values-ja/strings.xml | 8 +- .../src/main/res/values-pt-rBR/strings.xml | 27 + .../src/main/res/values-uk-rUA/strings.xml | 8 + .../src/main/res/values-zh-rCN/strings.xml | 653 +++++++++++++++++- core/res/src/main/res/values/strings.xml | 8 + features/tokendetails/impl/build.gradle.kts | 8 + .../DefaultTokenDetailsComponent.kt | 1 - .../model/TokenDetailsDialogFactory.kt | 73 ++ .../tokendetails/model/TokenDetailsModel.kt | 220 +++--- .../state/TokenDetailsStateController.kt | 54 ++ .../tokendetails/state/TokenDetailsUM.kt | 36 +- ...InitializeWithCryptoCurrencyTransformer.kt | 23 + .../transformer/SetTopBarTitleTransformer.kt | 78 +++ .../UpdateTopBarMenuTransformer.kt | 51 ++ .../tokendetails/ui/TokenDetailsScreen.kt | 148 +++- .../tokendetails/ui/TokenDetailsTopBar.kt | 478 +++++++++++++ ...ializeWithCryptoCurrencyTransformerTest.kt | 128 ++++ .../SetTopBarTitleTransformerTest.kt | 204 ++++++ .../UpdateTopBarMenuTransformerTest.kt | 214 ++++++ 22 files changed, 2423 insertions(+), 151 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsTopBar.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt create mode 100644 features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml index 07bf709771..6582af4d32 100644 --- a/core/res/src/main/res/values-de/strings.xml +++ b/core/res/src/main/res/values-de/strings.xml @@ -306,6 +306,7 @@ Stunde Importieren In Arbeit + Unzureichende Mittel Später Mehr erfahren %1$s übrig @@ -421,6 +422,7 @@ Token existiert bereits Dezimalzahl muss eine gültige Ganzzahl sein, bis zu %d Benutzerdefinierte Ableitung(derivation) + Die Funktion \"Dynamische Adressen\" ist aktiviert. Benutzerdefinierte \\"change\\" und \\"index\\" sind nicht verfügbar. E. g. m/00\'/0000\'/0\'/0/0 Benutzerdefinierte Ableitung (Derivation) eingeben Dezimalstellen @@ -473,6 +475,19 @@ Das Senden von Vermögenswerten in anderen Netzwerken führt zu dauerhaftem Verlust. %s Netzwerk Sende Geld nur mit + Dynamische Adressen + Dynamische Adressen ist aktiviert. Die benutzerdefinierten Funktionen \"Ändern\" und \"Index\" sind nicht verfügbar. + Einige Adressen fehlen + Verwenden Sie für jede Transaktion eine neue Adresse, um die Rückverfolgbarkeit zu verringern und den Datenschutz in der Kette zu verbessern. + Verbesserter Datenschutz + Einfacher Geldempfang in UTXO-basierten Netzwerken mit automatischer Adressgenerierung - keine manuelle Adressverwaltung erforderlich. + Nahtloser Empfang + Dynamische Adressen aktivieren + Bei dynamischen Adressen wird jedes Mal eine neue Adresse erstellt, um den Datenschutz zu gewährleisten - Ihr Gesamtguthaben bleibt gleich. + Dynamische Adressen können nicht aktiviert werden, da einige benutzerdefinierte Adressen/Tokens einen geänderten Ableitungspfad verwenden, der die erforderlichen Kriterien nicht erfüllt. + Nicht verfügbar + Wir können im Moment keine Verbindung zum Provider herstellen. Bitte versuchen Sie es später noch einmal. + Der Dienst ist nicht verfügbar. Bitte versuchen Sie es erneut. Beste Gelegenheiten Filter löschen Die Liste ist vorübergehend leer, da sie gerade aktualisiert wird. Schauen Sie in Kürze wieder rein. @@ -749,12 +764,18 @@ Die Daten dieses Abschnitts stammen aus den folgenden Netzwerken: %s Die Daten konnten nicht geladen werden... Keine Daten + **Hinzufügen zu Ihrem Portfolio**, um mit dem Kauf, Tausch oder Erhalt dieses Vermögenswerts zu beginnen + Zum Portfolio hinzufügen Marktimpuls Schnelle Aktionen + Alles löschen Markt durchsuchen + Neueste + In Ihrem Portfolio Ergebnis Token unter 100k USD Marktkapitalisierung anzeigen Token anzeigen + Krypto, Nachrichten und mehr Kein Ergebnis Netzwerk auswählen Wallet auswählen @@ -827,9 +848,11 @@ Position im Krypto-Rating zwischen allen Coins basierend auf der Marktkapitalisierung Marktposition Maximale Versorgung + Zirkulation und maximale Versorgung Die maximale Anzahl von Coins oder Tokens, die jemals für eine bestimmte Kryptowährung existieren können Maximale Versorgung Metriken + Nicht begrenzt Offizielle Links Preisleistung Aufbewahrungsort @@ -1104,10 +1127,14 @@ Einstellungen Du hast keinen Zugriff auf deine Kamera gewährt Kamerazugriff verweigert + Der angeforderte Token wurde Ihrer Wallet nicht hinzugefügt. Bitte fügen Sie ihn hinzu und versuchen Sie es erneut. + Token nicht hinzugefügt Dieser QR-Code konnte leider nicht erkannt werden. Unerkannter QR-Code Dieses Netzwerk wird von keinem der von Ihnen hinzugefügten Token unterstützt. Fügen Sie einen unterstützten Token hinzu, um Krypto zu senden. Keine unterstützten Token gefunden + Dieser QR-Code enthält Parameter, die nicht erkannt werden: %s. Einige Zahlungsdetails können verloren gehen, wenn Sie fortfahren. + Unbekannte Parameter Kein Memo erforderlich %1$s ( %2$s ) im %3$s Netzwerk %1$s im %2$s Netzwerk @@ -1222,6 +1249,10 @@ Memo: %s Ungültiges Memo Abdeckung der Netzgebühren + + %d Token ist mit dieser Adresse nicht kompatibel + %d -Tokens sind mit dieser Adresse nicht kompatibel + Nonce Eindeutige Nummer für jede Transaktion. Verwende diese Nummer, um eine ausstehende Transaktion erneut zu senden oder abzubrechen. Nonce eingeben… @@ -1489,14 +1520,18 @@ Fehler bei der Gebührenschätzung. Bitte sende dein Feedback an den Support. Du wechselst Der Tausch dieser Menge ausgewählter Token hat erhebliche Auswirkungen auf den Preis und verringert dein Ergebnis. + Aufgrund geringer Liquidität erhalten Sie möglicherweise deutlich weniger. Versuchen Sie es mit einem kleineren Betrag oder einem anderen Anbieter. Hoher Einfluss auf den Preis Unzureichende Mittel + Nicht genügend Geldmittel, um diese Transaktion abzuschließen. Verringern Sie den zu erhaltenden Betrag oder fügen Sie weitere Mittel hinzu. Erlaubnis erteilen Tauschen Tauschen... Du erhältst Token auswählen Nicht verfügbar + Nicht genug Liquidität für diesen Handel. Reduzieren Sie den Betrag oder wählen Sie einen anderen Anbieter. + Handel zu groß Wir freuen uns über Ihr Feedback Tangem Pay jetzt in der Beta Karte eingefroren @@ -1567,7 +1602,7 @@ Empfangen ist jetzt nicht verfügbar Aufdecken Details anzeigen - Tausche beliebige Vermögenswerte in Deinem Portfolio gegen eine Karte. + Tausche beliebige Vermögenswerte in Deinem Portfolio für deine Karte. Kartendetails Karte entsperren Komm zurück zur App, falls du es vergisst. @@ -1657,6 +1692,7 @@ Token in %%image%% %1$s Netzwerk Der %1$s (%2$s) Token ist die Hauptwährung im %3$s Netzwerk und kann nicht versteckt werden, solange du andere Token dieses Netzwerks in der Liste aktiv hast. %s kann nicht ausgeblendet werden + N / A QR-Code anzeigen Tausche diesen Token gegen einen anderen zu %1$s Servicegebühren von Februar %2$s-%3$s. Tausche mit Changelly, %s Gebühren @@ -1699,6 +1735,9 @@ Es ist ein Fehler aufgetreten. Fehlercode: %s. Bitte kontaktiere unseren Support. Verwende %s oder scanne eine Karte oder Ring, um Zugriff auf deine Wallet zu erhalten. Verbindung fehlgeschlagen: Diese dApp verwendet Wallet Connect Version 1.0, die nicht unterstützt wird. Bitte stelle sicher, dass die dApp Wallet Connect Version 2.0 unterstützt, um eine erfolgreiche Verbindung herzustellen. + Die vorherige Genehmigung wird widerrufen und eine neue erteilt. Das Netzwerk erhebt eine Token-Genehmigungsgebühr für jede dieser Aktionen. Als Beweis für den Widerruf wird in der Historie eine Transaktion mit einem Betrag von Null angezeigt. + Die Transaktion übersteigt den zuvor genehmigten Betrag.\nErlaubnis zum Fortfahren aktualisieren + Erlaubnis zur Aktualisierung Upgrade auf Hardware-Wallet Bleib auf dem Laufenden mit den neuesten Funktionen und Neuigkeiten Echtzeit-Warnungen für Transaktionen, Umtausch und wichtige Aktualisierungen. diff --git a/core/res/src/main/res/values-es/strings.xml b/core/res/src/main/res/values-es/strings.xml index b257cbe438..970e3f83af 100644 --- a/core/res/src/main/res/values-es/strings.xml +++ b/core/res/src/main/res/values-es/strings.xml @@ -161,7 +161,7 @@ Se requiere atención Se produjo un error al procesar tu código promocional. Inténtalo de nuevo más tarde. Error de activación - Tu código promocional se activó correctamente. Una recompensa se acreditará en tu cuenta dentro de 14 días. + Su código promocional se activó correctamente. Se acreditará la recompensa en su cuenta dentro de 14 días. Código promocional activado Este código promocional ya ha sido utilizado y no puede activarse de nuevo. Código no disponible @@ -306,6 +306,7 @@ hora Importe En progreso + Fondos insuficientes Más tarde Más información %1$s quedan @@ -326,6 +327,7 @@ NFT No Ninguna dirección + Sin resultados No agregadas No disponible Ahora no @@ -384,6 +386,7 @@ A A %s Hoy + Token para enviar %d token %d tokens @@ -396,6 +399,7 @@ Entiendo Entiendo, continuar Hubo un error. Por favor inténtelo de nuevo. + Desbloquear Inaccesible Termine el staking Debido a limitaciones sobre %1$s, solo %2$d UTXO pueden caber en una sola transacción. Esto significa que solo puedes enviar %3$s o menos. Debe reducir la cantidad. @@ -405,7 +409,7 @@ semana con - Modo de rendimiento + Modo de Rendimiento Dirección de contrato copiada Redes disponibles La derivación de su token coincide con la derivación de %1$s. Su token se añadirá a esta cuenta. @@ -418,6 +422,7 @@ Este token ya existe Los decimales deben ser un número entero válido, hasta %d Derivación personalizada + La opción Direcciones dinámicas está habilitada. Las opciones personalizadas \\"cambiar\\" e \\"índice\\" no están disponibles. Por ejemplo m/00\'/0000\'/0\'/0/0 Introduzca una derivación personalizada Decimales @@ -460,7 +465,7 @@ Firmado Enviar comentarios Detalles - Puedes tener solo una billetera móvil a la vez. Puede actualizarse a una billetera fría Tangem o usarse junto con una nueva billetera fría. + Puede tener solo una billetera móvil. Puede actualizarse a una billetera fría Tangem o añadir una billetera de hardware nueva. Compruebe su conexión a internet o cambia a una red diferente Condiciones de uso Dirección por defecto @@ -470,6 +475,19 @@ Enviar activos a otras redes resultará en una pérdida permanente. Red de %s Envíe fondos utilizando solo + Direcciones dinámicas + La opción Direcciones dinámicas está habilitada. Las opciones personalizadas \"cambiar\" e \"índice\" no están disponibles. + Direcciones dinámicas habilitadas + Utilice una nueva dirección para cada transacción para reducir la trazabilidad y mejorar la privacidad on-chain. + Privacidad mejorada + Reciba fondos fácilmente en redes basadas en UTXO con generación automática de direcciones, sin necesidad de gestión manual de direcciones. + Recepción sin problemas + Habilitar direcciones dinámicas + Las direcciones dinámicas crean una nueva cada vez para mayor privacidad - su saldo total no varía. + Las direcciones dinámicas no pueden activarse porque algunas direcciones/tokens personalizadas utilizan una ruta de derivación modificada, que no cumple los criterios requeridos. + Direcciones dinámicas no disponibles + No podemos conectar con el proveedor en este momento. Vuelva a intentarlo más tarde. + Servicio no disponible. Por favor, inténtelo de nuevo. Las mejores oportunidades Limpiar filtro La lista está temporalmente vacía porque se está actualizando. Vuelva a consultarla en un momento. @@ -478,9 +496,9 @@ Filtrar por Mis redes Redes - Mayormente usado + Popular Sin resultados - Ganar + Modo Staking & Rendimiento Hola equipo de soporte, he encontrado un error con el código: %s Error de WalletConnect Ha usado una tarjeta de otra billetera. Toque la tarjeta asociada con esta billetera @@ -653,7 +671,7 @@ ¿Está seguro que desea hacer esto? Olvidar billetera Ir a copia de seguridad - Ver copia de seguridad + Ver frase de recuperación Olvidar la billetera Olvidar de todos modos Esta billetera tiene una copia de seguridad. Asegúrese de poder recuperarla antes de olvidarla. @@ -689,6 +707,9 @@ Acerque para escanear Toque para firmar Coloque la tarjeta o el anillo + Su billetera está sincronizada y lista. \n Faltan algunos tokens? + Billetera importada correctamente + Restaurando %d%% Ha actualizado la biometría, escanee su tarjeta o anillo para entrar Su saldo debe ser mayor que el valor de la tarifa para hacer una transferencia Saldo insuficiente @@ -700,6 +721,7 @@ Nivel de Mana Para hacer un seguimiento de sus criptomonedas y transacciones, agregue tokens Gestionar tokens + Escanee el código QR para enviar fondos o conectarse a una aplicación Para acceder a todas las redes necesita escanear la tarjeta Escanee su tarjeta o anillo Disfrute de %1$s comisiones de servicio en los swaps a través de Changelly a partir de febrero %2$s-%3$s @@ -734,15 +756,22 @@ APY %s Mi portafolio Mercado - Gane con Tangem + Modo Staking & Rendimiento Para generar direcciones para las redes seleccionadas, debe escanear su tarjeta Tangem Para agregar tokens, abra esta página o pulse sobre la barra de búsqueda + Deslice el dedo hacia arriba para explorar el mercado + Descubre nuevas gemas ocultas Los datos de este apartado proceden de las siguientes redes: %s No se pueden cargar los datos… Sin datos + **Añadir a su portafolio** para empezar a comprar, intercambiar o recibir este activo + En su portafolio Análisis del Mercado Acciones rápidas + Borrar todo Buscar tokens + Recientes + En su portafolio Resultado Ver tokens con marketcap inferior a 100.000$ Mostrar tokens @@ -799,6 +828,10 @@ Titulares El cambio en el número de poseedores de tokens dentro de un período de tiempo específico Titulares + D + M + S + A Ideas Enlaces Liquidez @@ -814,9 +847,11 @@ Posición en la clasificación de criptomonedas entre todas las monedas según la capitalización de mercado Evaluación de mercado Suministro máximo + Suministro circulante y máximo La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular Suministro máximo Métrica + Sin limitación Enlaces oficiales Rendimiento de precios Repositorio @@ -826,9 +861,11 @@ Suministro total La cantidad máxima de monedas o tokens que pueden existir para una criptomoneda en particular Suministro total + 24h Volumen de operaciones (24 horas) La cantidad total de una criptomoneda que se ha negociado en las últimas 24 horas, lo que indica el nivel de actividad y liquidez en el mercado. Volumen de operaciones (24 horas) + %s en total Volumen Tire hacia arriba o toque la barra de búsqueda para agregar tokens directamente desde el mercado Agregar tokens @@ -852,6 +889,7 @@ Tokens relacionados Noticias relacionadas Manténgase informado + Puntuación de tendencia NFC no está disponible en su dispositivo Acerca de NFT Activo NFT @@ -909,8 +947,8 @@ Obtén $10 en BTC con cada billetera \n ¡Date prisa! Black Friday: hasta 30% DESCUENTO Vamos - Crea el par perfecto de packs de Tangem. Por tiempo limitado. - 1+1: Compra una billetera y obtén un 50% dto. en la segunda + ¡Tiempo limitado! + 1+1: Compra una billetera y obtenga un 50% dto. en la segunda Únase ahora Comparta su código y gane 5 USDT por venta. Su amigo obtiene un 10% de descuento. ¡Obtenga RECOMPENSAS por cada amigo! @@ -1088,6 +1126,14 @@ Ajustes No ha dado acceso a su cámara Acceso a la cámara denegado + El token solicitado no se ha añadido a su billetera. Por favor, añádalo e inténtelo de nuevo. + Token no añadido + Lo sentimos, no se ha podido reconocer este código QR. + Código QR no reconocido + Esta red no es compatible con ninguno de sus tokens añadidos. Añada un token compatible para enviar cripto. + No se encontraron tokens compatibles + Este código QR contiene parámetros que no son reconocidos: %s. Si continúa, es posible que se pierdan algunos detalles de pago. + Parámetros desconocidos No se requiere nota %1$s (%2$s) en la red %3$s %1$s en la red %2$s @@ -1164,7 +1210,7 @@ Ha especificado una comisión inferior a la cantidad recomendada, lo que podría provocar un retraso en su transacción. ¿Continuar? Razón: %1$s\nCódigo: %2$s La transacción está incompleta - Convertir a otro token + Cambiar a otro token o red Montante Se enviará al destinatario Puede establecer su tarifa de transacción ajustando el valor en el campo Satoshi por vByte. @@ -1202,6 +1248,10 @@ Memo: %s Memo no válido Cobertura de tarifa de red + + El token %d no es compatible con esta dirección + Los tokens %d no son compatibles con esta dirección + Nonce Número único para cada transacción. Utilícelo para reenviar o cancelar una transacción pendiente. Introduzca nonce... @@ -1229,6 +1279,11 @@ Limitación de transacciones Opcional Por favor, alinee su código QR con el cuadrado para escanearlo. Asegúrese de escanear la dirección de la red %s. + Al utilizar un tipo de cambio fijo, el importe que recibe queda garantizado en el momento de la operación. Esto le protege de las fluctuaciones de precio durante la transacción. + Tasa fija + Un tipo variable significa que la cantidad final que recibe puede variar ligeramente en función de las condiciones del mercado entre el momento en que inicia y finaliza el swap. + Tasa flotante + La tasa es fija Reciente Destinatario No es una dirección válida @@ -1272,6 +1327,7 @@ El destinatario recibe %s ¿Seguro que desea cancelar la conversión? Se borrarán sus datos anteriores. Eliminar conversión + Algo salió mal. Inténtelo de nuevo. Enviar con swap Transacción enviada Escanee la tarjeta/anillo que quiere configurar @@ -1463,14 +1519,18 @@ Error en la estimación de la tarifa. Envíe sus comentarios al servicio de asistencia. Usted intercambia Hacer un intercambio de esta cantidad del token seleccionado causará un impacto significativo en el precio y reducirá su resultado. + Es posible que reciba una cantidad significativamente menor debido a la baja liquidez. Pruebe con una cantidad menor o con otro proveedor. Alto impacto en los precios Fondos insuficientes + No hay fondos suficientes para completar esta transacción. Reduzca el importe a recibir o añada más fondos. Dar autorización Intercambiar Intercambiando... Usted recibe Elige token no disponible + No hay liquidez suficiente para esta operación. Reduzca el importe o elija otro proveedor. + Operación demasiado grande Nos encantaría recibir tus comentarios Tangem Pay ya está en beta Tarjeta congelada @@ -1509,6 +1569,7 @@ Ocultar verificación de la pantalla Agregar fondos Opciones de recarga + Añadir a Google Wallet Número de tarjeta Modificar PIN La tarjeta está completamente lista para pagos. @@ -1596,7 +1657,7 @@ Sesión expirada Restablecer acceso Usa USDC para pagos cotidianos - Tangem Pay temporalmente no disponible + Tangem Pay no está disponible temporalmente. Tangem Pay Haga clic en el botón de abajo para restaurar el acceso Tu saldo USDC Polygon on-chain difiere de tu saldo de tarjeta y se actualiza dentro de 2 días hábiles después de una compra. Los fondos de compras reembolsadas no se devolverán a tu saldo on-chain ni estarán disponibles para retiro, pero permanecerán en tu saldo de tarjeta para compras. @@ -1630,6 +1691,7 @@ Token en la red %%image%% %1$s El token %1$s (%2$s) es la moneda principal en la red %3$s y no se puede ocultar mientras tengas otros tokens de esta red en la lista No se puede ocultar %s + N/A Mostrar código QR Cambie este token por otro por una tarifa de servicio de %1$s del %2$s al %3$s de febrero. Intercambie con Changelly, %s comisiones @@ -1672,6 +1734,9 @@ Hemos encontrado un error. Código de error: %s. Póngase en contacto con nuestro servicio de soporte. Use %s o escanee una tarjeta/anillo para tener acceso a su billetera Error de conexión: Esta dApp utiliza la versión 1.0 de Wallet Connect, que no es compatible. Asegúrese de que la dApp sea compatible con la versión 2.0 de Wallet Connect para conectarse correctamente. + Se revocará el permiso anterior y se emitirá uno nuevo. La red cobrará una tarifa de aprobación de tokens por cada una de estas acciones. Verá una transacción de importe cero en el historial como comprobante de la revocación. + La transacción excede el límite de permisos otorgados previamente.\nActualice los permisos para continuar. + Actualizar permisos Actualización a billetera de hardware Manténgase actualizado con las últimas funciones y noticias Alertas en tiempo real de transacciones, intercambios y actualizaciones críticas. @@ -1842,7 +1907,7 @@ Este código de acceso protege su billetera y se utiliza para iniciar sesión y firmar transacciones. Establecer/Cambiar código de acceso Cambiar código de acceso - Manténgase informado sobre las transacciones entrantes de la billetera y las actualizaciones de Tangem. + Reciba notificaciones push para las transacciones entrantes en su billetera. Es posible que las notificaciones push no funcionen actualmente en dispositivos Huawei. Estamos trabajando activamente en una solución y la publicaremos en una próxima actualización. ¡Gracias por su comprensión! Notificaciones de transacciones Establecer código de acceso @@ -1870,7 +1935,7 @@ Según la documentación oficial de Clore, todas las monedas recibidas antes del 21 de diciembre serán migradas a Clore (token ERC-20); las monedas recibidas después de esa fecha no lo serán. Se está desarrollando una solución de transferencia — mantente atento. Mensaje Abrir el portal de reclamaciones - Para mantener el acceso a sus fondos, comience la migración de acuerdo con las pautas oficiales de Clore. + Para continuar utilizando sus tokens de Clore, debe completar la migración de tokens de acuerdo con la información del Portal de Reclamación. Migración de la red Clore Firmar Firma @@ -1925,7 +1990,7 @@ La red no está disponible actualmente. Por favor, inténtalo de nuevo más tarde. La red no está disponible Recargue su billetera - Su billetera no ha sido respaldado. Realice este procedimiento para proteger sus activos ahora. + Su billetera no tiene aún una copia de seguridad. Hágala ahora para proteger sus activos. Falta backup Esta tarjeta se ha utilizado previamente para transacciones. Si la recibió de una fuente no confiable, considere retirar todos los fondos. Si es su tarjeta, no se requiere ninguna acción. La tarjeta ya ha firmado transacciones @@ -2099,7 +2164,7 @@ La tasa de red es actualmente demasiado alta para ejecutar préstamos. Los fondos se suministrarán una vez que baje a %1$s o menos. Mis fondos Su %1$s está ahora depositado en Aave y devenga intereses. Tiene un token%2$s, que representa su saldo y crece con el tiempo. Cuando deposite más fondos, estos se suministrarán a Aave para ganar intereses, menos una comisión por transacción. - Modo de rendimiento + Modo Rendimiento Ganancias totales Transferencias a Aave Explore Aave @@ -2111,7 +2176,7 @@ Tarifa máxima El monto mínimo se calcula en función de la comisión de la red actual para que no supere el 4%% del importe de la recarga, lo que hace que el mínimo sea %1$s (%2$s). Recarga mínima - Política de tarifas + Política de tarifas por recarga Tangem también cobra una comisión de servicio del 15% sobre el rendimiento obtenido. Sus fondos se suministrarán automáticamente a Aave una vez que las comisiones de red sean más bajas o su saldo alcance el importe mínimo requerido. Las comisiones son más altas de lo habitual debido a la alta actividad del mercado. Puede continuar ahora o volver a consultar más tarde cuando las comisiones sean más bajas. @@ -2131,7 +2196,7 @@ Aave es un protocolo on-chain para crear mercados de liquidez no custodiales y ganar intereses a tasa variable. Descentralizado y autocustodiado Al utilizar este servicio, usted acepta que el proveedor\n%1$s y %2$s - Conectar Aave + Conectar con Aave Consiga un %1$s%% APY\nen su saldo Aave %1$s%% - Tipo de interés variable Tipo de interés variable @@ -2144,7 +2209,7 @@ Cuando recargue, sus fondos se enviarán automáticamente a Aave para comenzar a ganar intereses. %s se deducirá para cubrir la tarifa de transacción. Suministro de activos Su %s se suministrará a Aave, pero seguirá siendo gestionable. - Ver política de tarifas + Ver política de tarifas por recarga Sus próximas recargas se suministrarán automáticamente a Aave. Todos tus futuros depósitos %1$s se suministrarán automáticamente a Aave. Activo @@ -2158,8 +2223,8 @@ APY Los intereses se devengan automáticamente Los intereses se devengan automáticamente - Modo de rendimiento - Procesando su depósito + Modo Rendimiento + Activando Modo Rendimiento Modo Rendimiento Implementación del contrato del Modo Rendimiento Modo Rendimiento activado diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 9075ee073a..5cfb59bdbc 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -1086,6 +1086,14 @@ Paramètres Vous n\'avez pas donné accès à votre caméra Accès à la caméra refusé + Le jeton demandé n\'est pas ajouté à votre portefeuille. Veuillez l\'ajouter, puis réessayez. + Le jeton n\'a pas été ajouté + Désolé, ce code QR n\'a pas pu être reconnu. + Code QR non reconnu + Ce réseau n\'est pris en charge par aucun des jetons que vous avez ajoutés. Ajoutez un jeton pris en charge pour envoyer des cryptomonnaies. + Aucun jeton pris en charge n\'a été trouvé + Ce code QR contient des paramètres non reconnus : %s. Si vous continuez, certaines informations de paiement risquent d\'être perdues. + Paramètres inconnus Aucun mémo requis %1$s (%2$s) sur le réseau %3$s %1$s sur le réseau %2$s diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index 404b67969c..c2dfddaf6f 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -415,6 +415,7 @@ トークンはすでに存在します 小数は%dまでの有効な整数である必要があります カスタム派生パス + 動的アドレスが有効です。カスタムの「change」と「index」は利用できません。 例:m/00\'/0000\'/0\'/0/0 カスタム派生パスを入力 小数 @@ -756,11 +757,13 @@ このセクションのデータは、次のネットワークから取得されています: %s データを読み込めません… データなし + **ポートフォリオに追加して**、この資産の買付・交換・受け取りを始めましょう + ポートフォリオ内 マーケット動向 クイックアクション すべてクリア トークンを探す - 最近 + 最近の検索 ポートフォリオ内 結果 時価総額10万ドル以下のトークンを見る @@ -1712,6 +1715,9 @@ エラーが発生しました。エラー コード:%s。サポートにお問い合わせください。 %sを使用するか、カード / リングをスキャンしてウォレットにアクセスしてください 接続に失敗しました:このdAppは、サポートされていないWallet Connectバージョン1.0を使用しています。正常に接続するには、dAppがWallet Connectバージョン2.0をサポートしていることを確認してください。 + 以前の承認は取り消され、新しい承認が発行されます。これらの各操作には、ネットワークによるトークン承認手数料がかかります。履歴には、取り消しの証拠として金額0の取引が表示されます。 + 取引金額が、以前に許可された承認額を超えています。\n続行するには、承認内容を更新してください。 + 承認を更新 ハードウェアウォレットにアップグレード 最新の機能とニュースをお届けします 取引・スワップ・重要な更新に関するリアルタイム通知。 diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index 94c47b4783..e931a23bdc 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -422,6 +422,7 @@ O token já existe. O valor decimal deve ser um número inteiro válido, até um limite de %d Derivação personalizada + O recurso de Endereços Dinâmicos está ativado. As opções personalizadas \"alterar\" e \"indexar\" não estão disponíveis. Exemplo: m/00\'/0000\'/0\'/0/0 Insira uma derivação personalizada Decimais @@ -474,6 +475,19 @@ O envio de ativos por outras redes resultará em perda permanente. %s rede Envie fundos usando apenas + Endereços dinâmicos + O recurso de Endereços Dinâmicos está ativado. As opções personalizadas \"alterar\" e \"indexar\" não estão disponíveis. + Endereços dinâmicos ativados + Use um novo endereço para cada transação para reduzir a rastreabilidade e melhorar a privacidade na blockchain. + Privacidade aprimorada + Receba fundos facilmente em redes baseadas em UTXO com geração automática de endereços — sem necessidade de gerenciamento manual de endereços. + Recebimento perfeito + Habilitar endereços dinâmicos + Endereços dinâmicos criam um novo a cada vez para maior privacidade — seu saldo total permanece o mesmo. + Não é possível ativar os endereços dinâmicos porque alguns endereços/tokens personalizados usam um caminho de derivação modificado, que não atende aos critérios necessários. + Endereços dinâmicos indisponíveis + Não foi possível conectar-se ao provedor neste momento. Tente novamente mais tarde. + Serviço indisponível. Por favor, tente novamente. Melhores oportunidades Limpar filtro A lista está temporariamente vazia, pois está sendo atualizada. Volte daqui a pouco. @@ -750,9 +764,14 @@ Os dados desta seção são provenientes das seguintes redes: %s Não foi possível carregar os dados… Sem dados + **Adicione ao seu portfólio** para começar a comprar, trocar ou receber este ativo. + Em seu portfólio Pulso do mercado Ações rápidas + Limpar tudo Pesquisar tokens + Recentes + Em seu portfólio Resultado Veja tokens com capitalização de mercado inferior a 100 mil dólares. Mostrar tokens @@ -1114,6 +1133,8 @@ Código QR não reconhecido Esta rede não é compatível com nenhum dos tokens adicionados. Adicione um token compatível para enviar criptomoedas. Nenhum token compatível encontrado + Este código QR contém parâmetros que não são reconhecidos: %sAlgumas informações de pagamento podem ser perdidas se você continuar. + Parâmetros desconhecidos Não é necessário memorando %1$s (%2$s) sobre %3$s rede %1$s sobre %2$s rede @@ -1499,6 +1520,7 @@ Erro na estimativa de custos. Por favor, envie seu feedback para o suporte. Você troca Trocar essa quantidade de tokens selecionados causará um impacto significativo no preço e reduzirá seu resultado. + Você pode receber um valor significativamente menor devido à baixa liquidez. Tente um valor menor ou outro provedor. Alto impacto nos preços Fundos insuficientes Não há fundos suficientes para concluir esta transação. Reduza o valor a receber ou adicione mais fundos. @@ -1508,6 +1530,8 @@ Você recebe Escolha o token não disponível + Liquidez insuficiente para esta transação. \n o valor ou escolha outro provedor. + Negociação em grande escala Teremos todo o prazer em receber seu feedback. O Tangem Pay agora está em versão beta. Cartão bloqueado @@ -1711,6 +1735,9 @@ Ocorreu um erro. Código do erro: %sPor favor, entre em contato com nosso suporte. Usar %s ou escaneie um cartão/anel para acessar sua carteira. Falha na conexão: Este aplicativo descentralizado (dApp) usa a versão 1.0 do Wallet Connect, que não é compatível. Certifique-se de que o dApp seja compatível com a versão 2.0 do Wallet Connect para que a conexão seja estabelecida com sucesso. + A permissão anterior será revogada e uma nova será emitida. A rede cobrará uma taxa de aprovação de tokens para cada uma dessas ações. Você verá uma transação de valor zero no histórico como comprovante de revogação. + A transação excede o limite de permissão concedido anteriormente.\nAtualize a permissão para prosseguir. + Atualizar permissão Faça upgrade para uma carteira de hardware. Fique por dentro das últimas novidades e recursos. Alertas em tempo real para transações, câmbio e atualizações críticas. diff --git a/core/res/src/main/res/values-uk-rUA/strings.xml b/core/res/src/main/res/values-uk-rUA/strings.xml index 8df7a978d6..ffcd7afc54 100644 --- a/core/res/src/main/res/values-uk-rUA/strings.xml +++ b/core/res/src/main/res/values-uk-rUA/strings.xml @@ -1124,6 +1124,14 @@ Налаштування Ви не надали доступ до своєї камери Доступ до камери заборонено + Запитуваний токен не додано до вашого гаманця. Будь ласка, додайте його та спробуйте ще раз. + Токен не додано + На жаль, цей QR-код не вдалося розпізнати. + Нерозпізнаний QR-код + Ця мережа не підтримується жодним із ваших доданих токенів. Додайте підтримуваний токен, щоб надіслати криптовалюту. + Підтримуваних токенів не знайдено + Цей QR-код містить нерозпізнані параметри: %s. Деякі деталі платежу можуть бути втрачені, якщо ви продовжите. + Невідомі параметри Memo не вимагається %1$s (%2$s) у мережі %3$s %1$s у мережі %2$s diff --git a/core/res/src/main/res/values-zh-rCN/strings.xml b/core/res/src/main/res/values-zh-rCN/strings.xml index 9a07214403..13a1972ba4 100644 --- a/core/res/src/main/res/values-zh-rCN/strings.xml +++ b/core/res/src/main/res/values-zh-rCN/strings.xml @@ -1,19 +1,650 @@ - 添加代幣 - 沒有結果 - 要發送的代幣 - 解鎖 - 您的錢包已同步並準備就緒。\n缺少某些代幣? - 錢包導入成功 - 恢復%d%% + 更改代码 + 您的访问代码可解锁和保护您的钱包访问权限 + 无论如何都要用 + 这个访问码很容易猜到。 + 您不能创建超过 %1$s 账户。存档一个账户以添加新账户。 + 无法添加新帐户 + 账户已存档 + 已存档账户 + 您即将恢复 \"%1$s\"。 + 恢复账户 + 您已达到20个活跃账户的上限。请归档一个账户以恢复此账户。 + 无法恢复帐户 + 我们无法存档账户。请稍后再试。 + 此账户正在参与推荐计划。 + 此帐户无法存档 + 我们无法创建账户。请稍后再试。 + 账户已创建 + 存档... + 无法编辑帐户 + 账户已保存 + 奖励账户 + 已存在使用此名称的帐户。请选择其他名称。 + 帐户名已被使用 + 请稍后再试。如果问题仍然存在,请联系客服,我们将协助您解决问题。 + %1$s 在 %2$s + 主账户 + 您已超过 %1$s 活动账户的限制。存档一个以恢复 + 无法恢复帐户 + 账户已恢复 + 长按账户可重新排序 + 一些自定义代币将自动从 \"%1$s\" 移至 \"%2$s\",因为它们的派生词属于该账户。 + 部分自定义代币将自动移动 + 生物识别出现问题。请尝试重置设备上的生物识别技术或联系技术支持。 + 验证错误 + 设置访问码以启用生物识别功能 + 稍后会要求您提供访问代码以进行安全存储。 + 这将删除所有已保存的钱包访问码。您需要重新输入访问码才能使用钱包。 + 您的设备已关闭生物识别功能,因此无法使用此功能解锁钱包。请在设备设置中启用生物识别功能,即可再次使用此方法。 + 生物识别认证已禁用 + 您的生物识别尝试次数已达上限。请使用卡片/指环解锁钱包,或输入您的访问码。 + 生物识别认证已锁定 + 生物识别登录暂时锁定。请30秒后重试,或轻触设备或输入访问码解锁您的钱包。 + 您设备上的生物识别信息已更新。请选择您的钱包并输入其访问码以重新启用生物识别登录。 + 需要注意 + 处理您的优惠码时出错,请稍后再试。 + 激活错误 + 您的优惠码已成功激活。奖励将在14天内计入您的比特币账户。 + 促销代码已激活 + 此优惠码已被使用,无法再次激活。 + 代码不可用 + 此优惠码无效,无法激活。 + 无效代码 + 您需要提供比特币地址才能领取奖励。请将比特币地址添加到您的钱包,然后重试激活。 + 需要比特币地址 + 请重置下一个设备以继续。 + 钱包重置 + 所有 Tangem 设备均已重置。您现在可以继续升级钱包。 + 再次升级 + 重置完成 + 我们建议在此钱包中完成所有 Tangem 设备的重置过程。 + 部分 Tangem 设备仍需重置。 + 账户 + 账户 + 激活 + 添加 + 添加代币 + 已添加 + + 选择账户 + 联系客服 + 充值网络费 + 忘记 + + 从 %s + 开始 + 获取代币 + 保持到 %s + 余额不足 + 了解更多 + 传统比特币 + 锁定的钱包 + + %d网络 + + 新地址 + 新闻 + 无结果 + 不可用 + 或者 + 推荐 + 重置 + 出问题了 + Tangem + 点击并按住 + + 到 %s + 要发送的代币 + 无法加载数据…… + 我明白,请继续 + 解锁 + 钱包 + 收益模式 + 您的代币派生信息与 %1$s的派生信息一致。 您的代币将被添加到该账户。 + 派生属于另一个账户。 + 代币已添加到 %1$s 帐户 + 动态地址已启用。自定义\“更改”\和\“索引”\不可用。 + 升级到硬件钱包 + 您只能拥有一个手机钱包。将其升级为 Tangem 硬件钱包,或添加一个新的硬件钱包。 + 默认地址 + 传统 %s 地址 + %s 地址 + 动态地址 + 动态地址已启用。自定义“更改”和“索引”功能不可用。 + 已启用动态地址 + 每次交易都使用一个新地址,以减少可追溯性并提高链上隐私性。 + 增强隐私 + 通过自动生成地址,轻松在基于UTXO的网络中接收资金——无需手动管理地址。 + 无缝接收 + 启用动态地址 + 动态地址每次都会创建一个新地址,以增强隐私保护——您的总余额保持不变。 + 无法启用动态地址,因为某些自定义地址/代币使用的是修改后的派生路径,不符合所需的标准。 + 动态地址不可用 + 我们目前无法连接到服务提供商,请稍后再试。 + 服务不可用,请稍后再试。 + 最佳机会 + 清除筛选 + 列表正在刷新,暂时为空。请稍后再查看。 + 所有网络 + 所有类型 + 筛选方式 + 我的网络 + 网络 + 热门 + 无结果 + 质押与收益模式 + 将您投资组合中的任何资产交换为该代币 + 有竞争力的费率 + 速度越快,确认速度越快,但网络费用也越高。 %s + 选择速度 + 选择用于支付网络费用的代币。 %s + 选择代币 + 市场与新闻 + Tangem AI + 当下热门 + 备份问题 + 资金不足 + 转账费 + 地址直接在您的 Tangem 硬件钱包上生成,随时可用,并受到全面保护。 + 新地址 + 添加 Tangem 钱包 + 您的私钥将直接在 Tangem 卡内生成,并且永远不会离开该卡。 + 密钥生成 + 所有加密操作都在安全芯片内部进行,该芯片经过认证,可防止克隆和物理篡改。 + 硬件级安全 + 您是否允许“Tangem”使用生物识别认证来确认您的身份并打开应用程序? + 让您的加密货币安全离线存储。轻薄如信用卡,安全胜过银行金库。 + 创建一个安全的钱包并转移资金,以加强保护。 + 创建新钱包 + 使用卓越的 Tangem 硬件钱包,提升您的安全保障。 + 硬件钱包 + 将您当前的手机钱包转换为 Tangem 冷钱包。 + 升级当前钱包 + 先完成备份 + 其他方法 + 将您的恢复短语保存在安全的地方,保持其私密性以保护您的资金,并设置访问密码以提高安全性。 + 要使用访问码保护您的钱包,请完成备份过程。 + 要升级到硬件钱包,请完成备份过程。 + 将您的手机钱包升级到 Tangem 硬件钱包,享受最高级别的安全性。导入您的钱包或将资金转移到新钱包。 + 升级到冷钱包 + 随时安全地将手机钱包转移到 Tangem 卡片或指环上。 + 升级到硬件钱包 + 导入现有钱包 + 此恢复短语已导入 + 忘记钱包 + 此钱包将从您的设备中永久移除。 + 你确定要这么做吗? + 忘记钱包 + 转到备份 + 查看恢复短语 + 忘记钱包 + 确定忘记 + 这个钱包有备份。在忘记钱包之前,请确保你能找回备份。 + 如果您忘记了这个钱包而又没有备份,您将永远无法使用您的资金。 + 你确定要忘记这个钱包吗? + 我知道,如果我在移除钱包之前没有备份钱包,我将无法访问钱包。 + 我知道移除钱包并不会将其删除,只会将其从我的设备中移除。 + 升级 + 当您将手机钱包升级为 Tangem 卡或指环时,您的所有地址和余额都可以完全访问。 + 相同地址 + 无法升级。此设备上已有一个钱包。 + 请换一台设备。这台设备不能用于升级。 + 操作过程中发生错误。 + 在此过程中,您的资金将保持安全且完全可用。 + 获得资金 + 升级后,您的移动钱包将从应用程序中移除并存储在您的 Tangem 硬件钱包中。您的助记词将保留在您身边。 + 一般安全 + 私钥将从应用程序转移到您的 Tangem 硬件钱包。 + 私钥迁移 + 扫描设备 + 开始升级 + 您即将升级到我们的硬件钱包。它会将您的资产安全地存储在冷存储中。 + Tangem钱包 + 升级到我们的硬件钱包 + 使用 Tangem 的顶级硬件钱包,确保您的加密货币安全。 + 将您的钱包升级到硬件安全级别 + 您的钱包已同步并准备就绪。\n缺少某些代币? + 钱包已成功导入 + 恢复 %d%% + 扫描二维码即可发送资金或连接到应用程序 + 关于硬币 + 质押与收益模式 + 向上轻扫,探索市场 + 寻找新的隐藏宝石 + **添加到您的投资组合**,即可开始购买、交换或接收此资产 + 在您的投资组合中 + 市场脉搏 + 全部清除 + 最近的 + 在您的投资组合中 + 加密货币、新闻及更多 + 收益模式 + 代币已添加 - + - 使用固定利率時,您在置換交易時收到的金額將被鎖定,這可以保護您免受交易過程中價格波動的影響。 + 循环和最大供应 + 无限制 + 24小时 + %s 总共 + 添加更多代币 + 增强资产性能,并使其能够即时访问。 %s + 激活收益模式 + 创建手机钱包前必须更新至 %1$s + 手机钱包需要 %1$s 或更高版本 + 所有新闻 + 喜欢 + + 小时之前 + + + 分钟之前 + + 快速回顾 + 新闻 + 相关代币 + 相关新闻 + 随时了解最新动态 + 趋势得分 + 每个钱包均可获得价值 10 美元的 BTC \n快来领取! + 黑色星期五:最多可省 30 美元% + 我们出发吧 + 限时优惠! + 1+1:买一个钱包,优惠 50% + 购买加密货币 + 通过 SEPA 转账购买加密货币时,可享受 ** 0% 费用**。 + 使用 SEPA 购买加密货币 + 加入候补名单,即可获得一张与众不同的支付卡。 + Tangem Visa卡 + 条款和条件 + 存款 100 美元以上,持有 30 天,即可获得 10 美元 + 加入 \"收益模式 \"活动 + 生物识别 + 您只能拥有一个手机钱包。将其升级为 Tangem 硬件钱包,或添加一个新的硬件钱包。 + 所有优惠 + 此交易已处理完毕,无需进一步操作。 + 获得最佳利率... + 即时 + 服务由外部供应商提供。\nTangem对此不承担任何责任。 + 最快处理 + 付款方式 + + 提供者 + + 提供商 + 最近使用过 + 推荐 + + 至多%d天 + + %s 分钟 + 可从 + 最高可提供 + 你得到 + 服务由外部供应商提供。\nTangem 不承担任何责任。 + 至多 + 通过 %s + 您将支付 + %s 支持 + 从图库中选择 + 设置 + 您尚未授予摄像头访问权限 + 摄像头访问权限被拒绝 + 您请求的代币未添加到您的钱包。请添加后重试。 + 代币未添加 + 抱歉,无法识别此二维码。 + 无法识别的二维码 + 您添加的所有代币均不支持此网络。请添加受支持的代币以发送加密货币。 + 未找到支持的代币 + 此二维码包含无法识别的参数: %s如果您继续操作,部分支付信息可能会丢失。 + 未知参数 + 奖励地址 + 我明白,我将完全失去对 Tangem Pay 卡及其上所有资金的访问权,且无法挽回 + 所有 Tangem 设备均已重置。 + 激活过程中出了问题。请逐一重置卡片。 + 卡片验证失败 + 请重置下一个设备以继续 + 拥有 root 权限的设备安全性较低。您的数据可能面临额外的风险。 + 检测到 root 访问 + 您发送 + + 此地址与代币不兼容 + + 使用固定利率时,您在互换交易时收到的金额将被锁定。这可以保护您免受交易过程中价格波动的影响。 固定利率 - 浮動利率意味著您最終收到的金額可能會根據您開始和完成置換之間的市場情況略有變化。 - 浮動利率 + 浮动利率意味着您最终收到的金额可能会根据您开始和完成互换之间的市场情况略有变化。 + 浮动利率 利率固定 + 出错了。请再试一次。 + 简单易用 + 让您的加密货币安全离线存储。轻薄如信用卡,安全胜过银行金库。 + 无助记词 + 一流的硬件钱包 + Tangem 冷钱包 + 网络费用是处理和确认您在区块链上的交易时所需的小额费用。 + 要开始质押,您必须通过交易充值 1 TON 来激活您的 TON 账户。资金会保留在您的账户中,因为此步骤仅用于激活账户以进行质押。 + 激活账户 + 网络费用已更改。请在继续操作前查看新金额。 + 网络费用已更新 + 年利率 %1$s%% + 您的质押奖励将在 5 个周期(约 25 天)后开始发放,在此期间您的委托将由网络注册和统计。 + 年收益率 + 年利率 (APY):显示您一年内通过复利计算可获得的总利息。复利是指您获得的利息会添加到您的本金中,因此您还可以获得该利息的利息。 + APY + 奖励将累积到您的质押余额中。已赚取资金: %s + 最高金额: %s + 要开始质押,您需要先激活您的 TON 账户。 + 激活账户 + Solana 的奖励会自动添加到您的质押余额中,无法单独显示。 + 批准后,您即允许智能合约在未来的交易中使用您的代币。 + 还在寻找其他代币?\n尝试搜索或探索其他加密货币! + 搜索任何代币,即使它还不在你的列表中。 + 使用搜索查找所需内容 + 您的资产 + 由于流动性低,您收到的资金可能会大大减少。请尝试较小的金额或另一个提供商。 + 账户余额不足,无法完成此交易。请减少收款金额或增加余额。 + 互换... + 此交易流动性不足,请减少金额或选择其他供应商。 + 交易额过大 + 我们非常乐意收到您的反馈。 + Tangem Pay 现已进入测试阶段 + 卡片已冻结 + 卡片支付 + 存款 + 争议 + 探索交易 + 服务费 + 费用 + 确保您的资金安全。您可以随时解冻。 + 冻结您的卡片? + 冻结卡片失败,请稍后再试。 + 冻结 + 您的卡片已被冻结。 + 获取帮助 + 原因: %s + %s · %s + MCC %s + 其他 + 无法在已 root 的设备上使用 + 已完成 + 拒绝 + 待定 + 已反转 + 条款、费用和限制 + 条款和限制 + 银行拒绝了这项交易请求。 + 这笔费用用于支付您办理转账时的费用。 + 商家部分或全部撤销了交易 + 继续使用您的资金。您可以随时冻结资金。 + 要解冻您的卡片? + 卡片解冻失败,请稍后再试。 + 您的卡片已解冻。 + 提款 + 无法在已root的设备上使用 + 从主屏幕隐藏 KYC 页面 + 增加资金 + 充值选项 + 添加到 Google 钱包 + 卡号 + 更改PIN码 + 该卡已完全准备好用于支付。 + 已创建 PIN 码 + CVC + 数据加载失败,请稍后再试。 + 到期 + 冻结卡片 + 隐藏详情 + 隐藏 + 打开 Google 钱包 + 只需轻点几下即可设置 Tangem Pay,然后即可开始使用 Google Pay 付款。 + 只需轻点几下即可设置 Tangem Pay,然后即可开始使用 Apple Pay 付款。 + 将您的卡片添加到 Google Pay + 将您的卡片添加到 Apple Pay + 打开 Google 钱包 + 点击右上角的“+”按钮 + 打开 Apple 钱包 + 点击“添加卡片” + 点击“借记卡或信用卡” + 手动输入卡片信息 + 使用发送到您设备上的 OTP 来验证卡片。 + 一切就绪!您的卡已可以使用。 + 将卡片添加到 Google Pay + 将卡片添加到 Apple Pay + PIN码 + 分享您的地址或出示二维码 + 检测到技术问题。请稍后再试或联系技术支持。 + 目前无法接收 + 显示 + 显示详情 + 将您投资组合中的任何资产互换到卡片 + 卡片详情 + 解冻卡片 + 如果忘记了,请返回应用查看。 + 您的PIN码 + 提款 + 目前无法提款 + 当前提款交易完成之前,您无法发起新的提款交易或互换交易。 + 提款进行中 + 卡片设置 + 更改PIN码 + 如果忘记了,请返回应用程序。 + 我明白,我将完全失去对 Tangem Pay 卡及其上所有资金的访问权,且无法挽回 + 发卡失败 + 发生技术故障,请点击下方按钮重试。 + 发生技术故障,请联系技术支持。 + 免费领取您的 Tangem Visa 虚拟卡 + 获取 Tangem Pay + 前往支持页面 + 通常需要最多15分钟 + 设置您的 Tangem 卡 + 正在发放您的卡片 + 该卡通常会在 5 分钟内自动发放。极少数情况下,如果需要人工审核,则可能需要长达 48 小时。 + Tangem Pay + 确认取消 + 您确定要停止 KYC 流程吗?您可以随时返回继续。 + 我们无法验证您的个人资料。如有任何疑问,请联系客服。 + 很抱歉,我们无法验证您的身份。 + KYC 被拒 + KYC正在进行中 + 查看状态 + Tangem Pay 的 KYC 流程正在进行中 + 文件通常会在 5 分钟内自动验证。极少数情况下,如果需要人工审核,则可能需要长达 48 小时。 + 已拒绝 + 隐藏 KYC 块 + 抱歉,我们无法验证 + 您的个人资料。 + 免费领取您的 Tangem Visa 虚拟卡 + 使用 USDC 进行日常支付 + 获取卡片 + 数字卡可与 Apple Pay 和 Google Pay 一起使用 + 随意支配您的资产 + 购买无需支付任何额外费用 + 实际支付金额与所示金额一致 + 将在不透露您的地址和资产的情况下创建一个单独的付款账户 + 无与伦比的隐私保护 + 几分钟内即可获得免费的 Tangem Pay 卡 + 支付账户 + 支付账户未同步 + 无效PIN码:请避免使用连续或重复的密码。 + 我们正在修复技术问题,请稍后再试。 + 服务暂时不可用 + 无法显示详细信息。但刷卡支付功能仍然可用。 + 设置 PIN 码 + 会话已过期 + 恢复访问权限 + 使用 USDC 进行日常支付 + Tangem Pay暂时无法使用。 + Tangem Pay + 点击下方按钮恢复访问权限 + 您的Polygon链上 USDC 余额与您的卡片余额不同,并在购买后 2 个工作日内更新。购物退款的资金不会退还至您的链上余额,也不能提现,但会保留在您的卡片余额中用于购物。 + 请注意 + 您的PIN码 + 授权已被撤销。您的资金仍处于收益模式。如需执行操作,请进入收益模式并重新授予权限。 + 不适用 + 显示二维码 + 为 %s + 之前的授权将被撤销,并颁发新的授权。网络将对每次此类操作收取代币批准费。您将在交易记录中看到一笔金额为零的交易,作为撤销授权的证据。 + 交易金额超过先前授权的金额。\n请更新授权以继续 + 更新权限 + 升级到硬件钱包 + 实时提醒交易、兑换和重要更新。 + 交易提醒 + 抢先体验最新功能和专属优惠。 + 专题报道和新闻更新 + 启用推送通知,即可在资金到账时收到提醒。 + 不要错过任何一笔交易 + 如果您在没有备份的情况下删除此钱包,您将永久失去对您的资金的访问权限。 + 选择您的钱包类型 + 扫描您的 Tangem 卡或指环以恢复卡号或从其他钱包导入卡号。 + 创建硬件钱包 + 想购买 Tangem 钱包吗? + 导入助记词 + 在手机上恢复您的钱包或从其他应用程序导入—方便,但安全性不如 Tangem 卡。 + 选择哪个? + 多部分交易 + 为了顺利完成交易,您的交易将被拆分成多个部分。您需要多次刷卡才能完成交易。 + 交易正在处理中。请多次用卡轻触以完成交易。 + 交易进行中 + 创建 Tangem 钱包 + 开始迁移 + 复制 + 要继续使用您的资金,请根据 Clore 官方指南开始迁移。 + 该网络不支持信息签名 + 无法签署信息。请重试。 + 根据 Clore 的官方文档,所有在 12 月 21 日之前收到的 Clore 代币(ERC-20 代币)都将迁移到 Clore;此日期之后收到的代币则不会迁移。迁移方案即将推出,敬请期待。 + 消息 + 打开索赔通道 + 要继续使用您的 Clore 代币,您必须根据索赔通道上的信息完成代币迁移。 + Clore 网络迁移 + 签署 + 签名 + Clore 网络迁移 + 此交易对不支持互换。请选择其他代币重试。 + 不支持的互换对 + 所需区块链 %s 尚未添加到您的投资组合中。请先添加,然后再进行连接。 + 将区块链添加到投资组合 + 要继续,请将您的 dApp 会话重新连接到所需的区块链网络 %s。 + 区块链网络未连接 + 检查您的区块链连接 + 请求超时 + 该区块链 %s Tangem Wallet 不支持,无法连接。 + 不支持的区块链 + 通过批准,您允许 dApp 或智能合约在未来的交易中使用代币。 + 已经拥有 Tangem 钱包? + 数千种资产 + 顶级硬件钱包 + 快速发货 + 一触即开始 + 无缝且安全 + 无助记词 + 简单易用 + 用 Tangem 创建硬件钱包。像银行卡一样纤薄,像银行保险库一样安全。 + 创建或导入软件钱包 + 在您的手机上创建或导入软件钱包。 + 从手机钱包开始 + 其他方法 + 使用 Tangem 硬件钱包 + 了解更多并购买 + 启用收益模式后,所有未来充值到此地址的资金都将转入 Aave。您仍然可以自由管理您的资金。 + 你的 %s 提供给 Aave + 供应 %1$s %2$s 到 Aave + 批准 + 您的代币授权已被撤销。请重新授权代币以恢复服务功能。 + 需要批准 + 费用将被扣除,您的资产将被重新提供。 + 要继续产生收益,需要获得批准。 + 确认批准 + 您的资金目前已提供给 Aave 协议,但您可以随时对其进行管理。 + 你的 %s 提供给 Aave + 无法加载图表... + 供应 %1$s %2$s 到 Aave。 + 禁用收益模式 + APY %1$s%% + 可用 + 当前年收益率 + 充值贷款时,将从余额中扣除不超过 %1$s 的网络费。 + 目前网络费用过高,无法执行借贷。一旦降至 %1$s 或以下,将立即提供资金。 + 我的资金 + 您的 %1$s 现已部署到 Aave 并产生收益。您持有 %2$s 这些代币代表您的余额,并会自动累积收益。当您充值时,扣除手续费后,资金将提供给 Aave 以产生更多收益。 + 收益模式 + 总收益 + 转至Aave + 探索 Aave + 这是 %s上的当前供应费。实际费用将显示在激活选项卡上。 + 当前费用 + 在扣除交易费后,今后所有 %s 充值都将自动提供给 Aave。 + 以后每次充值都会扣除大约 %1$s (%2$s) 的网络费,但不会超过 %3$s (%4$s) 的限额。 + 如果网络费用超过最高限额,交易将无法完成,直到网络费用降低为止。您可以稍后更改此限额。 + 最高费用 + 最低金额根据当前网络费用计算,确保不超过充值金额的 4%% ,相当于最低 %1$s (%2$s)。 + 最低充值金额 + 充值费用政策 + Tangem 还从产生的收益中提取 15% 服务费。 + 一旦网络费用降低或您的余额达到最低金额要求,您的资金将自动转入 Aave。 + 由于市场活跃,手续费高于平时。您可以立即进行交易,也可以稍后手续费降低时再来查看。 + 高昂的网络费用 + 历史回报 + 启动 %1$s%% 您余额的APY + 您的代币在收益模式下的授权已被撤销。请打开代币以重新授予权限。 + 需要代币批准 + 检查您的网络连接 + 网络费用信息无法获取 + 每次充值都将自动提供给 Aave。 + 您账户上的所有 %1$s 都将自动供应给 Aave。 + 自动供应至Aave + 您可以随时随地发送、兑换或出售您的资金。 + 无锁仓 + 它是如何运作的? + Aave 是一个链上协议,提供非托管流动性市场,使用户能够以可变利率累积收益。 + 去中心化和自托管 + 使用此服务即表示您同意服务提供商的条款\n%1$s 和 %2$s + 连接到 Aave + 启动 %1$s%% APY\非您的余额 + Aave %1$s%% - 可变利率 + 浮动利率 + Aave + 平均值 %s + 去年的收益 + 当前利率始终是浮动的,由 Aave 的链上智能合约根据实时供求关系自动计算。 + 技术支持 + 利率为浮动利率 + 充值后,您的资金将自动转入 Aave 账户,开始产生收益。 %s 将从中扣除交易手续费。 + 供应资产 + 您的 %s 将提供给 Aave ,没有锁仓,并且可以完全访问。 + 请参见充值费用政策 + 您的下一笔充值将自动发送至 Aave。 + 您未来所有的收入 %1$s 存款将自动存入 Aave。 + 活跃 + 暂停 + 禁用收益模式 + 关闭此功能会将您的资产从 Aave 提取,并将其转换回 %s 放入钱包,同时收益累积停止。 + 退出收益模式时,区块链会收取网络费用。 + 禁用收益模式 + 供应 + 供应 APY + APY + 利息自动累积。 + 利息自动累积 + 收益模式 + 启用收益模式 + 收益模式 + 收益模式合约部署 + 已启用收益模式 + %1$s 供应给 Aave + 已禁用收益模式 + %1$s 已从 Aave 提款 + 收益模式已初始化 + 收益模式重新激活 + 供应给 Aave + %1$s 供应给 Aave + 从 Aave提款 + 自动 + 添加 %1$s %2$s 以支付交易网络费。 + 无法覆盖 %s 费用 + 收益模式暂时不可用。请稍后再试。 + 收益模式不可用 + 无法加载图表... diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 75603cdf2e..dd706dc203 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1535,6 +1535,7 @@ Trade too large We would be happy to receive your feedback Tangem Pay is now in beta + Unable to rename card Card frozen Card payment Deposit @@ -1601,10 +1602,13 @@ Share your address or show QR-code Technical issues detected. Please try again later or contact support. Receive unavailable now + Only letters and numbers are allowed. + Invalid characters Reveal Show details Swap any asset in your portfolio for card Card details + Please try again later. Unfreeze Card Come back to the app if you forget it. Your PIN code @@ -1692,6 +1696,10 @@ Staking Service %1$s token in %%image%% %2$s network Token in %%image%% %1$s network + %s network + %1$s in %2$s network + %1$s in %%image%% %2$s + %1$s in %2$s %%image%% The %1$s (%2$s) token is the main currency on the %3$s network and cannot be hidden as long as you have other tokens on this network in the list Unable to hide %s N/A diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 36bed031e2..fb3369069e 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -11,6 +11,9 @@ android { namespace = "com.tangem.features.tokendetails.impl" } +tasks.withType().configureEach { + useJUnitPlatform() +} dependencies { /** AndroidX */ @@ -113,4 +116,9 @@ dependencies { implementation(deps.decompose.ext.compose) + /** Tests */ + testImplementation(deps.test.junit5) + testRuntimeOnly(deps.test.junit5.engine) + testImplementation(deps.test.mockk) + testImplementation(deps.test.truth) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 035e4511bf..639595b1f4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -97,7 +97,6 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( TokenDetailsScreen( tokenDetailsUM = tokenDetailsUM, - tokenMarketBlockComponent = tokenMarketBlockComponent, modifier = modifier, ) } else { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt new file mode 100644 index 0000000000..df70f292b1 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsDialogFactory.kt @@ -0,0 +1,73 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.model + +import com.tangem.core.decompose.di.GlobalUiMessageSender +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.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.tokendetails.impl.R +import javax.inject.Inject + +@ModelScoped +internal class TokenDetailsDialogFactory @Inject constructor( + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, +) { + + fun showConfirmHideToken(currency: CryptoCurrency, onConfirm: () -> Unit) { + uiMessageSender.send( + DialogMessage( + title = resourceReference( + id = R.string.token_details_hide_alert_title, + formatArgs = wrappedList(currency.name), + ), + message = resourceReference(R.string.token_details_hide_alert_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.token_details_hide_alert_hide), + isWarning = true, + onClick = onConfirm, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + + fun showLinkedTokens(currency: CryptoCurrency) { + uiMessageSender.send( + DialogMessage( + title = resourceReference( + id = R.string.token_details_unable_hide_alert_title, + formatArgs = wrappedList(currency.symbol), + ), + message = resourceReference( + id = R.string.token_details_unable_hide_alert_message, + formatArgs = wrappedList(currency.name, currency.symbol, currency.network.name), + ), + ), + ) + } + + fun showDismissIncompleteTransactionConfirm(onConfirm: () -> Unit) { + uiMessageSender.send( + DialogMessage( + message = resourceReference(R.string.warning_kaspa_unfinished_token_transaction_discard_message), + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_yes), + onClick = onConfirm, + ) + }, + secondActionBuilder = { cancelAction() }, + ), + ) + } + + fun showError(text: TextReference) { + uiMessageSender.send(DialogMessage(message = text)) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 46d9a4eb87..f5def6e3a1 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 @@ -25,6 +25,10 @@ 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.di.GlobalUiMessageSender +import com.tangem.common.ui.userwallet.converter.WalletIconUMConverter +import com.tangem.domain.account.supplier.SingleAccountListSupplier +import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase +import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -32,9 +36,7 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.navigation.url.UrlOpener import com.tangem.common.ui.tokens.getUnavailabilityReasonText import com.tangem.core.ui.clipboard.ClipboardManager -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig -import com.tangem.core.ui.components.currency.icon.CurrencyIconState -import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference @@ -86,6 +88,7 @@ import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase +import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.wallets.usecase.NetworkHasDerivationUseCase @@ -98,13 +101,14 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.Token import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDetailsBottomSheetConfig import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState -import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsStateController import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.express.TokenDetailsExpressStatusFactory +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.InitializeWithCryptoCurrencyTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.SetTopBarTitleTransformer +import com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer.UpdateTopBarMenuTransformer import com.tangem.features.tokendetails.TokenDetailsComponent import com.tangem.features.tokendetails.impl.R import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter @@ -114,7 +118,6 @@ import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isZero import kotlinx.collections.immutable.PersistentList -import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -172,6 +175,13 @@ internal class TokenDetailsModel @Inject constructor( private val isXpubDerivedUseCase: IsXpubDerivedUseCase, private val dynamicAddressesRepository: DynamicAddressesRepository, private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, + private val dialogFactory: TokenDetailsDialogFactory, + private val userWalletsListRepository: UserWalletsListRepository, + private val singleAccountListSupplier: SingleAccountListSupplier, + private val getWalletIconUseCase: GetWalletIconUseCase, + private val walletIconUMConverter: WalletIconUMConverter, + private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, + private val redesignStateController: TokenDetailsStateController, ) : Model(), TokenDetailsClickIntents, ExpressTransactionsClickIntents, @@ -213,11 +223,10 @@ internal class TokenDetailsModel @Inject constructor( userWalletId = userWalletId, ) - private val internalUiState = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) - val uiState: StateFlow = internalUiState + val uiState: StateFlow + field = MutableStateFlow(stateFactory.getInitialState(cryptoCurrency)) - private val internalRedesignUiState = MutableStateFlow(createInitialRedesignState()) - val redesignUiState: StateFlow = internalRedesignUiState + val redesignUiState: StateFlow get() = redesignStateController.uiState // region Clore migration // TODO: Remove after Clore migration ends ([REDACTED_TASK_KEY]) @@ -279,7 +288,10 @@ internal class TokenDetailsModel @Inject constructor( } init { + initRedesignState() updateTopBarMenu() + updateRedesignTopBarMenu() + observeRedesignTopBarTitle() initButtons() updateContent() handleBalanceHiding() @@ -329,7 +341,7 @@ internal class TokenDetailsModel @Inject constructor( private fun handleBalanceHiding() { getBalanceHidingSettingsUseCase() .onEach { settings -> - internalUiState.value = stateFactory.getStateWithUpdatedHidden( + uiState.value = stateFactory.getStateWithUpdatedHidden( isBalanceHidden = settings.isBalanceHidden, ) } @@ -345,7 +357,7 @@ internal class TokenDetailsModel @Inject constructor( .distinctUntilChanged() .onEach { state -> sendButtonsEvents(state.states) - internalUiState.value = stateFactory.getManageButtonsState(actions = state.states) + uiState.value = stateFactory.getManageButtonsState(actions = state.states) } .flowOn(dispatchers.main) .launchIn(modelScope) @@ -377,8 +389,8 @@ internal class TokenDetailsModel @Inject constructor( .distinctUntilChanged() .onEach { warnings -> val updatedState = stateFactory.getStateWithNotifications(warnings) - notificationsAnalyticsSender.send(internalUiState.value, updatedState.notifications) - internalUiState.value = updatedState + notificationsAnalyticsSender.send(uiState.value, updatedState.notifications) + uiState.value = updatedState } .launchIn(modelScope) .saveIn(warningsJobHolder) @@ -391,7 +403,7 @@ internal class TokenDetailsModel @Inject constructor( .map { it.status.right() } .distinctUntilChanged() .onEach { maybeCurrencyStatus -> - internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) + uiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus) maybeCurrencyStatus.onRight { status -> sendOneTimeBalanceLoadedAnalyticsEvent(status) cryptoCurrencyStatus = status @@ -413,7 +425,7 @@ internal class TokenDetailsModel @Inject constructor( .distinctUntilChanged() .onEach { waitForFirstExpressStatusEmmit.value = true } .onEach { expressTxs -> - internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( + uiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( expressTxs = expressTxs, updateBalance = ::updateNetworkToSwapBalance, ) @@ -424,11 +436,11 @@ internal class TokenDetailsModel @Inject constructor( delay = EXPRESS_STATUS_UPDATE_DELAY, task = { runSuspendCatching { - expressStatusFactory.getUpdatedExpressStatuses(internalUiState.value.expressTxs) + expressStatusFactory.getUpdatedExpressStatuses(uiState.value.expressTxs) } }, onSuccess = { updatedTxs -> - internalUiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( + uiState.value = expressStatusFactory.getStateWithUpdatedExpressTxs( updatedTxs, ::updateNetworkToSwapBalance, ) @@ -449,14 +461,14 @@ internal class TokenDetailsModel @Inject constructor( } yieldSupplyGetRewardsBalanceUseCase(status = status, appCurrency = selectedAppCurrencyFlow.value) .onEach { formatted -> - internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(formatted) + uiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance(formatted) } .flowOn(dispatchers.main) .launchIn(modelScope) .saveIn(yieldSupplyBalanceJobHolder) } else { yieldSupplyBalanceJobHolder.cancel() - internalUiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance( + uiState.value = stateFactory.getStateWithUpdatedYieldSupplyDisplayBalance( YieldSupplyRewardBalance.empty(), ) } @@ -493,7 +505,7 @@ internal class TokenDetailsModel @Inject constructor( null } - internalUiState.update { state -> + uiState.update { state -> stateFactory.getStakingInfoState( state = state, stakingEntryInfo = stakingEntryInfo, @@ -517,7 +529,7 @@ internal class TokenDetailsModel @Inject constructor( val isSupported = isXPUBSupported() val isDynamicAddressesAvailable = isSupported && isDynamicAddressesAvailable() - internalUiState.value = stateFactory.getStateWithUpdatedMenu( + uiState.value = stateFactory.getStateWithUpdatedMenu( userWallet = userWallet, hasDerivations = hasDerivations, isSupported = isSupported, @@ -890,7 +902,7 @@ internal class TokenDetailsModel @Inject constructor( } override fun onRefreshSwipe(isRefreshing: Boolean) { - internalUiState.value = stateFactory.getRefreshingState() + uiState.value = stateFactory.getRefreshingState() modelScope.launch(dispatchers.main) { listOf( @@ -902,29 +914,29 @@ internal class TokenDetailsModel @Inject constructor( subscribeOnExpressTransactionsUpdates() }, ).awaitAll() - internalUiState.value = stateFactory.getRefreshedState() + uiState.value = stateFactory.getRefreshedState() }.saveIn(refreshStateJobHolder) } override fun onDismissBottomSheet() { - when (val bsContent = internalUiState.value.bottomSheetConfig?.content) { + when (val bsContent = uiState.value.bottomSheetConfig?.content) { is ExpressStatusBottomSheetConfig -> { modelScope.launch(dispatchers.main) { expressStatusFactory.removeTransactionOnBottomSheetClosed(bsContent.value) } } } - internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + uiState.value = stateFactory.getStateWithClosedBottomSheet() } override fun onCloseRentInfoNotification() { - internalUiState.value = stateFactory.getStateWithRemovedRentNotification() + uiState.value = stateFactory.getStateWithRemovedRentNotification() } override fun onExpressTransactionClick(txId: String) { - val expressTxState = internalUiState.value.expressTxsToDisplay.firstOrNull { it.info.txId == txId } + val expressTxState = uiState.value.expressTxsToDisplay.firstOrNull { it.info.txId == txId } ?: return - internalUiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState) + uiState.value = expressStatusFactory.getStateWithExpressStatusBottomSheet(expressTxState) } override fun onGoToProviderClick(url: String) { @@ -1018,7 +1030,7 @@ internal class TokenDetailsModel @Inject constructor( } }, ifRight = { - internalUiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() + uiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() }, ) } @@ -1060,7 +1072,7 @@ internal class TokenDetailsModel @Inject constructor( showErrorDialog(message) } }, - ifRight = { internalUiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() }, + ifRight = { uiState.value = stateFactory.getStateWithRemovedRequiredTrustlineNotification() }, ) } } @@ -1086,7 +1098,7 @@ internal class TokenDetailsModel @Inject constructor( TangemLogger.e("Error: $e") }, ifRight = { - internalUiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() + uiState.value = stateFactory.getStateWithRemovedKaspaIncompleteTransactionNotification() }, ) } @@ -1120,13 +1132,13 @@ internal class TokenDetailsModel @Inject constructor( } } }, - ifRight = { internalUiState.value = stateFactory.getStateWithRemovedHederaAssociateNotification() }, + ifRight = { uiState.value = stateFactory.getStateWithRemovedHederaAssociateNotification() }, ) } } override fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) { - internalUiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config) + uiState.value = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config) } override fun onConfirmDisposeExpressStatus() { @@ -1134,7 +1146,7 @@ internal class TokenDetailsModel @Inject constructor( } override fun onDisposeExpressStatus() { - val bottomSheetState = internalUiState.value.bottomSheetConfig?.content + val bottomSheetState = uiState.value.bottomSheetConfig?.content if (bottomSheetState is ExpressStatusBottomSheetConfig) { modelScope.launch { expressStatusFactory.removeTransactionOnBottomSheetClosed( @@ -1143,7 +1155,7 @@ internal class TokenDetailsModel @Inject constructor( ) } } - internalUiState.value = stateFactory.getStateWithClosedBottomSheet() + uiState.value = stateFactory.getStateWithClosedBottomSheet() } override fun onYieldInfoClick() { @@ -1194,52 +1206,16 @@ internal class TokenDetailsModel @Inject constructor( } private fun showConfirmHideTokenDialog(currency: CryptoCurrency) { - uiMessageSender.send( - DialogMessage( - title = resourceReference( - id = R.string.token_details_hide_alert_title, - formatArgs = wrappedList(currency.name), - ), - message = resourceReference(R.string.token_details_hide_alert_message), - firstActionBuilder = { - EventMessageAction( - title = resourceReference(R.string.token_details_hide_alert_hide), - isWarning = true, - onClick = ::onHideConfirmed, - ) - }, - secondActionBuilder = { cancelAction() }, - ), - ) + dialogFactory.showConfirmHideToken(currency = currency, onConfirm = ::onHideConfirmed) } private fun showLinkedTokensDialog(currency: CryptoCurrency) { - uiMessageSender.send( - DialogMessage( - title = resourceReference( - id = R.string.token_details_unable_hide_alert_title, - formatArgs = wrappedList(currency.symbol), - ), - message = resourceReference( - id = R.string.token_details_unable_hide_alert_message, - formatArgs = wrappedList(currency.name, currency.symbol, currency.network.name), - ), - ), - ) + dialogFactory.showLinkedTokens(currency = currency) } private fun showDismissIncompleteTransactionConfirmDialog() { - uiMessageSender.send( - DialogMessage( - message = resourceReference(R.string.warning_kaspa_unfinished_token_transaction_discard_message), - firstActionBuilder = { - EventMessageAction( - title = resourceReference(R.string.common_yes), - onClick = ::onConfirmDismissIncompleteTransactionClick, - ) - }, - secondActionBuilder = { cancelAction() }, - ), + dialogFactory.showDismissIncompleteTransactionConfirm( + onConfirm = ::onConfirmDismissIncompleteTransactionClick, ) } @@ -1262,7 +1238,7 @@ internal class TokenDetailsModel @Inject constructor( } private fun showErrorDialog(text: TextReference) { - uiMessageSender.send(DialogMessage(message = text)) + dialogFactory.showError(text = text) } private fun checkForActionUpdates() { @@ -1402,29 +1378,79 @@ internal class TokenDetailsModel @Inject constructor( // endregion Clore migration - private fun createInitialRedesignState(): TokenDetailsUM { - return TokenDetailsUM( - topAppBarUM = TokenDetailsTopAppBarUM( - title = stringReference(cryptoCurrency.name), - subtitle = stringReference(cryptoCurrency.symbol), - menuItems = persistentListOf(), + private fun updateRedesignTopBarMenu() { + modelScope.launch(dispatchers.main) { + val hasDerivations = networkHasDerivationUseCase( + userWallet = userWallet, + network = cryptoCurrency.network, + ).getOrElse { false } + + val isSupported = isXPUBSupported() + + redesignStateController.update( + UpdateTopBarMenuTransformer( + userWallet = userWallet, + hasDerivations = hasDerivations, + isXPubSupported = isSupported, + onGenerateExtendedKey = ::onGenerateExtendedKey, + onHideClick = ::onHideClick, + ), + ) + } + } + + private fun initRedesignState() { + redesignStateController.update( + InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = ::onBackClick, ), - balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( - actionButtons = persistentListOf(), - tokenBalanceTypeUM = TokenBalanceTypeUM.Single, - currencyIconState = CurrencyIconState.Loading, - ), - marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol), - stakingBlocksState = null, - pullToRefreshConfig = PullToRefreshConfig( - isRefreshing = false, - onRefresh = {}, - ), - isBalanceHidden = false, - isMarketPriceAvailable = false, ) } + private fun observeRedesignTopBarTitle() { + combine( + flow = userWalletsListRepository.userWallets.filterNotNull(), + flow2 = isAccountsModeEnabledUseCase.invoke(), + flow3 = singleAccountListSupplier(userWalletId), + flow4 = getAccountCryptoCurrencyStatusUseCase(userWalletId, cryptoCurrency) + .map { status -> status.account } + .distinctUntilChanged(), + ) { wallets, accountsModeEnabled, accountList, currentAccount -> + val currentWallet = wallets.firstOrNull { it.walletId == userWalletId } ?: userWallet + TopBarTitleInputs( + hasMultipleWallets = wallets.size > 1, + hasMultipleAccounts = accountsModeEnabled && accountList.accounts.size > 1, + walletName = currentWallet.name, + deviceIconUM = walletIconUMConverter.convert(getWalletIconUseCase(currentWallet)), + account = currentAccount, + ) + } + .distinctUntilChanged() + .onEach { inputs -> + redesignStateController.update( + SetTopBarTitleTransformer( + cryptoCurrency = cryptoCurrency, + hasMultipleWallets = inputs.hasMultipleWallets, + hasMultipleAccounts = inputs.hasMultipleAccounts, + walletName = inputs.walletName, + deviceIconUM = inputs.deviceIconUM, + account = inputs.account, + ), + ) + } + .flowOn(dispatchers.default) + .launchIn(modelScope) + } + + private data class TopBarTitleInputs( + val hasMultipleWallets: Boolean, + val hasMultipleAccounts: Boolean, + val walletName: String, + val deviceIconUM: DeviceIconUM, + val account: Account.CryptoPortfolio?, + ) + private companion object { const val EXPRESS_STATUS_UPDATE_DELAY = 10_000L const val BASE_DERIVATION_NODE_COUNT = 5 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt new file mode 100644 index 0000000000..41e505d325 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsStateController.kt @@ -0,0 +1,54 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state + +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.TextReference +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@ModelScoped +internal class TokenDetailsStateController @Inject constructor() { + + val uiState: StateFlow + field = MutableStateFlow(value = getInitialState()) + + val value: TokenDetailsUM get() = uiState.value + + fun update(function: (TokenDetailsUM) -> TokenDetailsUM) { + uiState.update(function = function) + } + + fun update(transformer: Transformer) { + uiState.update(function = transformer::transform) + } + + private fun getInitialState(): TokenDetailsUM { + return TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TokenDetailsTopAppBarUM.TitleState.Simple(tokenName = ""), + subtitle = TextReference.EMPTY, + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = ""), + stakingBlocksState = null, + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt index 14b55185e8..91ee9dc832 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsUM.kt @@ -1,12 +1,15 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state -import androidx.compose.runtime.Stable +import androidx.compose.runtime.Immutable +import com.tangem.common.ui.account.AccountIconUM import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.image.DeviceIconUM import com.tangem.core.ui.extensions.TextReference import kotlinx.collections.immutable.ImmutableList -@Stable +@Immutable internal data class TokenDetailsUM( val topAppBarUM: TokenDetailsTopAppBarUM, val balanceBlockUM: TokenDetailsBalanceBlockUM, @@ -17,8 +20,31 @@ internal data class TokenDetailsUM( val isMarketPriceAvailable: Boolean, ) +@Immutable internal data class TokenDetailsTopAppBarUM( - val title: TextReference, + val titleState: TitleState, val subtitle: TextReference, - val menuItems: ImmutableList, -) \ No newline at end of file + val onBackClick: () -> Unit, + val menuItems: ImmutableList, +) { + @Immutable + sealed interface TitleState { + val tokenName: String + + data class Simple( + override val tokenName: String, + ) : TitleState + + data class WithWallet( + override val tokenName: String, + val walletName: String, + val deviceIconUM: DeviceIconUM, + ) : TitleState + + data class WithAccount( + override val tokenName: String, + val accountName: TextReference, + val accountIconUM: AccountIconUM.CryptoPortfolio, + ) : TitleState + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt new file mode 100644 index 0000000000..64f054639c --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformer.kt @@ -0,0 +1,23 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class InitializeWithCryptoCurrencyTransformer( + private val cryptoCurrency: CryptoCurrency, + private val onBackClick: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( + topAppBarUM = prevState.topAppBarUM.copy( + titleState = TokenDetailsTopAppBarUM.TitleState.Simple(tokenName = cryptoCurrency.name), + subtitle = stringReference(cryptoCurrency.symbol), + onBackClick = onBackClick, + ), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = cryptoCurrency.symbol), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformer.kt new file mode 100644 index 0000000000..443e904a62 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformer.kt @@ -0,0 +1,78 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.common.ui.account.toUM +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.wrappedList +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.network.Network +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.utils.transformer.Transformer +import com.tangem.core.res.R as CoreResR + +internal class SetTopBarTitleTransformer( + private val cryptoCurrency: CryptoCurrency, + private val hasMultipleWallets: Boolean, + private val hasMultipleAccounts: Boolean, + private val walletName: String, + private val deviceIconUM: DeviceIconUM, + private val account: Account.CryptoPortfolio?, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( + topAppBarUM = prevState.topAppBarUM.copy( + titleState = createTitleState(), + subtitle = createSubtitle(), + ), + ) + + private fun createTitleState(): TitleState { + val tokenName = cryptoCurrency.name + + return when { + hasMultipleAccounts && account != null -> { + val accountNameUM = account.accountName.toUM() + TitleState.WithAccount( + tokenName = tokenName, + accountName = accountNameUM.value, + accountIconUM = AccountIconUM.CryptoPortfolio( + value = account.icon.value, + color = account.icon.color, + ), + ) + } + hasMultipleWallets -> TitleState.WithWallet( + tokenName = tokenName, + walletName = walletName, + deviceIconUM = deviceIconUM, + ) + else -> TitleState.Simple(tokenName = tokenName) + } + } + + private fun createSubtitle(): TextReference { + val networkName = cryptoCurrency.network.name + return when (cryptoCurrency) { + is CryptoCurrency.Token -> { + val standardName = cryptoCurrency.network.standardType + .takeIf { it !is Network.StandardType.Unspecified } + ?.name + if (standardName != null) { + resourceReference( + CoreResR.string.token_details_toolbar_subtitle_standard, + wrappedList(standardName, networkName), + ) + } else { + resourceReference(CoreResR.string.token_details_toolbar_subtitle_network, wrappedList(networkName)) + } + } + is CryptoCurrency.Coin -> { + resourceReference(CoreResR.string.token_details_toolbar_subtitle_network, wrappedList(networkName)) + } + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt new file mode 100644 index 0000000000..320e28bdce --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformer.kt @@ -0,0 +1,51 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.themedColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import com.tangem.features.tokendetails.impl.R +import com.tangem.utils.transformer.Transformer +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList + +internal class UpdateTopBarMenuTransformer( + private val userWallet: UserWallet, + private val hasDerivations: Boolean, + private val isXPubSupported: Boolean, + private val onGenerateExtendedKey: () -> Unit, + private val onHideClick: () -> Unit, +) : Transformer { + + override fun transform(prevState: TokenDetailsUM): TokenDetailsUM = prevState.copy( + topAppBarUM = prevState.topAppBarUM.copy(menuItems = createMenuItems()), + ) + + private fun createMenuItems() = if (userWallet is UserWallet.Cold && + userWallet.cardTypesResolver.isSingleWalletWithToken() + ) { + persistentListOf() + } else { + buildList { + if (isXPubSupported && hasDerivations) { + add( + TangemDropdownMenuItem( + title = resourceReference(R.string.token_details_generate_xpub), + textColor = themedColor { TangemTheme.colors.text.primary1 }, + onClick = onGenerateExtendedKey, + ), + ) + } + add( + TangemDropdownMenuItem( + title = resourceReference(R.string.token_details_hide_token), + textColor = themedColor { TangemTheme.colors.text.warning }, + onClick = onHideClick, + ), + ) + }.toImmutableList() + } +} \ No newline at end of file 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 01902e1cab..6a1b96d329 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 @@ -1,30 +1,148 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui +import android.content.res.Configuration +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar +import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.themedColor +import com.tangem.core.ui.res.LocalRootBackgroundColor import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceTypeUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM -import com.tangem.features.markets.token.block.TokenMarketBlockComponent +import dev.chrisbanes.haze.HazeProgressive +import dev.chrisbanes.haze.HazeTint +import kotlinx.collections.immutable.persistentListOf -@Suppress("UnusedParameter") @Composable -internal fun TokenDetailsScreen( - tokenDetailsUM: TokenDetailsUM, - tokenMarketBlockComponent: TokenMarketBlockComponent?, - modifier: Modifier = Modifier, -) { +internal fun TokenDetailsScreen(tokenDetailsUM: TokenDetailsUM, modifier: Modifier = Modifier) { + val topAppBarUM = tokenDetailsUM.topAppBarUM + + // TODO [REDACTED_TASK_KEY] Token Details Make Balance with actions + val balanceBlockHeight = 200.dp + val partialCollapsedHeight = 0.dp + val expandedHeight = balanceBlockHeight + partialCollapsedHeight + + val behavior = rememberTangemExitUntilCollapsedScrollBehavior( + expandedHeight = expandedHeight, + partialCollapsedHeight = partialCollapsedHeight, + ) + Box( modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center, ) { - Text( - text = "Token Details Redesign", - style = TangemTheme.typography.h2, - color = TangemTheme.colors.text.primary1, + Box( + modifier = Modifier + .fillMaxSize() + .hazeSourceTangem(zIndex = -2f), + ) { + TangemCollapsingTopBar( + state = behavior.state, + collapsingPart = { + // [REDACTED_TASK_KEY] Token Details Make Balance with actions + Box( + modifier = Modifier + .fillMaxWidth() + .height(balanceBlockHeight), + ) + }, + body = { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .nestedScroll(behavior.nestedScrollConnection), + ) { + // TODO [REDACTED_TASK_KEY] Token Details Make Transaction History + } + }, + ) + } + + val rootBackground by LocalRootBackgroundColor.current + val hazeIntensity by animateFloatAsState( + targetValue = (behavior.state.collapsedFraction * 2f).coerceIn(0f, 1f), + label = "TopBarHazeIntensity", + ) + Box( + modifier = Modifier.hazeEffectTangem { + fallbackTint = HazeTint(rootBackground.copy(alpha = hazeIntensity / 2f)) + progressive = HazeProgressive.verticalGradient( + startIntensity = hazeIntensity, + endIntensity = 0f, + preferPerformance = true, + ) + }, + ) { + TokenDetailsTopBar(topAppBarUM = topAppBarUM) + } + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenDetailsScreen_Preview() { + TangemThemePreviewRedesign { + TokenDetailsScreen( + tokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.WithAccount( + tokenName = "Tether", + accountName = stringReference("Portfolio"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + subtitle = stringReference("ERC-20 in Ethereum network"), + onBackClick = {}, + menuItems = persistentListOf( + TangemDropdownMenuItem( + title = stringReference("Hide Token"), + textColor = themedColor { TangemTheme.colors.text.warning }, + onClick = {}, + ), + ), + ), + balanceBlockUM = TokenDetailsBalanceBlockUM.Loading( + actionButtons = persistentListOf(), + tokenBalanceTypeUM = TokenBalanceTypeUM.Single, + currencyIconState = CurrencyIconState.Loading, + ), + marketPriceBlockState = MarketPriceBlockState.Loading(currencySymbol = "USDT"), + stakingBlocksState = null, + pullToRefreshConfig = PullToRefreshConfig( + isRefreshing = false, + onRefresh = {}, + ), + isBalanceHidden = false, + isMarketPriceAvailable = true, + ), ) } -} \ No newline at end of file +} +// endregion Preview \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsTopBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsTopBar.kt new file mode 100644 index 0000000000..aaf3a4838f --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsTopBar.kt @@ -0,0 +1,478 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.TextAutoSize +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.rememberTextMeasurer +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.PreviewParameterProvider +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.util.fastForEach +import com.tangem.common.ui.account.AccountIcon +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.ui.components.account.AccountIconSize +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownItem +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenu +import com.tangem.core.ui.components.dropdownmenu.TangemDropdownMenuItem +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.ds.image.TangemDeviceIcon +import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarActionContent +import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM +import com.tangem.core.ui.extensions.resolveAnnotatedReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.core.ui.extensions.themedColor +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import kotlinx.collections.immutable.persistentListOf +import com.tangem.core.ui.R as CoreUiR + +@Composable +internal fun TokenDetailsTopBar(topAppBarUM: TokenDetailsTopAppBarUM, modifier: Modifier = Modifier) { + TangemTopBar( + modifier = modifier.statusBarsPadding(), + startContent = { + TangemTopBarActionContent( + actionUM = TangemTopBarActionUM( + iconRes = CoreUiR.drawable.ic_back_24, + onClick = topAppBarUM.onBackClick, + ghostModeProgress = 1f, + ), + ) + }, + endContent = if (topAppBarUM.menuItems.isNotEmpty()) { + { + var isDropdownMenuShown by rememberSaveable { mutableStateOf(false) } + Box { + TangemTopBarActionContent( + actionUM = TangemTopBarActionUM( + iconRes = CoreUiR.drawable.ic_more_default_24, + onClick = { isDropdownMenuShown = true }, + ghostModeProgress = 1f, + ), + ) + TangemDropdownMenu( + expanded = isDropdownMenuShown, + modifier = Modifier.background(TangemTheme.colors.background.primary), + onDismissRequest = { isDropdownMenuShown = false }, + content = { + topAppBarUM.menuItems.fastForEach { menuItem -> + TangemDropdownItem( + item = menuItem, + dismissParent = { isDropdownMenuShown = false }, + ) + } + }, + ) + } + } + } else { + null + }, + content = { + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = TangemTheme.dimens2.x1), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), + ) { + TokenDetailsTitle(titleState = topAppBarUM.titleState) + Text( + text = topAppBarUM.subtitle.resolveAnnotatedReference(), + color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.captionMedium12, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + }, + ) +} + +@Composable +private fun TokenDetailsTitle(titleState: TitleState) { + val appearance = TitleAppearance( + style = TangemTheme.typography2.bodySemibold16, + iconSize = TangemTheme.dimens2.x5, + spacing = TangemTheme.dimens2.x1, + ) + + when (titleState) { + is TitleState.Simple -> { + Text( + text = titleState.tokenName, + color = TangemTheme.colors2.text.neutral.primary, + style = appearance.style, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = MIN_TITLE_FONT_SIZE, + maxFontSize = MAX_TITLE_FONT_SIZE, + ), + ) + } + is TitleState.WithWallet -> { + AdaptiveTokenWithSecondaryRow( + tokenName = titleState.tokenName, + secondaryName = titleState.walletName, + template = stringResourceSafe( + id = CoreUiR.string.token_details_toolbar_title_token_in_wallet, + titleState.tokenName, + titleState.walletName, + ), + appearance = appearance, + icon = { + TangemDeviceIcon( + state = titleState.deviceIconUM, + modifier = Modifier.size(appearance.iconSize), + ) + }, + ) + } + is TitleState.WithAccount -> { + val accountNameStr = titleState.accountName.resolveAnnotatedReference().toString() + AdaptiveTokenWithSecondaryRow( + tokenName = titleState.tokenName, + secondaryName = accountNameStr, + template = stringResourceSafe( + id = CoreUiR.string.token_details_toolbar_title_token_in_account, + titleState.tokenName, + accountNameStr, + ), + appearance = appearance, + icon = { + AccountIcon( + name = titleState.accountName, + icon = titleState.accountIconUM, + size = AccountIconSize.ExtraSmall, + ) + }, + ) + } + } +} + +/** + * Adaptive title for [TitleState.WithAccount] / [TitleState.WithWallet]. + * + * Phrase template carries the [IMAGE_PLACEHOLDER] marker — translator decides where + * the icon sits (e.g. "Tether in [⭐] Portfolio" or "Tether in My Wallet [⭐]"). + * RTL is handled by BiDi inside the single [Text]. + * + * Width-driven cascade: + * 1–2. Full phrase as single [Text] with inline icon; [TextOverflow.Ellipsis] + * trims the secondary name tail when needed. + * 3. No meaningful tail left → fall back to `[tokenName] [icon]` in a [Row]; + * icon is a sibling so ellipsis trims only tokenName, never the icon. + * 4–5. tokenName itself doesn't fit → [TextAutoSize] shrinks to + * [MIN_TITLE_FONT_SIZE], then [TextOverflow.Ellipsis] tails. + * + * #1/#2 vs #3 is decided here via [rememberTextMeasurer]; #4/#5 are delegated + * to [TextAutoSize] + [TextOverflow.Ellipsis] in the fallback branch. + */ +@Composable +private fun AdaptiveTokenWithSecondaryRow( + tokenName: String, + secondaryName: String, + template: String, + appearance: TitleAppearance, + icon: @Composable () -> Unit, +) { + val (beforeIcon, afterIcon) = remember(template) { splitTemplate(template) } + val inlineContent = rememberIconInlineContent(appearance.iconSize, icon) + + BoxWithConstraints( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + val isFullTextShown = rememberShouldShowFullText( + beforeIcon = beforeIcon, + afterIcon = afterIcon, + secondaryName = secondaryName, + appearance = appearance, + maxWidthPx = constraints.maxWidth, + ) + if (isFullTextShown) { + FullPhraseTitle( + beforeIcon = beforeIcon, + afterIcon = afterIcon, + style = appearance.style, + inlineContent = inlineContent, + ) + } else { + FallbackTokenWithIconTitle( + tokenName = tokenName, + appearance = appearance, + icon = icon, + ) + } + } +} + +private fun splitTemplate(template: String): Pair { + val parts = template.split(IMAGE_PLACEHOLDER, limit = 2) + return if (parts.size == 2) parts[0] to parts[1] else template to "" +} + +@Composable +private fun rememberIconInlineContent(iconSize: Dp, icon: @Composable () -> Unit): Map { + val iconSizeSp = with(LocalDensity.current) { iconSize.toSp() } + val currentIcon by rememberUpdatedState(icon) + return remember(iconSizeSp) { + mapOf( + ICON_INLINE_ID to InlineTextContent( + placeholder = Placeholder( + width = iconSizeSp, + height = iconSizeSp, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center, + ), + children = { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + currentIcon() + } + }, + ), + ) + } +} + +@Composable +private fun rememberShouldShowFullText( + beforeIcon: String, + afterIcon: String, + secondaryName: String, + appearance: TitleAppearance, + maxWidthPx: Int, +): Boolean { + val measurer = rememberTextMeasurer() + val density = LocalDensity.current + return remember(beforeIcon, afterIcon, secondaryName, maxWidthPx, appearance, density) { + if (maxWidthPx <= 0) return@remember true + val fullTextWidthPx = measurer + .measure(text = beforeIcon + afterIcon, style = appearance.style, softWrap = false) + .size.width + val secondaryWidthPx = measurer + .measure(text = secondaryName, style = appearance.style, softWrap = false) + .size.width + val staticWidthPx = (fullTextWidthPx - secondaryWidthPx).coerceAtLeast(0) + val iconReservePx = with(density) { + (appearance.iconSize + appearance.spacing * 2).toPx() + }.toInt() + val minSecondaryPx = with(density) { MIN_SECONDARY_NAME_WIDTH.toPx() }.toInt() + staticWidthPx + iconReservePx + minSecondaryPx <= maxWidthPx + } +} + +@Composable +private fun FullPhraseTitle( + beforeIcon: String, + afterIcon: String, + style: TextStyle, + inlineContent: Map, +) { + val fullText = remember(beforeIcon, afterIcon) { + buildAnnotatedString { + append(beforeIcon) + appendInlineContent(ICON_INLINE_ID, IMAGE_PLACEHOLDER) + append(afterIcon) + } + } + Text( + text = fullText, + inlineContent = inlineContent, + color = TangemTheme.colors2.text.neutral.primary, + style = style, + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) +} + +@Composable +private fun FallbackTokenWithIconTitle(tokenName: String, appearance: TitleAppearance, icon: @Composable () -> Unit) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(appearance.spacing, Alignment.CenterHorizontally), + ) { + Text( + text = tokenName, + color = TangemTheme.colors2.text.neutral.primary, + style = appearance.style, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + autoSize = TextAutoSize.StepBased( + minFontSize = MIN_TITLE_FONT_SIZE, + maxFontSize = MAX_TITLE_FONT_SIZE, + ), + modifier = Modifier.weight(weight = 1f, fill = false), + ) + icon() + } +} + +@Immutable +private data class TitleAppearance( + val style: TextStyle, + val iconSize: Dp, + val spacing: Dp, +) + +private const val ICON_INLINE_ID = "account_icon" +private const val IMAGE_PLACEHOLDER = "%image%" + +private val MIN_TITLE_FONT_SIZE = 12.sp +private val MAX_TITLE_FONT_SIZE = 16.sp +private val MIN_SECONDARY_NAME_WIDTH = 48.dp + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TokenDetailsTopBar_Preview( + @PreviewParameter(TokenDetailsTopBarPreviewProvider::class) titleState: TitleState, +) { + TangemThemePreviewRedesign { + TokenDetailsTopBar( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = titleState, + subtitle = stringReference("ERC-20 in Ethereum network"), + onBackClick = {}, + menuItems = persistentListOf( + TangemDropdownMenuItem( + title = stringReference("Hide Token"), + textColor = themedColor { TangemTheme.colors.text.warning }, + onClick = {}, + ), + ), + ), + ) + } +} + +private class TokenDetailsTopBarPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + // === Base title states === + // Simple — 1 wallet, 1 account + TitleState.Simple(tokenName = "Tether"), + // WithWallet — N wallets, 1 account + TitleState.WithWallet( + tokenName = "Tether", + walletName = "Tangem wallet", + deviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), + ), + // WithAccount — 1 wallet, N accounts + TitleState.WithAccount( + tokenName = "Tether", + accountName = stringReference("Portfolio"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + // === AdaptiveTokenTitleRow cascade cases === + // Cascade #1 — full text fits as is (short token + short wallet) + TitleState.WithWallet( + tokenName = "BTC", + walletName = "Main", + deviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), + ), + // Cascade #2 — full text doesn't fit, ellipsized tail still meaningful + TitleState.WithWallet( + tokenName = "Tether", + walletName = "My Long Tangem Hardware Wallet", + deviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), + ), + // Cascade #3 — secondary part too small to be meaningful, drop to token-only + icon + TitleState.WithWallet( + tokenName = "USDCoinWrapped", + walletName = "Super Extra Long Wallet Name That Definitely Wont Fit", + deviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ), + ), + // Cascade #4 — even tokenName alone doesn't fit at full font size: TextAutoSize shrinks it + TitleState.WithAccount( + tokenName = "VeryLongTokenNameThatOverflowsVeryLongTokenNameThatOverflows", + accountName = stringReference("Portfolio"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + // Cascade #5 — even at MIN_TITLE_FONT_SIZE doesn't fit: TextOverflow.Ellipsis tails it + TitleState.Simple( + tokenName = "ExtremelyLongTokenNameThatCannotPossiblyFitEvenAtMinFontSize", + ), + // Account variant — long account name triggers ellipsized tail (#2) + TitleState.WithAccount( + tokenName = "Tether", + accountName = stringReference("My Personal Long Account Name"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + // Account variant — drop secondary, icon-only fallback (#3) + TitleState.WithAccount( + tokenName = "USDCoinWrapped", + accountName = stringReference("Super Extra Long Account Name That Wont Fit Anywhere"), + accountIconUM = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ), + ), + ) +} +// endregion Preview \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt new file mode 100644 index 0000000000..43672cf4c9 --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/InitializeWithCryptoCurrencyTransformerTest.kt @@ -0,0 +1,128 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class InitializeWithCryptoCurrencyTransformerTest { + + private val cryptoCurrency: CryptoCurrency = mockk(relaxed = true) { + every { name } returns TOKEN_NAME + every { symbol } returns TOKEN_SYMBOL + } + private val onBackClick: () -> Unit = mockk(relaxed = true) + + @Test + fun `GIVEN crypto currency WHEN transform THEN top bar title is Simple with token name`() { + // GIVEN + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isEqualTo(TitleState.Simple(tokenName = TOKEN_NAME)) + } + + @Test + fun `GIVEN crypto currency WHEN transform THEN subtitle is token symbol`() { + // GIVEN + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.subtitle).isEqualTo(stringReference(TOKEN_SYMBOL)) + } + + @Test + fun `GIVEN onBackClick callback WHEN top bar onBackClick invoked THEN callback is dispatched`() { + // GIVEN + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(initialState()) + result.topAppBarUM.onBackClick() + + // THEN + verify(exactly = 1) { onBackClick.invoke() } + } + + @Test + fun `GIVEN crypto currency WHEN transform THEN market price loading carries currency symbol`() { + // GIVEN + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.marketPriceBlockState) + .isEqualTo(MarketPriceBlockState.Loading(currencySymbol = TOKEN_SYMBOL)) + } + + @Test + fun `GIVEN any state WHEN transform THEN unrelated fields are preserved`() { + // GIVEN + val state = initialState() + val transformer = InitializeWithCryptoCurrencyTransformer( + cryptoCurrency = cryptoCurrency, + onBackClick = onBackClick, + ) + + // WHEN + val result = transformer.transform(state) + + // THEN — only top bar title/subtitle/onBackClick and marketPriceBlockState are touched + assertThat(result.topAppBarUM.menuItems).isEqualTo(state.topAppBarUM.menuItems) + assertThat(result.balanceBlockUM).isSameInstanceAs(state.balanceBlockUM) + assertThat(result.stakingBlocksState).isEqualTo(state.stakingBlocksState) + assertThat(result.pullToRefreshConfig).isSameInstanceAs(state.pullToRefreshConfig) + assertThat(result.isBalanceHidden).isEqualTo(state.isBalanceHidden) + assertThat(result.isMarketPriceAvailable).isEqualTo(state.isMarketPriceAvailable) + } + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = ""), + subtitle = stringReference(""), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) + + private companion object { + const val TOKEN_NAME = "Tether" + const val TOKEN_SYMBOL = "USDT" + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt new file mode 100644 index 0000000000..7f72295f3a --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/SetTopBarTitleTransformerTest.kt @@ -0,0 +1,204 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import androidx.compose.ui.graphics.Color +import com.google.common.truth.Truth.assertThat +import com.tangem.common.ui.account.AccountIconUM +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.ds.image.DeviceIconUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.models.account.Account +import com.tangem.domain.models.account.AccountName +import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import io.mockk.every +import io.mockk.mockk +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.Test + +class SetTopBarTitleTransformerTest { + + private val tokenName = "Tether" + private val walletName = "My Wallet" + private val deviceIconUM: DeviceIconUM = DeviceIconUM.Card( + mainColor = Color.DarkGray, + secondColor = null, + ) + private val cryptoCurrency: CryptoCurrency.Coin = mockk(relaxed = true) { + every { name } returns tokenName + } + + @Test + fun `GIVEN single wallet single account WHEN transform THEN Simple title`() { + // GIVEN + val transformer = createTransformer( + hasMultipleWallets = false, + hasMultipleAccounts = false, + account = null, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isEqualTo(TitleState.Simple(tokenName = tokenName)) + } + + @Test + fun `GIVEN multiple wallets and single account WHEN transform THEN WithWallet title`() { + // GIVEN + val transformer = createTransformer( + hasMultipleWallets = true, + hasMultipleAccounts = false, + account = null, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val expected = TitleState.WithWallet( + tokenName = tokenName, + walletName = walletName, + deviceIconUM = deviceIconUM, + ) + assertThat(result.topAppBarUM.titleState).isEqualTo(expected) + } + + @Test + fun `GIVEN single wallet and multiple accounts WHEN transform THEN WithAccount title`() { + // GIVEN + val transformer = createTransformer( + hasMultipleWallets = false, + hasMultipleAccounts = true, + account = stubAccount(), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val title = result.topAppBarUM.titleState + assertThat(title).isInstanceOf(TitleState.WithAccount::class.java) + assertThat((title as TitleState.WithAccount).tokenName).isEqualTo(tokenName) + } + + @Test + fun `GIVEN multiple wallets AND multiple accounts WHEN transform THEN WithAccount wins`() { + // GIVEN — design priority: account branch wins over wallet branch + val transformer = createTransformer( + hasMultipleWallets = true, + hasMultipleAccounts = true, + account = stubAccount(), + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isInstanceOf(TitleState.WithAccount::class.java) + } + + @Test + fun `GIVEN multiple accounts but null account WHEN transform THEN falls back to wallet branch`() { + // GIVEN — race protection: hasMultipleAccounts=true but account not loaded yet + val transformer = createTransformer( + hasMultipleWallets = true, + hasMultipleAccounts = true, + account = null, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isInstanceOf(TitleState.WithWallet::class.java) + } + + @Test + fun `GIVEN multiple accounts but null account AND single wallet WHEN transform THEN falls back to Simple`() { + // GIVEN + val transformer = createTransformer( + hasMultipleWallets = false, + hasMultipleAccounts = true, + account = null, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.titleState).isEqualTo(TitleState.Simple(tokenName = tokenName)) + } + + @Test + fun `GIVEN account with custom icon WHEN transform THEN icon is propagated`() { + // GIVEN + val account = stubAccount( + iconValue = CryptoPortfolioIcon.Icon.Star, + iconColor = CryptoPortfolioIcon.Color.Azure, + ) + val transformer = createTransformer( + hasMultipleWallets = false, + hasMultipleAccounts = true, + account = account, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + val title = result.topAppBarUM.titleState as TitleState.WithAccount + val expectedIcon = AccountIconUM.CryptoPortfolio( + value = CryptoPortfolioIcon.Icon.Star, + color = CryptoPortfolioIcon.Color.Azure, + ) + assertThat(title.accountIconUM).isEqualTo(expectedIcon) + } + + private fun createTransformer( + hasMultipleWallets: Boolean, + hasMultipleAccounts: Boolean, + account: Account.CryptoPortfolio?, + ) = SetTopBarTitleTransformer( + cryptoCurrency = cryptoCurrency, + hasMultipleWallets = hasMultipleWallets, + hasMultipleAccounts = hasMultipleAccounts, + walletName = walletName, + deviceIconUM = deviceIconUM, + account = account, + ) + + private fun stubAccount( + iconValue: CryptoPortfolioIcon.Icon = CryptoPortfolioIcon.Icon.Star, + iconColor: CryptoPortfolioIcon.Color = CryptoPortfolioIcon.Color.Azure, + ): Account.CryptoPortfolio { + val icon: CryptoPortfolioIcon = mockk { + every { value } returns iconValue + every { color } returns iconColor + } + return mockk { + every { accountName } returns AccountName.DefaultMain + every { this@mockk.icon } returns icon + } + } + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = ""), + subtitle = stringReference(""), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt new file mode 100644 index 0000000000..f5c2d80d1e --- /dev/null +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTopBarMenuTransformerTest.kt @@ -0,0 +1,214 @@ +package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig +import com.tangem.core.ui.components.marketprice.MarketPriceBlockState +import com.tangem.core.ui.extensions.stringReference +import com.tangem.domain.card.CardTypesResolver +import com.tangem.domain.card.common.util.cardTypesResolver +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsUM +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import io.mockk.verify +import kotlinx.collections.immutable.persistentListOf +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test + +class UpdateTopBarMenuTransformerTest { + + private val coldWallet: UserWallet.Cold = mockk(relaxed = true) + private val hotWallet: UserWallet.Hot = mockk(relaxed = true) + private val cardTypesResolver: CardTypesResolver = mockk(relaxed = true) + private val onGenerateExtendedKey: () -> Unit = mockk(relaxed = true) + private val onHideClick: () -> Unit = mockk(relaxed = true) + + @BeforeEach + fun setUp() { + mockkStatic(UserWallet.Cold::cardTypesResolver) + every { coldWallet.cardTypesResolver } returns cardTypesResolver + } + + @AfterEach + fun tearDown() { + unmockkStatic(UserWallet.Cold::cardTypesResolver) + } + + @Test + fun `GIVEN cold wallet AND single wallet with token WHEN transform THEN menu is empty`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns true + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = true, + isXPubSupported = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).isEmpty() + } + + @Test + fun `GIVEN cold wallet AND multi-wallet WHEN transform THEN Hide item is the only one`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = false, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).hasSize(1) + + result.topAppBarUM.menuItems.single().onClick() + verify(exactly = 1) { onHideClick.invoke() } + verify(exactly = 0) { onGenerateExtendedKey.invoke() } + } + + @Test + fun `GIVEN hot wallet WHEN transform THEN Hide item is shown regardless of single-wallet flag`() { + // GIVEN — flag is read only for cold wallets, so no stub needed for hotWallet + val transformer = createTransformer( + userWallet = hotWallet, + hasDerivations = false, + isXPubSupported = false, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).hasSize(1) + } + + @Test + fun `GIVEN xPub supported AND derivations exist WHEN transform THEN both items are shown`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = true, + isXPubSupported = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN — Generate xPub first, Hide token second + assertThat(result.topAppBarUM.menuItems).hasSize(2) + } + + @Test + fun `GIVEN xPub supported but no derivations WHEN transform THEN xPub item is hidden`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).hasSize(1) + } + + @Test + fun `GIVEN derivations exist but xPub unsupported WHEN transform THEN xPub item is hidden`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = true, + isXPubSupported = false, + ) + + // WHEN + val result = transformer.transform(initialState()) + + // THEN + assertThat(result.topAppBarUM.menuItems).hasSize(1) + } + + @Test + fun `GIVEN any state WHEN transform THEN unrelated state fields are preserved`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val state = initialState() + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = false, + isXPubSupported = false, + ) + + // WHEN + val result = transformer.transform(state) + + // THEN — only menuItems is touched + assertThat(result.topAppBarUM.titleState).isEqualTo(state.topAppBarUM.titleState) + assertThat(result.topAppBarUM.subtitle).isEqualTo(state.topAppBarUM.subtitle) + assertThat(result.balanceBlockUM).isSameInstanceAs(state.balanceBlockUM) + assertThat(result.marketPriceBlockState).isSameInstanceAs(state.marketPriceBlockState) + } + + @Test + fun `GIVEN callbacks WHEN menu items invoked THEN callbacks are dispatched`() { + // GIVEN + every { cardTypesResolver.isSingleWalletWithToken() } returns false + val transformer = createTransformer( + userWallet = coldWallet, + hasDerivations = true, + isXPubSupported = true, + ) + + // WHEN + val result = transformer.transform(initialState()) + result.topAppBarUM.menuItems.forEach { it.onClick() } + + // THEN + verify(exactly = 1) { onGenerateExtendedKey.invoke() } + verify(exactly = 1) { onHideClick.invoke() } + } + + private fun createTransformer( + userWallet: UserWallet, + hasDerivations: Boolean, + isXPubSupported: Boolean, + ) = UpdateTopBarMenuTransformer( + userWallet = userWallet, + hasDerivations = hasDerivations, + isXPubSupported = isXPubSupported, + onGenerateExtendedKey = onGenerateExtendedKey, + onHideClick = onHideClick, + ) + + private fun initialState(): TokenDetailsUM = TokenDetailsUM( + topAppBarUM = TokenDetailsTopAppBarUM( + titleState = TitleState.Simple(tokenName = "Tether"), + subtitle = stringReference("ERC-20 in Ethereum network"), + onBackClick = {}, + menuItems = persistentListOf(), + ), + balanceBlockUM = mockk(relaxed = true), + marketPriceBlockState = mockk(relaxed = true), + stakingBlocksState = null, + pullToRefreshConfig = mockk(relaxed = true), + isBalanceHidden = false, + isMarketPriceAvailable = false, + ) +} \ No newline at end of file From 8966e28e293782172262728b4d8eb90799f42cb7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Apr 2026 18:57:53 +0400 Subject: [PATCH 13/32] Updated on 2026-08-14 --- .../domain/token/MockCryptoCurrencyFactory.kt | 8 +- .../local/preferences/PreferencesDataStore.kt | 2 + .../utils/SwapCurrencyIdMigration.kt | 151 ++++++++++++ .../utils/SwapCurrencyIdMigrationTest.kt | 224 ++++++++++++++++++ .../data/common/network/NetworkFactory.kt | 3 +- .../data/common/network/NetworkFactoryTest.kt | 9 +- .../NetworkStatusDataModelConverter.kt | 11 +- .../SimpleNetworkStatusConverter.kt | 11 +- .../store/DefaultNetworksStatusesStore.kt | 8 +- .../NetworkStatusDataModelConverterTest.kt | 5 +- .../SimpleNetworkStatusConverterTest.kt | 21 +- .../fetcher/CommonNetworkStatusFetcherTest.kt | 10 +- .../qrscanning/Bip321PaymentUriParserTest.kt | 19 +- .../DefaultQrScanningEventsRepositoryTest.kt | 10 +- .../qrscanning/Eip681PaymentUriParserTest.kt | 3 +- .../qrscanning/QrContentClassifierTest.kt | 3 +- .../qrscanning/SolanaPaymentUriParserTest.kt | 11 +- .../qrscanning/TronPaymentUriParserTest.kt | 11 +- .../DefaultCurrencyChecksRepository.kt | 8 +- .../DefaultGaslessTransactionRepository.kt | 8 +- .../MockedGaslessTransactionRepository.kt | 3 +- .../DefaultAllowanceRepositoryTest.kt | 17 +- .../utils/WcNetworksConverter.kt | 5 +- ...ultYieldSupplyTransactionRepositoryTest.kt | 3 +- .../AccountCryptoCurrencyStatusFinder.kt | 20 +- .../tangem/domain/models/network/Network.kt | 7 +- .../staking/model/StakingIntegrationID.kt | 2 +- .../domain/staking/StakingIdFactoryTest.kt | 11 +- .../staking/StakingIntegrationIDTest.kt | 7 +- .../tangem/domain/tokens/mock/MockNetworks.kt | 3 - domain/yield-supply/build.gradle.kts | 1 + .../YieldSupplyGetCurrentFeeUseCase.kt | 4 +- .../supply/YieldSupplyMinAmountUseCaseTest.kt | 3 +- .../YieldSupplyEnterStatusUseCaseTest.kt | 3 +- .../YieldSupplyGetCurrentFeeUseCaseTest.kt | 18 +- .../YieldSupplyGetDustMinAmountUseCaseTest.kt | 3 +- ...YieldSupplyGetRewardsBalanceUseCaseTest.kt | 6 +- .../usecase/YieldSupplyPendingTrackerTest.kt | 1 - .../PreviewCustomTokenSelectorComponent.kt | 1 - .../preview/PreviewManageTokensComponent.kt | 1 - .../PreviewOnboardingManageTokensComponent.kt | 1 - .../model/CustomTokenSelectorModel.kt | 4 +- .../extended/ui/FeeExtendedSelectorContent.kt | 1 - .../speed/ui/FeeSpeedSelectorContent.kt | 1 - .../token/ui/FeeTokenSelectorContent.kt | 1 - .../feeselector/ui/FeeSelectorBlockContent.kt | 1 - ...firmationNotificationsTransformerV2Test.kt | 3 +- ...firmationNotificationsTransformerV2Test.kt | 3 +- .../ui/preview/SwapAmountContentPreview.kt | 1 - .../ExpressStatusBottomSheetStateProvider.kt | 1 - .../YieldSupplyPromoBannerConverterTest.kt | 25 +- .../DefaultPromoDeeplinkHandlerTest.kt | 9 +- .../wallet/qr/QrContentClassifierTest.kt | 3 +- .../tangem/blockchainsdk/utils/NetworkExt.kt | 7 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 48 ++-- 55 files changed, 572 insertions(+), 192 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SwapCurrencyIdMigration.kt create mode 100644 core/datasource/src/test/kotlin/com/tangem/datasource/local/preferences/utils/SwapCurrencyIdMigrationTest.kt diff --git a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt index d5f73cbee0..79aea5c82e 100644 --- a/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt +++ b/common/test/src/main/java/com/tangem/common/test/domain/token/MockCryptoCurrencyFactory.kt @@ -55,8 +55,7 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul ) val network = Network( - id = Network.ID(blockchain.id, derivationPath), - backendId = blockchain.toNetworkId(), + id = Network.ID(value = blockchain.toNetworkId(), derivationPath = derivationPath), name = blockchain.fullName, isTestnet = blockchain.isTestnet(), derivationPath = derivationPath, @@ -85,12 +84,11 @@ class MockCryptoCurrencyFactory(private val userWallet: UserWallet.Cold = defaul return CryptoCurrency.Token( id = CryptoCurrency.ID( prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, - body = CryptoCurrency.ID.Body.NetworkId(blockchain.id), + body = CryptoCurrency.ID.Body.NetworkId(blockchain.toNetworkId()), suffix = CryptoCurrency.ID.Suffix.RawID(blockchain.id), ), network = Network( - id = Network.ID(value = blockchain.id, derivationPath), - backendId = "NEVER-MIND", + id = Network.ID(value = blockchain.toNetworkId(), derivationPath = derivationPath), name = blockchain.fullName, currencySymbol = "NEVER-MIND", derivationPath = derivationPath, diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt index db9df71d9d..49fc4000a4 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesDataStore.kt @@ -16,6 +16,7 @@ import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PR import com.tangem.datasource.local.preferences.PreferencesKeys.SHOULD_SHOW_RING_PROMO_KEY import com.tangem.datasource.local.preferences.utils.CleanupKeyMigration import com.tangem.datasource.local.preferences.utils.SharedPreferencesKeyMigration +import com.tangem.datasource.local.preferences.utils.SwapCurrencyIdMigration import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.logging.TangemLogger @@ -80,6 +81,7 @@ internal object PreferencesDataStore { legacyKeyName = LEGACY_DEFAULT_KEY_NAME, keyName = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY.name, ), + SwapCurrencyIdMigration(), CleanupKeyMigration(key = APP_LOGS_KEY), CleanupKeyMigration(key = IS_WALLET_SWAP_PROMO_OKX_SHOW_KEY), CleanupKeyMigration(key = IS_TOKEN_SWAP_PROMO_OKX_SHOW_KEY), diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SwapCurrencyIdMigration.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SwapCurrencyIdMigration.kt new file mode 100644 index 0000000000..aa573c9fae --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/SwapCurrencyIdMigration.kt @@ -0,0 +1,151 @@ +package com.tangem.datasource.local.preferences.utils + +import androidx.datastore.core.DataMigration +import androidx.datastore.preferences.core.Preferences +import com.tangem.datasource.local.preferences.PreferencesKeys + +/** + * Migrates cached CryptoCurrency.ID strings from old blockchain.id format to new networkId format. + * + * After refactoring, Network.rawId stores backendId values (e.g. "ethereum") instead of + * blockchain.id values (e.g. "ETH"). CryptoCurrency.ID body contains this value, so cached IDs + * like "coin⟨ETH⟩ethereum" must become "coin⟨ethereum⟩ethereum". + * + * Affected DataStore keys: [PreferencesKeys.SWAP_TRANSACTIONS_KEY], + * [PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY]. + */ +internal class SwapCurrencyIdMigration : DataMigration { + + override suspend fun shouldMigrate(currentData: Preferences): Boolean { + return currentData.contains(PreferencesKeys.SWAP_TRANSACTIONS_KEY) || + currentData.contains(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY) + } + + override suspend fun migrate(currentData: Preferences): Preferences { + val mutablePrefs = currentData.toMutablePreferences() + + currentData[PreferencesKeys.SWAP_TRANSACTIONS_KEY]?.let { json -> + mutablePrefs[PreferencesKeys.SWAP_TRANSACTIONS_KEY] = migrateJson(json) + } + + currentData[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY]?.let { json -> + mutablePrefs[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY] = migrateJson(json) + } + + return mutablePrefs.toPreferences() + } + + override suspend fun cleanUp() { + // nothing to clean up + } + + /** + * Replaces old blockchain.id values with networkId values inside CryptoCurrency.ID strings + * found anywhere in the JSON. Works by finding all `⟨oldId⟩` and `⟨oldId→` patterns and + * replacing the old ID with the new one. + */ + private fun migrateJson(json: String): String { + var result = json + + for ((oldId, newId) in BLOCKCHAIN_ID_TO_NETWORK_ID) { + // Body without derivation path: ⟨oldId⟩ → ⟨newId⟩ + result = result.replace("$BODY_START$oldId$BODY_END", "$BODY_START$newId$BODY_END") + // Body with derivation path: ⟨oldId→ → ⟨newId→ + result = result.replace( + "$BODY_START$oldId$DERIVATION_DELIMITER", + "$BODY_START$newId$DERIVATION_DELIMITER", + ) + } + + return result + } + + private companion object { + const val BODY_START = '\u27E8' // ⟨ + const val BODY_END = '\u27E9' // ⟩ + const val DERIVATION_DELIMITER = '\u2192' // → + + /** Mapping of old blockchain.id → new networkId (only entries where values differ). */ + val BLOCKCHAIN_ID_TO_NETWORK_ID = mapOf( + "ARBITRUM-ONE" to "arbitrum-one", + "ARBITRUM/test" to "arbitrum-one/test", + "AVALANCHE" to "avalanche", + "AVALANCHE/test" to "avalanche/test", + "BINANCE" to "binancecoin", + "BINANCE/test" to "binancecoin/test", + "BSC" to "binance-smart-chain", + "BSC/test" to "binance-smart-chain/test", + "BTC" to "bitcoin", + "BTC/test" to "bitcoin/test", + "BCH" to "bitcoin-cash", + "BCH/test" to "bitcoin-cash/test", + "CARDANO-S" to "cardano", + "DOGE" to "dogecoin", + "DUC" to "ducatus", + "ETH" to "ethereum", + "ETH/test" to "ethereum/test", + "ETC" to "ethereum-classic", + "ETC/test" to "ethereum-classic/test", + "ETH-Pow" to "ethereum-pow-iou", + "ETH-Pow/test" to "ethereum-pow-iou/test", + "FTM" to "fantom", + "FTM/test" to "fantom/test", + "GNO" to "xdai", + "KAS" to "kaspa", + "KAS/test" to "kaspa/test", + "KAVA" to "kava", + "KAVA/test" to "kava/test", + "Kusama" to "kusama", + "LTC" to "litecoin", + "NEAR" to "near-protocol", + "NEAR/test" to "near-protocol/test", + "NEXA" to "nexa", + "NEXA/test" to "nexa/test", + "OPTIMISM" to "optimistic-ethereum", + "Polkadot" to "polkadot", + "POLYGON" to "polygon-pos", + "POLYGON/test" to "polygon-pos/test", + "RSK" to "rootstock", + "SOLANA" to "solana", + "SOLANA/test" to "solana/test", + "TELOS" to "telos", + "TELOS/test" to "telos/test", + "The-Open-Network" to "the-open-network", + "The-Open-Network/test" to "the-open-network/test", + "TRON" to "tron", + "TRON/test" to "tron/test", + "XLM" to "stellar", + "XLM/test" to "stellar/test", + "XRP" to "xrp", + "XTZ" to "tezos", + "DASH" to "dash", + "xdc" to "xdc-network", + "xdc/test" to "xdc-network/test", + "hedera" to "hedera-hashgraph", + "hedera/test" to "hedera-hashgraph/test", + "areon" to "areon-network", + "areon/test" to "areon-network/test", + "pls" to "pulsechain", + "pls/test" to "pulsechain/test", + "zkSyncEra" to "zksync", + "zkSyncEra/test" to "zksync/test", + "polygonZkEVM" to "polygon-zkevm", + "polygonZkEVM/test" to "polygon-zkevm/test", + "flare" to "flare-network", + "flare/test" to "flare-network/test", + "playa3ull" to "playa3ull-games", + "sei" to "sei-network", + "sei/test" to "sei-network/test", + "casper" to "casper-network", + "casper/test" to "casper-network/test", + "odyssey" to "dione", + "odyssey/test" to "dione/test", + "hyperliquid" to "hyperevm", + "hyperliquid/test" to "hyperevm/test", + "quai" to "quai-network", + "quai/test" to "quai-network/test", + "manta/test" to "manta-pacific/test", + "dischain" to "ethereumfair", + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/test/kotlin/com/tangem/datasource/local/preferences/utils/SwapCurrencyIdMigrationTest.kt b/core/datasource/src/test/kotlin/com/tangem/datasource/local/preferences/utils/SwapCurrencyIdMigrationTest.kt new file mode 100644 index 0000000000..d6e94efd26 --- /dev/null +++ b/core/datasource/src/test/kotlin/com/tangem/datasource/local/preferences/utils/SwapCurrencyIdMigrationTest.kt @@ -0,0 +1,224 @@ +package com.tangem.datasource.local.preferences.utils + +import androidx.datastore.preferences.core.mutablePreferencesOf +import com.google.common.truth.Truth.assertThat +import com.tangem.datasource.local.preferences.PreferencesKeys +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.Test + +class SwapCurrencyIdMigrationTest { + + private val migration = SwapCurrencyIdMigration() + + // region shouldMigrate + + @Test + fun `shouldMigrate returns true when swap transactions key exists`() = runTest { + val prefs = mutablePreferencesOf(PreferencesKeys.SWAP_TRANSACTIONS_KEY to "[]") + + assertThat(migration.shouldMigrate(prefs)).isTrue() + } + + @Test + fun `shouldMigrate returns true when last swapped currency key exists`() = runTest { + val prefs = mutablePreferencesOf(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to "[]") + + assertThat(migration.shouldMigrate(prefs)).isTrue() + } + + @Test + fun `shouldMigrate returns true when both keys exist`() = runTest { + val prefs = mutablePreferencesOf( + PreferencesKeys.SWAP_TRANSACTIONS_KEY to "[]", + PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to "[]", + ) + + assertThat(migration.shouldMigrate(prefs)).isTrue() + } + + @Test + fun `shouldMigrate returns false when no keys exist`() = runTest { + val prefs = mutablePreferencesOf() + + assertThat(migration.shouldMigrate(prefs)).isFalse() + } + + // endregion + + // region migrate — coin IDs without derivation path + + @Test + fun `migrates simple coin ID - ETH to ethereum`() = runTest { + val result = migrateLastSwapped(currencyIdJson("coin${BS}ETH${BE}ethereum")) + + assertThat(result).isEqualTo(currencyIdJson("coin${BS}ethereum${BE}ethereum")) + } + + @Test + fun `migrates simple coin ID - BTC to bitcoin`() = runTest { + val result = migrateLastSwapped(currencyIdJson("coin${BS}BTC${BE}bitcoin")) + + assertThat(result).isEqualTo(currencyIdJson("coin${BS}bitcoin${BE}bitcoin")) + } + + @Test + fun `migrates BSC to binance-smart-chain`() = runTest { + val result = migrateLastSwapped(currencyIdJson("coin${BS}BSC${BE}binancecoin")) + + assertThat(result).isEqualTo(currencyIdJson("coin${BS}binance-smart-chain${BE}binancecoin")) + } + + // endregion + + // region migrate — coin IDs with derivation path + + @Test + fun `migrates coin ID with derivation path`() = runTest { + val result = migrateLastSwapped(currencyIdJson("coin${BS}ETH${DP}12367123${BE}ethereum")) + + assertThat(result).isEqualTo(currencyIdJson("coin${BS}ethereum${DP}12367123${BE}ethereum")) + } + + @Test + fun `migrates POLYGON with derivation path`() = runTest { + val result = migrateLastSwapped(currencyIdJson("coin${BS}POLYGON${DP}99999${BE}polygon-pos")) + + assertThat(result).isEqualTo(currencyIdJson("coin${BS}polygon-pos${DP}99999${BE}polygon-pos")) + } + + // endregion + + // region migrate — token IDs + + @Test + fun `migrates token ID with contract address`() = runTest { + val result = migrateLastSwapped(currencyIdJson("token${BS}ETH${BE}usdt${CA}0xdAC17")) + + assertThat(result).isEqualTo(currencyIdJson("token${BS}ethereum${BE}usdt${CA}0xdAC17")) + } + + @Test + fun `migrates token ID with derivation path and contract address`() = runTest { + val result = migrateLastSwapped( + currencyIdJson("token${BS}ETH${DP}12345${BE}usdt${CA}0xdAC17"), + ) + + assertThat(result).isEqualTo( + currencyIdJson("token${BS}ethereum${DP}12345${BE}usdt${CA}0xdAC17"), + ) + } + + // endregion + + // region migrate — swap transactions (both from and to IDs) + + @Test + fun `migrates both fromCryptoCurrencyId and toCryptoCurrencyId`() = runTest { + val from = "coin${BS}ETH${BE}ethereum" + val to = "coin${BS}BTC${BE}bitcoin" + val oldJson = "[{\"fromCryptoCurrencyId\":\"$from\",\"toCryptoCurrencyId\":\"$to\"}]" + + val expectedFrom = "coin${BS}ethereum${BE}ethereum" + val expectedTo = "coin${BS}bitcoin${BE}bitcoin" + val expected = "[{\"fromCryptoCurrencyId\":\"$expectedFrom\",\"toCryptoCurrencyId\":\"$expectedTo\"}]" + + val result = migrateSwapTransactions(oldJson) + + assertThat(result).isEqualTo(expected) + } + + // endregion + + // region migrate — no-op cases + + @Test + fun `does not modify already migrated IDs`() = runTest { + val json = currencyIdJson("coin${BS}ethereum${BE}ethereum") + + val result = migrateLastSwapped(json) + + assertThat(result).isEqualTo(json) + } + + @Test + fun `does not modify IDs where blockchain id equals networkId`() = runTest { + val json = currencyIdJson("coin${BS}cosmos${BE}cosmos") + + val result = migrateLastSwapped(json) + + assertThat(result).isEqualTo(json) + } + + @Test + fun `does not modify empty list`() = runTest { + val result = migrateLastSwapped("[]") + + assertThat(result).isEqualTo("[]") + } + + // endregion + + // region migrate — multiple entries + + @Test + fun `migrates multiple entries in list`() = runTest { + val old1 = currencyIdValue("coin${BS}ETH${BE}ethereum") + val old2 = currencyIdValue("coin${BS}TRON${BE}tron") + val oldJson = "[$old1,$old2]" + + val new1 = currencyIdValue("coin${BS}ethereum${BE}ethereum") + val new2 = currencyIdValue("coin${BS}tron${BE}tron") + val expected = "[$new1,$new2]" + + val result = migrateLastSwapped(oldJson) + + assertThat(result).isEqualTo(expected) + } + + // endregion + + // region migrate — preserves unrelated keys + + @Test + fun `preserves unrelated preference keys`() = runTest { + val unrelatedKey = PreferencesKeys.BALANCE_HIDING_SETTINGS_KEY + val unrelatedValue = "some_value" + val prefs = mutablePreferencesOf( + PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to currencyIdJson("coin${BS}ETH${BE}ethereum"), + unrelatedKey to unrelatedValue, + ) + + val result = migration.migrate(prefs) + + assertThat(result[unrelatedKey]).isEqualTo(unrelatedValue) + } + + // endregion + + // region helpers + + private suspend fun migrateLastSwapped(json: String): String? { + val prefs = mutablePreferencesOf(PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY to json) + val result = migration.migrate(prefs) + return result[PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY] + } + + private suspend fun migrateSwapTransactions(json: String): String? { + val prefs = mutablePreferencesOf(PreferencesKeys.SWAP_TRANSACTIONS_KEY to json) + val result = migration.migrate(prefs) + return result[PreferencesKeys.SWAP_TRANSACTIONS_KEY] + } + + private fun currencyIdJson(id: String): String = "[${currencyIdValue(id)}]" + + private fun currencyIdValue(id: String): String = "{\"cryptoCurrencyId\":\"$id\"}" + + private companion object { + const val BS = '\u27E8' // ⟨ body start + const val BE = '\u27E9' // ⟩ body end + const val DP = '\u2192' // → derivation path delimiter + const val CA = '\u2693' // ⚓ contract address delimiter + } + + // endregion +} \ No newline at end of file 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 97ba053720..4d9c4d9cdc 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 @@ -136,8 +136,7 @@ class NetworkFactory @Inject constructor( return runCatching { Network( - id = Network.ID(value = blockchain.id, derivationPath = derivationPath), - backendId = blockchain.toNetworkId(), + id = Network.ID(value = blockchain.toNetworkId(), derivationPath = derivationPath), name = blockchain.fullName, isTestnet = blockchain.isTestnet(), derivationPath = derivationPath, 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..cfc2969751 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 @@ -3,6 +3,7 @@ package com.tangem.data.common.network import com.google.common.truth.Truth import com.tangem.blockchain.common.Blockchain import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.common.test.domain.card.MockScanResponseFactory import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.common.test.domain.wallet.MockUserWalletFactory @@ -187,7 +188,7 @@ class NetworkFactoryTest { userWallet = userWallet, expected = MockCryptoCurrencyFactory().ethereum.network.copy( id = Network.ID( - value = Blockchain.Ethereum.id, + value = Blockchain.Ethereum.toNetworkId(), derivationPath = expectedDerivationPath, ), derivationPath = expectedDerivationPath, @@ -201,12 +202,12 @@ class NetworkFactoryTest { derivationPath: Network.DerivationPath, ): CreateTestModel.Second { return CreateTestModel.Second( - networkId = Network.ID(value = Blockchain.Ethereum.id, derivationPath = derivationPath), + networkId = Network.ID(value = Blockchain.Ethereum.toNetworkId(), derivationPath = derivationPath), derivationPath = derivationPath, userWallet = userWallet, expected = MockCryptoCurrencyFactory().ethereum.network.copy( id = Network.ID( - value = Blockchain.Ethereum.id, + value = Blockchain.Ethereum.toNetworkId(), derivationPath = derivationPath, ), derivationPath = derivationPath, @@ -227,7 +228,7 @@ class NetworkFactoryTest { derivationStyleProvider = derivationStyleProvider, canHandleTokens = canHandleTokens, expected = MockCryptoCurrencyFactory().ethereum.network.copy( - id = Network.ID(value = Blockchain.Ethereum.id, derivationPath = expectedDerivationPath), + id = Network.ID(value = Blockchain.Ethereum.toNetworkId(), derivationPath = expectedDerivationPath), derivationPath = expectedDerivationPath, canHandleTokens = canHandleTokens, ), diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt index d6878c7f82..d876b01234 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt @@ -1,5 +1,6 @@ package com.tangem.data.networks.converters +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.models.network.NetworkStatus import com.tangem.utils.converter.Converter @@ -15,17 +16,18 @@ internal object NetworkStatusDataModelConverter : Converter { val address = NetworkAddressConverter.convertBack(value = status.address) + val blockchainId = value.network.toBlockchain().id val amountsConverter = NetworkAmountsConverter( - rawNetworkId = value.network.rawId, + rawNetworkId = blockchainId, derivationPath = value.network.derivationPath, ) val yieldSupplyStatusConverter = NetworkYieldSupplyStatusConverter( - rawNetworkId = value.network.rawId, + rawNetworkId = blockchainId, derivationPath = value.network.derivationPath, ) NetworkStatusDM.Verified( - networkId = NetworkStatusDM.ID(value = value.network.rawId), + networkId = NetworkStatusDM.ID(value = blockchainId), derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath), selectedAddress = address.selectedAddress, availableAddresses = address.addresses, @@ -35,9 +37,10 @@ internal object NetworkStatusDataModelConverter : Converter { val address = NetworkAddressConverter.convertBack(value = status.address) + val blockchainId = value.network.toBlockchain().id NetworkStatusDM.NoAccount( - networkId = NetworkStatusDM.ID(value = value.network.rawId), + networkId = NetworkStatusDM.ID(value = blockchainId), derivationPath = NetworkDerivationPathConverter.convertBack(value = value.network.derivationPath), selectedAddress = address.selectedAddress, availableAddresses = address.addresses, diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt index 287e82b3fa..b82bfb9455 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt @@ -1,5 +1,7 @@ package com.tangem.data.networks.converters +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.networks.models.SimpleNetworkStatus import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.models.StatusSource @@ -23,14 +25,15 @@ internal object SimpleNetworkStatusConverter : Converter> * Default implementation of [NetworksStatusesStore] * * @param context context + * @param scope app coroutine scope * @property runtimeStore runtime store * @property persistenceDataStore persistence store - * @param dispatchers dispatchers */ internal class DefaultNetworksStatusesStore( context: Context, + scope: AppCoroutineScope, private val runtimeStore: RuntimeSharedStore, private val persistenceDataStore: DataStore, - private val scope: AppCoroutineScope, ) : NetworksStatusesStore { init { @@ -112,7 +113,8 @@ internal class DefaultNetworksStatusesStore( storedStatuses.toMutableMap().apply { val updatedValues = this[userWalletId.stringValue].orEmpty().filterNot { networks.any { network -> - it.networkId.value == network.rawId && it.derivationPath.value == network.derivationPath.value + it.networkId.value == network.toBlockchain().id && + it.derivationPath.value == network.derivationPath.value } } diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt index 97265f47a1..b995196d80 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt @@ -1,6 +1,7 @@ package com.tangem.data.networks.converters import com.google.common.truth.Truth +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.datasource.local.network.entity.NetworkStatusDM.* @@ -74,7 +75,7 @@ internal class NetworkStatusDataModelConverterTest { ), ), expected = Verified( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "", type = DerivationPath.Type.NONE, @@ -119,7 +120,7 @@ internal class NetworkStatusDataModelConverterTest { ), ), expected = NoAccount( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "", type = DerivationPath.Type.NONE, diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt index 1b57d19996..a7413a891e 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt @@ -1,6 +1,7 @@ package com.tangem.data.networks.converters import com.google.common.truth.Truth +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.data.networks.models.SimpleNetworkStatus import com.tangem.datasource.local.network.entity.NetworkStatusDM @@ -48,7 +49,7 @@ internal class SimpleNetworkStatusConverterTest { // region Verified ConvertModel( value = Verified( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "card", type = DerivationPath.Type.CARD, @@ -74,7 +75,7 @@ internal class SimpleNetworkStatusConverterTest { ), expected = SimpleNetworkStatus( id = Network.ID( - value = network.rawId, + value = network.backendId, derivationPath = Network.DerivationPath.Card("card"), ), value = NetworkStatus.Verified( @@ -116,7 +117,7 @@ internal class SimpleNetworkStatusConverterTest { // region NoAccount ConvertModel( value = NoAccount( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "card", type = DerivationPath.Type.CARD, @@ -137,7 +138,7 @@ internal class SimpleNetworkStatusConverterTest { ), expected = SimpleNetworkStatus( id = Network.ID( - value = network.rawId, + value = network.backendId, derivationPath = Network.DerivationPath.Card("card"), ), value = NetworkStatus.NoAccount( @@ -168,7 +169,7 @@ internal class SimpleNetworkStatusConverterTest { // region Error ConvertModel( value = Verified( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "card", type = DerivationPath.Type.CARD, @@ -189,7 +190,7 @@ internal class SimpleNetworkStatusConverterTest { ), ConvertModel( value = Verified( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "card", type = DerivationPath.Type.CARD, @@ -205,7 +206,7 @@ internal class SimpleNetworkStatusConverterTest { ), ConvertModel( value = Verified( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "card", type = DerivationPath.Type.CARD, @@ -230,7 +231,7 @@ internal class SimpleNetworkStatusConverterTest { ), ConvertModel( value = NoAccount( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "card", type = DerivationPath.Type.CARD, @@ -255,7 +256,7 @@ internal class SimpleNetworkStatusConverterTest { ), ConvertModel( value = NoAccount( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "card", type = DerivationPath.Type.CARD, @@ -276,7 +277,7 @@ internal class SimpleNetworkStatusConverterTest { ), ConvertModel( value = NoAccount( - networkId = ID(network.rawId), + networkId = ID(network.toBlockchain().id), derivationPath = DerivationPath( value = "card", type = DerivationPath.Type.CARD, diff --git a/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt index e6f9bf012d..a36840920e 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcherTest.kt @@ -52,7 +52,7 @@ internal class CommonNetworkStatusFetcherTest { val userWalletId = UserWalletId("011") val network = cryptoCurrencyFactory.ethereum.network val extraTokens = setOf( - cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token, + cryptoCurrencyFactory.createToken(Blockchain.Ethereum), ) val updateException = IllegalStateException() @@ -80,7 +80,7 @@ internal class CommonNetworkStatusFetcherTest { val userWalletId = UserWalletId("011") val network = cryptoCurrencyFactory.ethereum.network val extraTokens = setOf( - cryptoCurrencyFactory.createToken(Blockchain.Ethereum) as CryptoCurrency.Token, + cryptoCurrencyFactory.createToken(Blockchain.Ethereum), ) val updateResult = model.updateResult val status = model.status @@ -143,15 +143,15 @@ internal class CommonNetworkStatusFetcherTest { it.copy( amounts = mapOf( CryptoCurrency.ID.fromValue( - value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND", + value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND", ) to NetworkStatus.Amount.NotFound, ), pendingTransactions = mapOf( - CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND") to emptySet(), + CryptoCurrency.ID.fromValue(value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND") to emptySet(), ), yieldSupplyStatuses = mapOf( CryptoCurrency.ID.fromValue( - value = "token⟨ETH⟩NEVER-MIND⚓NEVER-MIND", + value = "token⟨ethereum⟩NEVER-MIND⚓NEVER-MIND", ) to null, ), ) diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Bip321PaymentUriParserTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Bip321PaymentUriParserTest.kt index cac610a9e7..19478a403d 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Bip321PaymentUriParserTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Bip321PaymentUriParserTest.kt @@ -176,7 +176,7 @@ internal class Bip321PaymentUriParserTest { @Test fun `includes tokens on matching network`() { - val btcToken = buildToken("BTC", "RUNE", "contractAddr") + val btcToken = buildToken("bitcoin", "RUNE", "contractAddr") val result = parser.parse( qrCode = "bitcoin:1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa?amount=0.01", @@ -263,7 +263,13 @@ internal class Bip321PaymentUriParserTest { @Test fun `memo on network with memo support is not unsupported`() { - val xrpCoin = buildCoin("XRP", "XRP", "XRP", decimals = 6, extrasType = Network.TransactionExtrasType.DESTINATION_TAG) + val xrpCoin = buildCoin( + rawNetworkId = "xrp", + name = "XRP", + symbol = "XRP", + decimals = 6, + extrasType = Network.TransactionExtrasType.DESTINATION_TAG, + ) val result = parser.parse( qrCode = "ripple:rAddress?dt=12345", @@ -299,9 +305,9 @@ internal class Bip321PaymentUriParserTest { } } - private val bitcoinCoin = buildCoin("BTC", "Bitcoin", "BTC", decimals = 8) - private val litecoinCoin = buildCoin("LTC", "Litecoin", "LTC", decimals = 8) - private val dogecoinCoin = buildCoin("DOGE", "Dogecoin", "DOGE", decimals = 8) + private val bitcoinCoin = buildCoin("bitcoin", "Bitcoin", "BTC", decimals = 8) + private val litecoinCoin = buildCoin("litecoin", "Litecoin", "LTC", decimals = 8) + private val dogecoinCoin = buildCoin("dogecoin", "Dogecoin", "DOGE", decimals = 8) private fun buildCoin( rawNetworkId: String, @@ -349,8 +355,7 @@ internal class Bip321PaymentUriParserTest { extrasType: Network.TransactionExtrasType = Network.TransactionExtrasType.NONE, ): Network { return Network( - id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None), - backendId = rawNetworkId, + id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None), name = name, currencySymbol = symbol, derivationPath = Network.DerivationPath.None, diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt index 3aecfcfbfa..f904a7a32b 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt @@ -1,7 +1,6 @@ package com.tangem.data.qrscanning import com.google.common.truth.Truth -import com.tangem.blockchain.common.Blockchain import com.tangem.data.qrscanning.parser.QrContentClassifierParser import com.tangem.data.qrscanning.repository.DefaultQrScanningEventsRepository import com.tangem.domain.models.currency.CryptoCurrency @@ -78,7 +77,8 @@ internal class DefaultQrScanningEventsRepositoryTest { @Test fun testBip021() { - every { network.id.rawId.value } returns Blockchain.Bitcoin.id + every { network.id } returns Network.ID(value = "bitcoin", derivationPath = Network.DerivationPath.None) + every { network.backendId } returns "bitcoin" positiveCase( "$schema1:$address1", QrResult(address = address1), @@ -128,7 +128,8 @@ internal class DefaultQrScanningEventsRepositoryTest { @Test fun testErc681Coin() { - every { network.id.rawId.value } returns Blockchain.Ethereum.id + every { network.id } returns Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None) + every { network.backendId } returns "ethereum" positiveCase( address2, QrResult(address = address2), @@ -183,7 +184,8 @@ internal class DefaultQrScanningEventsRepositoryTest { @Test fun testErc681Token() { - every { network.id.rawId.value } returns Blockchain.Ethereum.id + every { network.id } returns Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None) + every { network.backendId } returns "ethereum" positiveCase( address2, QrResult(address = address2), diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Eip681PaymentUriParserTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Eip681PaymentUriParserTest.kt index a98cbe0026..7dba3a75fe 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Eip681PaymentUriParserTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/Eip681PaymentUriParserTest.kt @@ -382,8 +382,7 @@ internal class Eip681PaymentUriParserTest { private fun buildNetwork(rawNetworkId: String): Network { return Network( - id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None), - backendId = rawNetworkId, + id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None), name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = Network.DerivationPath.None, diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/QrContentClassifierTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/QrContentClassifierTest.kt index af88722a8d..d1259b8904 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/QrContentClassifierTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/QrContentClassifierTest.kt @@ -233,8 +233,7 @@ internal class QrContentClassifierTest { private fun buildNetwork(rawNetworkId: String): Network { return Network( - id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None), - backendId = rawNetworkId, + id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None), name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = Network.DerivationPath.None, diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/SolanaPaymentUriParserTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/SolanaPaymentUriParserTest.kt index 068663caed..4c692cc027 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/SolanaPaymentUriParserTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/SolanaPaymentUriParserTest.kt @@ -198,13 +198,13 @@ internal class SolanaPaymentUriParserTest { } } - private val solanaNetwork = buildNetwork("SOLANA", "Solana", "SOL") + private val solanaNetwork = buildNetwork("solana", "Solana", "SOL") private val solanaCoin = CryptoCurrency.Coin( id = CryptoCurrency.ID( prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX, - body = CryptoCurrency.ID.Body.NetworkId("SOLANA"), - suffix = CryptoCurrency.ID.Suffix.RawID("SOLANA"), + body = CryptoCurrency.ID.Body.NetworkId("solana"), + suffix = CryptoCurrency.ID.Suffix.RawID("solana"), ), network = solanaNetwork, name = "Solana", @@ -217,7 +217,7 @@ internal class SolanaPaymentUriParserTest { private val usdcToken = CryptoCurrency.Token( id = CryptoCurrency.ID( prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, - body = CryptoCurrency.ID.Body.NetworkId("SOLANA"), + body = CryptoCurrency.ID.Body.NetworkId("solana"), suffix = CryptoCurrency.ID.Suffix.RawID("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"), ), network = solanaNetwork, @@ -231,8 +231,7 @@ internal class SolanaPaymentUriParserTest { private fun buildNetwork(rawNetworkId: String, name: String, symbol: String): Network { return Network( - id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None), - backendId = rawNetworkId, + id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None), name = name, currencySymbol = symbol, derivationPath = Network.DerivationPath.None, diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/TronPaymentUriParserTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/TronPaymentUriParserTest.kt index 8c07742046..64bab29d5c 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/TronPaymentUriParserTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/TronPaymentUriParserTest.kt @@ -238,13 +238,13 @@ internal class TronPaymentUriParserTest { } } - private val tronNetwork = buildNetwork("TRON", "Tron", "TRX") + private val tronNetwork = buildNetwork("tron", "Tron", "TRX") private val tronCoin = CryptoCurrency.Coin( id = CryptoCurrency.ID( prefix = CryptoCurrency.ID.Prefix.COIN_PREFIX, - body = CryptoCurrency.ID.Body.NetworkId("TRON"), - suffix = CryptoCurrency.ID.Suffix.RawID("TRON"), + body = CryptoCurrency.ID.Body.NetworkId("tron"), + suffix = CryptoCurrency.ID.Suffix.RawID("tron"), ), network = tronNetwork, name = "Tron", @@ -257,7 +257,7 @@ internal class TronPaymentUriParserTest { private val usdtToken = CryptoCurrency.Token( id = CryptoCurrency.ID( prefix = CryptoCurrency.ID.Prefix.TOKEN_PREFIX, - body = CryptoCurrency.ID.Body.NetworkId("TRON"), + body = CryptoCurrency.ID.Body.NetworkId("tron"), suffix = CryptoCurrency.ID.Suffix.RawID("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"), ), network = tronNetwork, @@ -271,8 +271,7 @@ internal class TronPaymentUriParserTest { private fun buildNetwork(rawNetworkId: String, name: String, symbol: String): Network { return Network( - id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None), - backendId = rawNetworkId, + id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None), name = name, currencySymbol = symbol, derivationPath = Network.DerivationPath.None, diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt index 9f1b39e4d4..f258d37a0b 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrencyChecksRepository.kt @@ -2,7 +2,11 @@ package com.tangem.data.tokens.repository import com.tangem.blockchain.blockchains.ethereum.eip1559.isGaslessTxSupported import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider -import com.tangem.blockchain.common.* +import com.tangem.blockchain.common.FeeResourceAmountProvider +import com.tangem.blockchain.common.MinimumSendAmountProvider +import com.tangem.blockchain.common.ReserveAmountProvider +import com.tangem.blockchain.common.UtxoAmountLimitProvider +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.getTotalStakingBalance import com.tangem.data.tokens.converters.UtxoConverter import com.tangem.domain.models.currency.CryptoCurrency @@ -67,7 +71,7 @@ internal class DefaultCurrencyChecksRepository( } override fun isNetworkSupportedForGaslessTx(network: Network): Boolean { - val blockchain = Blockchain.fromId(network.rawId) + val blockchain = network.toBlockchain() return blockchain.isGaslessTxSupported } diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt index 849883a543..2664312cb5 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultGaslessTransactionRepository.kt @@ -1,7 +1,7 @@ package com.tangem.data.transaction -import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.transaction.convertes.GaslessSignedTransactionResultConverter import com.tangem.data.transaction.convertes.GaslessTransactionRequestBuilder @@ -45,7 +45,7 @@ class DefaultGaslessTransactionRepository( val supportedTokensData = gaslessTxServiceApi.getSupportedTokens().getOrThrow() if (supportedTokensData.isSuccess) { - val networkBlockchain = Blockchain.fromId(network.rawId) + val networkBlockchain = network.toBlockchain() val supportedTokens = supportedTokensData.result.tokens .filter { it.chainId == networkBlockchain.getChainId() @@ -100,7 +100,7 @@ class DefaultGaslessTransactionRepository( network: Network, eip7702Auth: Eip7702Authorization?, ): GaslessSignedTransactionResult = withContext(coroutineDispatcherProvider.io) { - val blockchain = Blockchain.fromId(network.rawId) + val blockchain = network.toBlockchain() val transactionRequest = gaslessTransactionRequestBuilder.build( gaslessTransaction = gaslessTransactionData, signature = signature, @@ -124,7 +124,7 @@ class DefaultGaslessTransactionRepository( } override fun getChainIdForNetwork(network: Network): Int { - val networkBlockchain = Blockchain.fromId(network.rawId) + val networkBlockchain = network.toBlockchain() return networkBlockchain.getChainId() ?: error("ChainId not found for blockchain ${networkBlockchain.name}") } diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/MockedGaslessTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/MockedGaslessTransactionRepository.kt index be0a8b25f4..2d6b86ba9a 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/MockedGaslessTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/MockedGaslessTransactionRepository.kt @@ -2,6 +2,7 @@ package com.tangem.data.transaction import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -30,7 +31,7 @@ class MockedGaslessTransactionRepository( } override fun getChainIdForNetwork(network: Network): Int { - val networkBlockchain = Blockchain.fromId(network.rawId) + val networkBlockchain = network.toBlockchain() return networkBlockchain.getChainId() ?: error("ChainId not found for blockchain ${networkBlockchain.name}") } diff --git a/data/transaction/src/test/kotlin/com/tangem/data/transaction/DefaultAllowanceRepositoryTest.kt b/data/transaction/src/test/kotlin/com/tangem/data/transaction/DefaultAllowanceRepositoryTest.kt index 0211c42b33..b7f49cec2b 100644 --- a/data/transaction/src/test/kotlin/com/tangem/data/transaction/DefaultAllowanceRepositoryTest.kt +++ b/data/transaction/src/test/kotlin/com/tangem/data/transaction/DefaultAllowanceRepositoryTest.kt @@ -171,7 +171,11 @@ class DefaultAllowanceRepositoryTest { @Test fun `returns NotEnough when partial allowance for non-tether token`() = runTest { - val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "usd-coin") + val token = buildToken( + rawNetworkId = "ethereum", + rawCurrencyId = "usd-coin", + contractAddress = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + ) coEvery { (approverWalletManager as Approver).getAllowance(spenderAddress, any()) @@ -187,7 +191,7 @@ class DefaultAllowanceRepositoryTest { @Test fun `returns NotEnough when partial allowance for tether on non-ethereum network`() = runTest { - val token = buildToken(rawNetworkId = "polygon", rawCurrencyId = "tether") + val token = buildToken(rawNetworkId = "polygon-pos", rawCurrencyId = "tether") coEvery { (approverWalletManager as Approver).getAllowance(spenderAddress, any()) @@ -200,7 +204,7 @@ class DefaultAllowanceRepositoryTest { @Test fun `returns ResetNeeded when partial allowance for tether on ethereum`() = runTest { - val token = buildToken(rawNetworkId = "ETH", rawCurrencyId = "tether") + val token = buildToken(rawNetworkId = "ethereum", rawCurrencyId = "tether") coEvery { (approverWalletManager as Approver).getAllowance(spenderAddress, any()) @@ -216,7 +220,7 @@ class DefaultAllowanceRepositoryTest { @Test fun `returns ResetNeeded when partial allowance for tether on ethereum testnet`() = runTest { - val token = buildToken(rawNetworkId = "ETH/test", rawCurrencyId = "tether") + val token = buildToken(rawNetworkId = "ethereum/test", rawCurrencyId = "tether") coEvery { (approverWalletManager as Approver).getAllowance(spenderAddress, any()) @@ -248,8 +252,7 @@ class DefaultAllowanceRepositoryTest { private fun buildNetwork(rawNetworkId: String): Network { val derivationPath = Network.DerivationPath.None return Network( - id = Network.ID(Network.RawID(rawNetworkId), derivationPath), - backendId = rawNetworkId, + id = Network.ID(value = rawNetworkId, derivationPath = derivationPath), name = rawNetworkId.replaceFirstChar { it.uppercase() }, currencySymbol = "ETH", derivationPath = derivationPath, @@ -263,7 +266,7 @@ class DefaultAllowanceRepositoryTest { } private fun buildToken( - rawNetworkId: String = "ETH", + rawNetworkId: String = "ethereum", rawCurrencyId: String = "tether", contractAddress: String = "0xdAC17F958D2ee523a2206206994597C13D831ec7", ): CryptoCurrency.Token { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt index 8ef4d7a69e..b295d0356d 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/utils/WcNetworksConverter.kt @@ -4,6 +4,7 @@ import com.reown.walletkit.client.Wallet import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.address.AddressType import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.data.common.currency.isCustomCoin import com.tangem.data.walletconnect.model.CAIP10 import com.tangem.domain.account.status.producer.SingleAccountStatusListProducer @@ -84,7 +85,7 @@ internal class WcNetworksConverter @Inject constructor( val blockchain = namespaceConverters .firstNotNullOfOrNull { it.toBlockchain(rawChainId) } ?: return listOf() - val allCoinNetwork = portfolioNetworks.filter { it.rawId == blockchain.id } + val allCoinNetwork = portfolioNetworks.filter { it.rawId == blockchain.toNetworkId() } return allCoinNetwork } @@ -103,7 +104,7 @@ internal class WcNetworksConverter @Inject constructor( ?: return@mapNotNullTo null portfolioNetworks // find all derivation - .filter { it.rawId == blockchain.id } + .filter { it.rawId == blockchain.toNetworkId() } // find equal address .firstOrNull { network -> val walletAddress = getAddressForWC(wallet.walletId, network) diff --git a/data/yield-supply/src/test/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepositoryTest.kt b/data/yield-supply/src/test/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepositoryTest.kt index cf8174d299..6d5251e8df 100644 --- a/data/yield-supply/src/test/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepositoryTest.kt +++ b/data/yield-supply/src/test/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepositoryTest.kt @@ -29,13 +29,14 @@ import com.tangem.blockchain.yieldsupply.providers.YieldSupplyStatus as SDKYield @TestInstance(TestInstance.Lifecycle.PER_CLASS) class DefaultYieldSupplyTransactionRepositoryTest { - private val networkId = Network.ID(value = "ETH/test", derivationPath = Network.DerivationPath.None) + private val networkId = Network.ID(value = "ethereum/test", derivationPath = Network.DerivationPath.None) private val mockedContractAddress = "0x000000000000000000000000000000000000" private val yieldContractAddress = "0x1234" private val userWalletId = mockk() private val cryptoCurrency = mockk(relaxed = true) { every { network.id } returns networkId + every { network.backendId } returns networkId.rawId.value every { contractAddress } returns mockedContractAddress } private val cryptoCurrencyStatus = mockk(relaxed = true) { diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt index 24da2b65e0..9c33c3153e 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/utils/AccountCryptoCurrencyStatusFinder.kt @@ -1,6 +1,6 @@ package com.tangem.domain.account.status.utils -import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountStatusList import com.tangem.domain.account.status.model.AccountCryptoCurrency @@ -180,7 +180,7 @@ internal object AccountCryptoCurrencyStatusFinder { contractAddress: String?, ): AccountCryptoCurrency? { return accountList.getExpectedAccounts( - rawNetworkId = networkId.rawId.value, + rawNetworkId = networkId.rawId, derivationPath = derivationPath, ) .asSequence() @@ -220,7 +220,7 @@ internal object AccountCryptoCurrencyStatusFinder { internal fun AccountStatusList.getExpectedAccountStatuses(networkId: Network.ID): List { val possibleAccountIndex = getAccountIndexOrNull( - rawNetworkId = networkId.rawId.value, + rawNetworkId = networkId.rawId, derivationPath = networkId.derivationPath, ) @@ -239,7 +239,7 @@ internal object AccountCryptoCurrencyStatusFinder { } internal fun AccountStatusList.getExpectedAccountStatuses(networks: List): List { - val possibleAccountIndexes = networks.mapNotNull { getAccountIndexOrNull(it.rawId, it.derivationPath) } + val possibleAccountIndexes = networks.mapNotNull { getAccountIndexOrNull(it.id.rawId, it.derivationPath) } if (possibleAccountIndexes.isEmpty()) return accountStatuses @@ -256,16 +256,14 @@ internal object AccountCryptoCurrencyStatusFinder { // region AccountList helpers internal fun AccountList.getExpectedAccounts(network: Network?): List { - return getExpectedAccounts(rawNetworkId = network?.rawId, derivationPath = network?.derivationPath) + return getExpectedAccounts(rawNetworkId = network?.id?.rawId, derivationPath = network?.derivationPath) } private fun AccountList.getExpectedAccounts( - rawNetworkId: String?, + rawNetworkId: Network.RawID?, derivationPath: Network.DerivationPath?, ): List { - val possibleAccountIndex = getAccountIndexOrNull(rawNetworkId, derivationPath) - - return when (possibleAccountIndex) { + return when (val possibleAccountIndex = getAccountIndexOrNull(rawNetworkId, derivationPath)) { null -> accounts DerivationIndex.Main.value -> listOf(mainAccount) // currency only in the account with specific derivation index or in the main account @@ -283,10 +281,10 @@ internal object AccountCryptoCurrencyStatusFinder { // region Common helpers - private fun getAccountIndexOrNull(rawNetworkId: String?, derivationPath: Network.DerivationPath?): Int? { + private fun getAccountIndexOrNull(rawNetworkId: Network.RawID?, derivationPath: Network.DerivationPath?): Int? { if (rawNetworkId == null || derivationPath == null) return null - val blockchain = Blockchain.fromId(id = rawNetworkId) + val blockchain = rawNetworkId.toBlockchain() val recognizer = AccountNodeRecognizer(blockchain) return recognizer.recognize(derivationPath)?.toInt() diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt index b5a972d24c..53be78f1ff 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt @@ -10,7 +10,6 @@ import kotlinx.serialization.Serializable * (e.g., ERC20, BEP20). * * @property id the unique identifier of the network - * @property backendId the name of this network in the Tangem backend * @property name the human-readable name of the network, such as "Ethereum" or "Bitcoin" * @property currencySymbol the symbol of the currency associated with the network * @property derivationPath the path used to derive keys for this network @@ -25,7 +24,6 @@ import kotlinx.serialization.Serializable @Serializable data class Network( val id: ID, - val backendId: String, val name: String, val currencySymbol: String, val derivationPath: DerivationPath, @@ -37,6 +35,11 @@ data class Network( val nameResolvingType: NameResolvingType, ) { + /** Backend ID */ + @Deprecated("Will be removed later") + val backendId: String + get() = id.rawId.value + /** Raw ID */ val rawId: String get() = id.rawId.value diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt index db33de7982..0705a3a520 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/model/StakingIntegrationID.kt @@ -147,7 +147,7 @@ sealed interface StakingIntegrationID { * @return a [StakingIntegrationID] if supported, or `null` if not supported. */ fun create(currencyId: CryptoCurrency.ID): StakingIntegrationID? { - val blockchain = Blockchain.fromId(id = currencyId.rawNetworkId) + val blockchain = currencyId.toBlockchain() return if (currencyId.contractAddress.isNullOrBlank()) { // Order is not important — either P2PEthPool or Stakekit.Coin can be in any order diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt index 3211cdff64..1d754890f5 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIdFactoryTest.kt @@ -5,6 +5,7 @@ import arrow.core.left import arrow.core.right import com.google.common.truth.Truth import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.staking.StakingID @@ -13,11 +14,7 @@ import com.tangem.domain.staking.model.StakingIntegrationID import com.tangem.domain.staking.toggles.StakingFeatureToggles import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.test.core.ProvideTestModels -import io.mockk.clearMocks -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.every -import io.mockk.mockk +import io.mockk.* import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested @@ -188,7 +185,7 @@ internal class StakingIdFactoryTest { ), CreateModel( currencyId = CryptoCurrency.ID.fromValue( - value = "token⟨ETH⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", + value = "token⟨ethereum⟩polygon-ecosystem-token⚓0x7d1afa7b718fb893db30a3abc0cfc608aacfebb0", ), expected = createStakingId(integrationId = StakingIntegrationID.StakeKit.EthereumToken.Polygon), ), @@ -202,6 +199,6 @@ internal class StakingIdFactoryTest { data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: Either) private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID { - return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}⟩") + return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.toNetworkId()}⟩") } } \ No newline at end of file diff --git a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt index c62e943341..c35f5af029 100644 --- a/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt +++ b/domain/staking/src/test/kotlin/com/tangem/domain/staking/StakingIntegrationIDTest.kt @@ -2,6 +2,7 @@ package com.tangem.domain.staking import com.google.common.truth.Truth import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingIntegrationID @@ -144,11 +145,11 @@ class StakingIntegrationIDTest { expected = StakingIntegrationID.P2PEthPool, ), CreateModel( - currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ETH⟩polygon-ecosystem-token⚓1234567890"), + currencyId = CryptoCurrency.ID.fromValue(value = "token⟨ethereum⟩polygon-ecosystem-token⚓1234567890"), expected = StakingIntegrationID.StakeKit.EthereumToken.Polygon, ), CreateModel( - currencyId = CryptoCurrency.ID.fromValue(value = "token⟨SOLANA⟩solana⚓1234567890"), + currencyId = CryptoCurrency.ID.fromValue(value = "token⟨solana⟩solana⚓1234567890"), expected = null, ), ) @@ -157,6 +158,6 @@ class StakingIntegrationIDTest { data class CreateModel(val currencyId: CryptoCurrency.ID, val expected: StakingIntegrationID?) private fun createCurrencyId(blockchain: Blockchain): CryptoCurrency.ID { - return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.id}⟩") + return CryptoCurrency.ID.fromValue(value = "coin⟨${blockchain.toNetworkId()}⟩") } } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index d856cd5af9..6a98966d0d 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -18,7 +18,6 @@ internal object MockNetworks { name = "Network One", isTestnet = false, standardType = Network.StandardType.ERC20, - backendId = "network1", currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, @@ -32,7 +31,6 @@ internal object MockNetworks { name = "Network Two", isTestnet = false, standardType = Network.StandardType.ERC20, - backendId = "network1", currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, @@ -46,7 +44,6 @@ internal object MockNetworks { name = "Network Three", isTestnet = false, standardType = Network.StandardType.ERC20, - backendId = "network1", currencySymbol = "ETH", derivationPath = Network.DerivationPath.None, hasFiatFeeRate = true, diff --git a/domain/yield-supply/build.gradle.kts b/domain/yield-supply/build.gradle.kts index ea83aa3225..86c16fe968 100644 --- a/domain/yield-supply/build.gradle.kts +++ b/domain/yield-supply/build.gradle.kts @@ -16,6 +16,7 @@ dependencies { /** Core */ implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.libs.blockchainSdk) /** Domain */ implementation(projects.domain.account.status) diff --git a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt index 35cb09af2c..c5e83841ac 100644 --- a/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt +++ b/domain/yield-supply/src/main/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCase.kt @@ -5,6 +5,7 @@ import arrow.core.Either.Companion.catch import arrow.core.getOrElse import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchainsdk.utils.toNetworkId import com.tangem.domain.account.status.utils.CryptoCurrencyOperations.getCoin import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -67,8 +68,7 @@ class YieldSupplyGetCurrentFeeUseCase( val tokenValue = rateRatio.multiply(nativeGas.amount.value) - val isEthereum = cryptoCurrencyStatus.currency - .network.id.rawId.value == Blockchain.Ethereum.id + val isEthereum = cryptoCurrencyStatus.currency.network.rawId == Blockchain.Ethereum.toNetworkId() val isHighFee = if (isEthereum) { val maxFeePerGas = (feeWithoutGas as? Fee.Ethereum.EIP1559)?.maxFeePerGas ?: 0.toBigInteger() diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt index c26f6ba501..03680be520 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/YieldSupplyMinAmountUseCaseTest.kt @@ -165,8 +165,7 @@ class YieldSupplyMinAmountUseCaseTest { private fun createNetwork(): Network { val derivationPath = Network.DerivationPath.None return Network( - id = Network.ID(Network.RawID("polygon"), derivationPath), - backendId = "polygon", + id = Network.ID(value = "polygon", derivationPath = derivationPath), name = "Polygon", currencySymbol = "MATIC", derivationPath = derivationPath, diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt index d3f2581274..e8f69ad3c5 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyEnterStatusUseCaseTest.kt @@ -7,8 +7,8 @@ import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network import com.tangem.domain.models.network.NetworkAddress import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.models.yield.supply.YieldSupplyStatus +import com.tangem.domain.yield.supply.YieldSupplyRepository import com.tangem.domain.yield.supply.models.YieldSupplyPendingStatus import io.mockk.coEvery import io.mockk.coVerify @@ -324,7 +324,6 @@ class YieldSupplyEnterStatusUseCaseTest { val derivationPath = Network.DerivationPath.None val network = Network( id = Network.ID(value = rawNetworkId, derivationPath = derivationPath), - backendId = rawNetworkId, name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = derivationPath, diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt index f36d85dfaa..39ab76e2a6 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetCurrentFeeUseCaseTest.kt @@ -2,7 +2,7 @@ package com.tangem.domain.yield.supply.usecase import arrow.core.Either import com.google.common.truth.Truth.assertThat -import com.tangem.blockchain.common.Blockchain + import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.supplier.SingleAccountListSupplier @@ -48,7 +48,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { @Test fun `GIVEN valid inputs on non-ethereum WHEN invoke THEN returns fee value and not high`() = runTest { - val rawNetworkId = Blockchain.BSC.id + val rawNetworkId = "binance-smart-chain" val tokenDecimals = 8 val nativeDecimals = 18 val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals) @@ -100,7 +100,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { @Test fun `GIVEN ethereum with high gas WHEN invoke THEN returns high fee flag`() = runTest { - val rawNetworkId = Blockchain.Ethereum.id + val rawNetworkId = "ethereum" val tokenDecimals = 8 val nativeDecimals = 18 val token = createToken(rawNetworkId = rawNetworkId, decimals = tokenDecimals) @@ -152,7 +152,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { @Test fun `GIVEN token fiat rate missing WHEN invoke THEN returns error`() = runTest { - val rawNetworkId = Blockchain.BSC.id + val rawNetworkId = "binance-smart-chain" val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) val cryptoStatus = createStatus(token = token, fiatRate = null) @@ -175,7 +175,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { @Test fun `GIVEN native quotes unavailable WHEN invoke THEN returns error`() = runTest { - val rawNetworkId = Blockchain.BSC.id + val rawNetworkId = "binance-smart-chain" val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) @@ -204,7 +204,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { @Test fun `GIVEN empty quotes list WHEN invoke THEN returns error`() = runTest { - val rawNetworkId = Blockchain.BSC.id + val rawNetworkId = "binance-smart-chain" val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) @@ -233,7 +233,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { @Test fun `GIVEN native fiat rate non-positive WHEN invoke THEN returns error`() = runTest { - val rawNetworkId = Blockchain.BSC.id + val rawNetworkId = "binance-smart-chain" val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal("2.00")) val nativeCoin = createCoin(rawNetworkId = rawNetworkId, decimals = 18) @@ -274,7 +274,7 @@ class YieldSupplyGetCurrentFeeUseCaseTest { @Test fun `GIVEN token fiat rate non-positive WHEN invoke THEN returns error`() = runTest { - val rawNetworkId = Blockchain.BSC.id + val rawNetworkId = "binance-smart-chain" val token = createToken(rawNetworkId = rawNetworkId, decimals = 8) val cryptoStatus = createStatus(token = token, fiatRate = BigDecimal.ZERO) // non-positive @@ -299,7 +299,6 @@ class YieldSupplyGetCurrentFeeUseCaseTest { val derivationPath = Network.DerivationPath.None val network = Network( id = Network.ID(value = rawNetworkId, derivationPath = derivationPath), - backendId = rawNetworkId, name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = derivationPath, @@ -331,7 +330,6 @@ class YieldSupplyGetCurrentFeeUseCaseTest { val derivationPath = Network.DerivationPath.None val network = Network( id = Network.ID(value = rawNetworkId, derivationPath = derivationPath), - backendId = rawNetworkId, name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = derivationPath, diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt index 67c35ae602..d8defa5bea 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetDustMinAmountUseCaseTest.kt @@ -61,8 +61,7 @@ class YieldSupplyGetDustMinAmountUseCaseTest { private fun createNetwork(): Network { val derivationPath = Network.DerivationPath.None return Network( - id = Network.ID(Network.RawID("polygon"), derivationPath), - backendId = "polygon", + id = Network.ID(value = "polygon", derivationPath = derivationPath), name = "Polygon", currencySymbol = "MATIC", derivationPath = derivationPath, diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt index 6d40c26c08..01534f3b92 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyGetRewardsBalanceUseCaseTest.kt @@ -238,8 +238,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { @Test fun `GIVEN polygon USDT0 Loaded status WHEN invoke THEN emit formatted balances`() = runTest { val network = Network( - id = Network.ID(Network.RawID("POLYGON"), Network.DerivationPath.Card("m/44'/60'/0'/0/0")), - backendId = "polygon-pos", + id = Network.ID(value = "polygon-pos", derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/0")), name = "Polygon", currencySymbol = "POL", derivationPath = Network.DerivationPath.Card("m/44'/60'/0'/0/0"), @@ -365,8 +364,7 @@ class YieldSupplyGetRewardsBalanceUseCaseTest { private fun createNetwork(): Network { val derivationPath = Network.DerivationPath.None return Network( - id = Network.ID(Network.RawID("polygon"), derivationPath), - backendId = "polygon", + id = Network.ID(value = "polygon", derivationPath = derivationPath), name = "Polygon", currencySymbol = "MATIC", derivationPath = derivationPath, diff --git a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTrackerTest.kt b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTrackerTest.kt index e1c0349515..81fa8b8a84 100644 --- a/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTrackerTest.kt +++ b/domain/yield-supply/src/test/java/com/tangem/domain/yield/supply/usecase/YieldSupplyPendingTrackerTest.kt @@ -279,7 +279,6 @@ class YieldSupplyPendingTrackerTest { val derivationPath = Network.DerivationPath.None val network = Network( id = Network.ID(value = networkId, derivationPath = derivationPath), - backendId = networkId, name = networkId, currencySymbol = networkId.take(3).uppercase(), derivationPath = derivationPath, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt index ac0bd61222..1a7e88c79a 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewCustomTokenSelectorComponent.kt @@ -57,7 +57,6 @@ internal class PreviewCustomTokenSelectorComponent( CurrencyNetworkUM( network = Network( id = n.id, - backendId = n.id.rawId.value, name = "Network $index", currencySymbol = "N$index", derivationPath = Network.DerivationPath.Card(""), diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt index ed67875a8e..9f7567aee0 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewManageTokensComponent.kt @@ -154,7 +154,6 @@ internal class PreviewManageTokensComponent( CurrencyNetworkUM( network = Network( id = Network.ID(value = networkIndex.toString(), derivationPath = derivationPath), - backendId = networkIndex.toString(), name = "Network $networkIndex", currencySymbol = "N$networkIndex", derivationPath = derivationPath, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt index 7907f421fa..47fbb3f5e5 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/component/preview/PreviewOnboardingManageTokensComponent.kt @@ -95,7 +95,6 @@ internal class PreviewOnboardingManageTokensComponent( CurrencyNetworkUM( network = Network( id = Network.ID(value = networkIndex.toString(), derivationPath = derivationPath), - backendId = networkIndex.toString(), name = "Network $networkIndex", currencySymbol = "N$networkIndex", derivationPath = derivationPath, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt index 731f1757df..8931a13343 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenSelectorModel.kt @@ -3,7 +3,7 @@ package com.tangem.features.managetokens.model import arrow.core.getOrElse import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate -import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.ui.account.toUM import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -199,7 +199,7 @@ internal class CustomTokenSelectorModel @Inject constructor( private fun DerivationPathSelector.checkAccountDerivation(derivationPath: SelectedDerivationPath) = modelScope.launch { val account = derivationPath.id - ?.let { Blockchain.fromId(it.rawId.value) } + ?.toBlockchain() ?.let(::AccountNodeRecognizer) ?.let { recognizer -> val derivationPathValue = derivationPath.value.value diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/ui/FeeExtendedSelectorContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/ui/FeeExtendedSelectorContent.kt index e084555a7b..9e1f4c23df 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/ui/FeeExtendedSelectorContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/extended/ui/FeeExtendedSelectorContent.kt @@ -133,7 +133,6 @@ private fun Preview() { value = "bitcoin", derivationPath = Network.DerivationPath.None, ), - backendId = "bitcoin", name = "Bitcoin", currencySymbol = "BTC", derivationPath = Network.DerivationPath.None, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt index 5280350577..366e500c80 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/speed/ui/FeeSpeedSelectorContent.kt @@ -516,7 +516,6 @@ private val cryptoCurrencyStatus value = "bitcoin", derivationPath = Network.DerivationPath.None, ), - backendId = "bitcoin", name = "Bitcoin", currencySymbol = "BTC", derivationPath = Network.DerivationPath.None, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/token/ui/FeeTokenSelectorContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/token/ui/FeeTokenSelectorContent.kt index 76adedfcaf..7a57435e0e 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/token/ui/FeeTokenSelectorContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/component/token/ui/FeeTokenSelectorContent.kt @@ -171,7 +171,6 @@ private fun Preview() { value = "bitcoin", derivationPath = Network.DerivationPath.None, ), - backendId = "bitcoin", name = "Bitcoin", currencySymbol = "BTC", derivationPath = Network.DerivationPath.None, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt index a74fac4f79..80548cd189 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/feeselector/ui/FeeSelectorBlockContent.kt @@ -258,7 +258,6 @@ private class FeeSelectorUMProvider : PreviewParameterProvider { value = "bitcoin", derivationPath = Network.DerivationPath.None, ), - backendId = "bitcoin", name = "Bitcoin", currencySymbol = "BTC", derivationPath = Network.DerivationPath.None, diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt index 92dc0a0d66..c338f3ca59 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/NFTSendConfirmationNotificationsTransformerV2Test.kt @@ -30,7 +30,7 @@ class NFTSendConfirmationNotificationsTransformerV2Test { private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") private val analyticsCategoryName = "test_category" - val cryptoCurrencyStatus = CryptoCurrencyStatus( + private val cryptoCurrencyStatus = CryptoCurrencyStatus( currency = CryptoCurrency.Coin( id = CryptoCurrency.ID.fromValue("coin⟨BITCOIN⟩bitcoin"), network = Network( @@ -38,7 +38,6 @@ class NFTSendConfirmationNotificationsTransformerV2Test { value = "bitcoin", derivationPath = Network.DerivationPath.None, ), - backendId = "bitcoin", name = "Bitcoin", currencySymbol = "BTC", derivationPath = Network.DerivationPath.None, diff --git a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt index ee1d34d98e..aa166774b7 100644 --- a/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt +++ b/features/send-v2/impl/src/test/java/com/tangem/features/send/v2/send/confirm/model/transformers/SendConfirmationNotificationsTransformerV2Test.kt @@ -32,7 +32,7 @@ class SendConfirmationNotificationsTransformerV2Test { private val appCurrency = AppCurrency(name = "US Dollar", code = "USD", symbol = "$") private val analyticsCategoryName = "test_category" - val cryptoCurrencyStatus = CryptoCurrencyStatus( + private val cryptoCurrencyStatus = CryptoCurrencyStatus( currency = CryptoCurrency.Coin( id = CryptoCurrency.ID.fromValue("coin⟨BITCOIN⟩bitcoin"), network = Network( @@ -40,7 +40,6 @@ class SendConfirmationNotificationsTransformerV2Test { value = "bitcoin", derivationPath = Network.DerivationPath.None, ), - backendId = "bitcoin", name = "Bitcoin", currencySymbol = "BTC", derivationPath = Network.DerivationPath.None, diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt index f15aca16d8..26beb5a846 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/ui/preview/SwapAmountContentPreview.kt @@ -33,7 +33,6 @@ internal data object SwapAmountContentPreview { value = "bitcoin", derivationPath = Network.DerivationPath.None, ), - backendId = "bitcoin", name = "Bitcoin", currencySymbol = "BTC", derivationPath = Network.DerivationPath.None, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt index 427406a32b..8e92b4c203 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/express/ExpressStatusBottomSheetStateProvider.kt @@ -104,7 +104,6 @@ class ExpressStatusBottomSheetStateProvider : PreviewParameterProvider Network.StandardType.ERC20 else -> Network.StandardType.Unspecified("UNSPEC") }, diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt index de88d92e2a..a2e0fbda2f 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/deeplink/DefaultPromoDeeplinkHandlerTest.kt @@ -834,10 +834,9 @@ class DefaultPromoDeeplinkHandlerTest { address: String, derivationPath: Network.DerivationPath = Network.DerivationPath.None, ): NetworkStatus { - val networkId = Network.ID(Network.RawID(rawNetworkId), derivationPath) + val networkId = Network.ID(value = rawNetworkId, derivationPath = derivationPath) val network = Network( id = networkId, - backendId = rawNetworkId, name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = derivationPath, @@ -866,10 +865,9 @@ class DefaultPromoDeeplinkHandlerTest { } private fun buildUnreachableNetworkStatus(rawNetworkId: String): NetworkStatus { - val networkId = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None) + val networkId = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None) val network = Network( id = networkId, - backendId = rawNetworkId, name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = Network.DerivationPath.None, @@ -890,10 +888,9 @@ class DefaultPromoDeeplinkHandlerTest { rawNetworkId: String, derivationPath: Network.DerivationPath = Network.DerivationPath.None, ): CryptoCurrency.Coin { - val networkId = Network.ID(Network.RawID(rawNetworkId), derivationPath) + val networkId = Network.ID(value = rawNetworkId, derivationPath = derivationPath) val network = Network( id = networkId, - backendId = rawNetworkId, name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = derivationPath, diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/qr/QrContentClassifierTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/qr/QrContentClassifierTest.kt index 5b6afef517..9f687cf8d5 100644 --- a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/qr/QrContentClassifierTest.kt +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/presentation/wallet/qr/QrContentClassifierTest.kt @@ -285,8 +285,7 @@ internal class QrContentClassifierTest { private fun buildNetwork(rawNetworkId: String): Network { return Network( - id = Network.ID(Network.RawID(rawNetworkId), Network.DerivationPath.None), - backendId = rawNetworkId, + id = Network.ID(value = rawNetworkId, derivationPath = Network.DerivationPath.None), name = rawNetworkId, currencySymbol = rawNetworkId.take(3).uppercase(), derivationPath = Network.DerivationPath.None, diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/NetworkExt.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/NetworkExt.kt index 203d2fddbd..e78329bd4a 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/NetworkExt.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/utils/NetworkExt.kt @@ -1,6 +1,7 @@ package com.tangem.blockchainsdk.utils import com.tangem.blockchain.common.Blockchain +import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network /** Converts [Network] to [Blockchain] */ @@ -10,4 +11,8 @@ fun Network.toBlockchain(): Blockchain = id.toBlockchain() fun Network.ID.toBlockchain(): Blockchain = rawId.toBlockchain() /** Converts [Network.RawID] to [Blockchain] */ -fun Network.RawID.toBlockchain(): Blockchain = Blockchain.fromId(id = value) \ No newline at end of file +fun Network.RawID.toBlockchain(): Blockchain = value.toBlockchain() + +fun CryptoCurrency.ID.toBlockchain(): Blockchain = rawNetworkId.toBlockchain() + +private fun String.toBlockchain(): Blockchain = Blockchain.fromNetworkId(this) ?: Blockchain.Unknown \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index b3108e4f2e..cf3897b510 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 @@ -28,9 +28,9 @@ object BlockchainUtils { /** Decodes XRP Blockchain address */ fun decodeRippleXAddress(xAddress: String, blockchainId: String): XrpTaggedAddress? { - return if (blockchainId == Blockchain.XRP.id && xAddress.firstOrNull() == XRP_X_ADDRESS) { + return if (blockchainId.toBlockchain() == Blockchain.XRP && xAddress.firstOrNull() == XRP_X_ADDRESS) { val decodedAddress = XrpAddressService.decodeXAddress(xAddress) - return decodedAddress?.let(XrpTaggedAddressConverter()::convert) + decodedAddress?.let(XrpTaggedAddressConverter()::convert) } else { null } @@ -38,46 +38,46 @@ object BlockchainUtils { /** If current [networkId] is Bitcoin */ fun isBitcoin(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet } /** If current [networkId] is use custom fee */ fun isUseBitcoinFeeConverter(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return isBitcoin(blockchainId) || blockchain == Blockchain.Fact0rn } /** If current [blockchainId] is Tezos */ fun isTezos(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Tezos } fun isCardano(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Cardano } /** If current [blockchainId] is BeaconChain */ fun isBeaconChain(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet } /** If current [blockchainId] is Polygon */ fun isPolygonChain(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Polygon || blockchain == Blockchain.PolygonTestnet } fun isTron(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet } fun isTon(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet } @@ -89,7 +89,7 @@ object BlockchainUtils { coinId: String? = null, contractAddress: String? = null, ): Boolean { - val blockchain = Blockchain.fromNetworkId(blockchainId) ?: return false + val blockchain = blockchainId.toBlockchain() ?: return false if (blockchain in excludedBlockchains) return false if (hasOnlyHotWallets && blockchain in hotExcludedBlockchains) return false @@ -103,37 +103,37 @@ object BlockchainUtils { } fun isArbitrum(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Arbitrum } fun isSolana(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Solana } fun isPolkadot(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Polkadot || blockchain == Blockchain.PolkadotTestnet } fun isCosmos(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Cosmos || blockchain == Blockchain.CosmosTestnet } fun isBSC(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.BSC || blockchain == Blockchain.BSCTestnet } fun isEthereum(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain == Blockchain.Ethereum || blockchain == Blockchain.EthereumTestnet } fun isClore(blockchainId: String): Boolean { - return Blockchain.fromId(blockchainId) == Blockchain.Clore + return blockchainId.toBlockchain() == Blockchain.Clore } data class BlockchainInfo( @@ -163,7 +163,7 @@ object BlockchainUtils { * Blockchains not affecting total balance counting on errors */ fun isIncludeToBalanceOnError(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return when (blockchain) { Blockchain.Binance, Blockchain.BinanceTestnet -> true else -> false @@ -171,7 +171,7 @@ object BlockchainUtils { } fun isIncludeStakingTotalBalance(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return blockchain != Blockchain.Cardano } @@ -184,9 +184,9 @@ object BlockchainUtils { /** Checks if the blockchain uses case-insensitive contract addresses */ fun isCaseInsensitiveContractAddress(blockchainId: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() - return blockchain.isEvm() + return blockchain?.isEvm() == true } private fun getNetworkStandardName(blockchain: Blockchain): String { @@ -223,8 +223,10 @@ object BlockchainUtils { * Checks if the given coin is Tether on Ethereum network, which may require special handling in some cases. */ fun isTetherInEthereum(blockchainId: String, contractAddress: String): Boolean { - val blockchain = Blockchain.fromId(blockchainId) + val blockchain = blockchainId.toBlockchain() return (blockchain == Blockchain.Ethereum || blockchain == Blockchain.EthereumTestnet) && contractAddress.equals(TETHER_CONTRACT_ADDRESS, ignoreCase = true) } + + private fun String.toBlockchain(): Blockchain? = Blockchain.fromNetworkId(this) } \ No newline at end of file From 799a38655e8df72e482ae08faae5ab373fd504a7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2026 11:10:57 +0300 Subject: [PATCH 14/32] Updated on 2026-08-14 --- .../src/main/res/values-pt-rBR/strings.xml | 23 ++ core/res/src/main/res/values/strings.xml | 19 + .../DefaultConsolidationRepository.kt | 22 +- .../model/DynamicAddressesDelegate.kt | 300 +++++++++++++-- .../tokendetails/model/TokenDetailsModel.kt | 24 +- .../DynamicAddressesBottomSheet.kt | 18 +- .../DynamicAddressesBottomSheetConfig.kt | 28 +- .../DynamicAddressesBottomSheetContent.kt | 364 +++++++++++++++++- gradle/tangem_dependencies.toml | 2 +- 9 files changed, 733 insertions(+), 67 deletions(-) diff --git a/core/res/src/main/res/values-pt-rBR/strings.xml b/core/res/src/main/res/values-pt-rBR/strings.xml index e931a23bdc..e011607a12 100644 --- a/core/res/src/main/res/values-pt-rBR/strings.xml +++ b/core/res/src/main/res/values-pt-rBR/strings.xml @@ -80,6 +80,10 @@ Adicionar tokens Escolha o token que deseja receber. Escolha o token que deseja trocar. + Adicione ao seu portfólio + Adicionar tokens + Classificação e agrupamento + Organizar tokens Escolha a rede Adicionar token personalizado Gerenciar tokens @@ -584,6 +588,8 @@ Escolha qual token usar para pagar a taxa de rede. %s Escolha o token Mercado e Notícias + Mostrar tudo + Mostrar menos Em alta agora As informações a seguir são opcionais. Você pode apagá-las se não quiser compartilhá-las. Diga-nos quais funções estão faltando e tentaremos ajudá-lo. @@ -719,6 +725,7 @@ Limite de mana A rede Koinos exige Mana para o pagamento das taxas de rede. Você tem %1$s/%2$s Mana Nível de mana + Adicionar e gerenciar Para começar a rastrear seus criptoativos e transações, adicione tokens. Gerenciar tokens Leia o código QR para enviar fundos ou conectar-se a um aplicativo @@ -1534,6 +1541,7 @@ Negociação em grande escala Teremos todo o prazer em receber seu feedback. O Tangem Pay agora está em versão beta. + Não foi possível renomear o cartão. Cartão bloqueado Pagamento com cartão Depósito @@ -1567,7 +1575,9 @@ Seu cartão foi desbloqueado. Retirada Não é possível usar em dispositivos com root. + Saldo disponível Ocultar KYC da tela principal + Cartão Tangem Pay 1 Adicionar fundos Opções de recarga Adicionar ao Google Wallet @@ -1600,10 +1610,13 @@ Compartilhe seu endereço ou mostre o código QR. Problemas técnicos detectados. Tente novamente mais tarde ou entre em contato com o suporte. Receber indisponível agora + Somente letras e números são permitidos. + Caracteres inválidos Revelar Mostrar detalhes Troque qualquer ativo da sua carteira por um cartão. Detalhes do cartão + Por favor, tente novamente mais tarde. Descongelar cartão Volte ao aplicativo se você se esquecer. Seu código PIN @@ -1611,12 +1624,16 @@ Retirada indisponível agora Você não pode iniciar uma troca ou um novo saque até que o atual seja concluído. Retirada em andamento + Configurações do cartão Alterar código PIN Volte ao aplicativo se você se esquecer. + Cartão digital Entendo que perderei completamente o acesso ao meu cartão Tangem Pay e a todos os fundos nele contidos, sem possibilidade de recuperação. Falha na emissão do cartão Ocorreu um erro técnico. Tente novamente clicando no botão abaixo. Ocorreu um erro técnico. Por favor, entre em contato com o suporte. + O recurso estará disponível em breve. + Você poderá emitir cartões adicionais para sua conta de pagamento. Obtenha seu cartão virtual Visa Tangem grátis. Obtenha o Tangem Pay Acesse o Suporte @@ -1648,6 +1665,7 @@ Uma conta de pagamento separada será criada sem divulgar seus endereços e bens. Privacidade incomparável Obtenha seu cartão Tangem Pay gratuito em minutos. + Suporte de pagamento Conta de pagamento A conta de pagamento não está sincronizada. PIN inválido: evite sequências ou repetições. @@ -1660,6 +1678,7 @@ Use USDC para pagamentos do dia a dia. O serviço Tangem Pay está temporariamente inacessível. Tangem Pay + USDC na rede Polygon Clique no botão abaixo para restaurar o acesso. O saldo do seu Polygon em USDC na blockchain é diferente do saldo do seu cartão e é atualizado em até 2 dias úteis após uma compra. Os fundos de compras reembolsadas não serão devolvidos ao seu saldo na blockchain nem estarão disponíveis para saque, mas permanecerão no saldo do seu cartão para compras futuras. Observe @@ -1690,6 +1709,10 @@ Serviço de staking %1$s token em %%imagem%% %2$s rede Token em %%imagem%% %1$s rede + %s rede + %1$s em %2$s rede + %1$s em %%imagem%% %2$s + %1$s em %2$s %%imagem%% O %1$s (%2$sO token ) é a principal moeda da plataforma. %3$s rede e não pode ser ocultada enquanto você tiver outros tokens dessa rede na lista Não foi possível ocultar %s N/A diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index dd706dc203..1ec9514666 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -80,6 +80,10 @@ Add tokens Choose the token you want to receive Choose the token you want to swap + Add to your portfolio + Add tokens + Sorting and grouping + Organize tokens Choose network Add custom token Manage tokens @@ -477,6 +481,11 @@ Send funds using only Dynamic addresses Dynamic Addresses is enabled. Custom \"change\" and \"index\" are unavailable. + You are turning off Dynamic Addresses. Funds will now be received on your fixed address. + A network fee applies to consolidate funds into the fixed address. + Disable Dynamic Addresses + Disable dynamic addresses + Dynamic addresses disabled Dynamic addresses enabled Use a new address for each transaction to reduce traceability and improve on-chain privacy. Enhanced Privacy @@ -584,6 +593,8 @@ Choose which token to use to pay\nthe network fee. %s Choose token Market & News + Show All + Show less Tangem AI Trending Now The following information is optional. You can erase it if you don\'t want to share it. @@ -720,6 +731,7 @@ Mana limit The Koinos network requires Mana for network fees. You have %1$s/%2$s Mana Mana level + Add & Manage To begin tracking your crypto assets and transactions, add tokens Manage tokens Scan QR code to send funds or connect to an app @@ -1569,7 +1581,9 @@ Your card is unfrozen. Withdrawal Unable to use on rooted device + Available balance Hide KYC from main screen + Tangem Pay Card 1 Add funds Top-up options Add to Google Wallet @@ -1619,10 +1633,13 @@ Card settings Change PIN-code Come back to the app if you forget it. + Digital card I understand that I will completely lose access to my Tangem Pay Card and all funds on it without the possibility of recovery Failed to issue card A technical error has occurred, please try again by clicking the button below. A technical error has occurred, please contact support. + Feature will be availible soon + You will be available to issue additional cards for your payment account Get your free Tangem Visa virtual card Get Tangem Pay Go to Support @@ -1654,6 +1671,7 @@ A separate payment account will be created without disclosing your addresses and assets Unrivaled privacy Get your free Tangem Pay Card in minutes + Pay Support Payment account Payment account is not synced Invalid PIN: avoid sequences or repeats @@ -1666,6 +1684,7 @@ Use USDC for everyday payments Tangem Pay is temporarily unreachable Tangem Pay + USDC on Polygon network Click the button below to restore access Your on-chain USDC Polygon balance differs from your card balance and updates within 2 business days after a purchase. Funds from refunded purchases won’t be returned your on-chain balance or be available for withdrawal, but will stay on your card balance for purchases. Please note diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultConsolidationRepository.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultConsolidationRepository.kt index 0860a7b9ce..c1b88d69d2 100644 --- a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultConsolidationRepository.kt +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultConsolidationRepository.kt @@ -1,10 +1,12 @@ package com.tangem.data.dynamicaddresses import arrow.core.Either +import com.tangem.blockchain.common.AmountType import com.tangem.blockchain.common.DynamicAddressesManager import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.WalletManager import com.tangem.blockchain.common.transaction.Fee +import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.extensions.Result import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository import com.tangem.domain.models.network.Network @@ -33,13 +35,25 @@ internal class DefaultConsolidationRepository( when (val result = dynamicAddressesManager.createConsolidationTransaction(fee)) { is Result.Success -> result.data - is Result.Failure -> error("Failed to create consolidation tx: ${result.error}") + is Result.Failure -> throw result.error } } } - @Suppress("UnusedParameter") - private fun getNormalFee(walletManager: WalletManager): Fee { - TODO("Fee calculation for consolidation from multiple addresses is not yet implemented") + private suspend fun getNormalFee(walletManager: WalletManager): Fee { + val coinAmount = walletManager.wallet.amounts[AmountType.Coin] + ?: error("Coin amount not found") + val feeResult = walletManager.getFee( + amount = coinAmount, + destination = walletManager.wallet.address, + ) + + return when (feeResult) { + is Result.Success -> when (val txFee = feeResult.data) { + is TransactionFee.Single -> txFee.normal + is TransactionFee.Choosable -> txFee.normal + } + is Result.Failure -> throw feeResult.error + } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt index e9fe02cce1..a6a548aa4e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt @@ -1,42 +1,65 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.model import com.tangem.common.core.TangemSdkError +import com.tangem.core.decompose.di.GlobalUiMessageSender import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.res.R import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.common.ui.amountScreen.utils.getFiatString +import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase +import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesError import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase +import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.Provider +import com.tangem.domain.transaction.error.SendTransactionError +import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheetConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.logging.TangemLogger +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch @Suppress("LongParameterList") -internal class DynamicAddressesDelegate( +internal class DynamicAddressesDelegate @AssistedInject constructor( private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase, + private val disableDynamicAddressesUseCase: DisableDynamicAddressesUseCase, + private val createConsolidationTransactionUseCase: CreateConsolidationTransactionUseCase, + private val getFeeUseCase: GetFeeUseCase, + private val sendTransactionUseCase: SendTransactionUseCase, private val isXpubDerivedUseCase: IsXpubDerivedUseCase, private val dynamicAddressesRepository: DynamicAddressesRepository, private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase, - private val uiMessageSender: UiMessageSender, - private val userWalletId: UserWalletId, - private val network: Network, - private val coroutineScope: CoroutineScope, + @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val dispatchers: CoroutineDispatcherProvider, - private val showBottomSheet: () -> Unit, - private val dismissBottomSheet: () -> Unit, - private val onDynamicAddressesEnabled: () -> Unit, + @Assisted private val userWallet: UserWallet, + @Assisted private val cryptoCurrencyStatusProvider: Provider, + @Assisted private val appCurrencyProvider: Provider, + @Assisted private val coroutineScope: CoroutineScope, + @Assisted("showBottomSheet") private val showBottomSheet: () -> Unit, + @Assisted("dismissBottomSheet") private val dismissBottomSheet: () -> Unit, + @Assisted("onDynamicAddressesStateChanged") private val onDynamicAddressesStateChanged: () -> Unit, ) { + private val userWalletId get() = userWallet.walletId + private val _bottomSheetConfig = MutableStateFlow( DynamicAddressesBottomSheetConfig.Enable( isCardScanRequired = false, @@ -45,27 +68,45 @@ internal class DynamicAddressesDelegate( ) val bottomSheetConfig: StateFlow = _bottomSheetConfig.asStateFlow() - fun onDynamicAddressesClick() { - coroutineScope.launch(dispatchers.main) { - val hasConflicts = dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) - if (hasConflicts) { - _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Unavailable( - onGotItClick = dismissBottomSheet, - ) - showBottomSheet() - return@launch - } + // region Entry point - val isCardScanRequired = !isXpubAlreadyDerived() - _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable( - isCardScanRequired = isCardScanRequired, - onEnableClick = ::onEnableClick, - ) - showBottomSheet() + fun onDynamicAddressesClick() { + val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return + coroutineScope.launch(dispatchers.main) { + val status = dynamicAddressesRepository.getStatus(userWalletId, network).first() + when (status) { + DynamicAddressesStatus.ENABLED, + DynamicAddressesStatus.ENABLED_REQUIRES_SETUP, + -> onDisableFlow(network) + DynamicAddressesStatus.DISABLED -> onEnableFlow(network) + } } } + // endregion + + // region Enable flow + + private suspend fun onEnableFlow(network: Network) { + val hasConflicts = dynamicAddressesRepository.hasConflictingCustomTokens(userWalletId, network) + if (hasConflicts) { + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ConflictingCustomTokens( + onDismissClick = dismissBottomSheet, + ) + showBottomSheet() + return + } + + val isCardScanRequired = !isXpubAlreadyDerived(network) + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable( + isCardScanRequired = isCardScanRequired, + onEnableClick = ::onEnableClick, + ) + showBottomSheet() + } + private fun onEnableClick() { + val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return coroutineScope.launch(dispatchers.main) { _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Enable( isCardScanRequired = false, @@ -80,7 +121,7 @@ internal class DynamicAddressesDelegate( } else { TangemLogger.e("Failed to get XPUB: ${error.message}") _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( - onGotItClick = dismissBottomSheet, + onDismissClick = dismissBottomSheet, ) } return@launch @@ -92,21 +133,21 @@ internal class DynamicAddressesDelegate( ifLeft = { error -> when (error) { is EnableDynamicAddressesError.ConflictingCustomTokens -> { - _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.Unavailable( - onGotItClick = dismissBottomSheet, + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ConflictingCustomTokens( + onDismissClick = dismissBottomSheet, ) } is EnableDynamicAddressesError.ServiceError -> { TangemLogger.e("Failed to enable DA: ${error.cause.message}") _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( - onGotItClick = dismissBottomSheet, + onDismissClick = dismissBottomSheet, ) } } }, ifRight = { dismissBottomSheet() - onDynamicAddressesEnabled() + onDynamicAddressesStateChanged() uiMessageSender.send( SnackbarMessage(message = resourceReference(R.string.dynamic_addresses_enabled_toast_title)), ) @@ -115,11 +156,208 @@ internal class DynamicAddressesDelegate( } } - private suspend fun isXpubAlreadyDerived(): Boolean { + // endregion + + // region Disable flow + + private fun onDisableFlow(network: Network) { + coroutineScope.launch(dispatchers.main) { + disableDynamicAddressesUseCase(userWalletId, network).fold( + ifLeft = { error -> + TangemLogger.e("Failed to check disable: ${error.message}") + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( + onDismissClick = dismissBottomSheet, + ) + showBottomSheet() + }, + ifRight = { isConsolidationRequired -> + if (!isConsolidationRequired) { + showSimpleDisableSheet() + } else { + showDisableSheetAndLoadFee() + } + }, + ) + } + } + + private fun showSimpleDisableSheet() { + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation( + onDisableClick = ::onSimpleDisableClick, + onReadMoreClick = ::onReadMoreClick, + ) + showBottomSheet() + } + + private fun onSimpleDisableClick() { + val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return + coroutineScope.launch(dispatchers.main) { + runSuspendCatching { dynamicAddressesRepository.disable(userWalletId, network) } + .onSuccess { + dismissBottomSheet() + onDynamicAddressesStateChanged() + uiMessageSender.send( + SnackbarMessage(message = resourceReference(R.string.dynamic_addresses_disabled_popup_title)), + ) + } + .onFailure { e -> + TangemLogger.e("Failed to disable DA: ${e.message}") + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( + onDismissClick = dismissBottomSheet, + ) + } + } + } + + private fun showDisableSheetAndLoadFee() { + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.DisableWithConsolidation( + feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Loading, + onDisableClick = ::onDisableClick, + onRefreshFee = ::loadDisableFee, + onReadMoreClick = ::onReadMoreClick, + ) + showBottomSheet() + loadDisableFee() + } + + private fun loadDisableFee() { + coroutineScope.launch(dispatchers.main) { + _bottomSheetConfig.value = disableWithConsolidationConfig().copy( + feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Loading, + ) + + val status = cryptoCurrencyStatusProvider() + val currency = status?.currency + val balance = status?.value?.amount + val address = status?.value?.networkAddress?.defaultAddress?.value + + if (currency == null || balance == null || address == null) { + _bottomSheetConfig.value = disableWithConsolidationConfig().copy( + feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Error, + ) + return@launch + } + + getFeeUseCase( + amount = balance, + destination = address, + userWallet = userWallet, + cryptoCurrency = currency, + ).fold( + ifLeft = { + _bottomSheetConfig.value = disableWithConsolidationConfig().copy( + feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Error, + ) + }, + ifRight = { txFee -> + val fee = txFee.normal + val fiatFormatted = getFiatString( + value = fee.amount.value, + rate = status.value.fiatRate, + appCurrency = appCurrencyProvider(), + approximate = true, + ) + _bottomSheetConfig.value = disableWithConsolidationConfig().copy( + feeState = DynamicAddressesBottomSheetConfig.DisableFeeState.Content( + feeSymbol = currency.symbol, + fiatFormatted = fiatFormatted, + ), + ) + }, + ) + } + } + + private fun disableWithConsolidationConfig(): DynamicAddressesBottomSheetConfig.DisableWithConsolidation { + return _bottomSheetConfig.value as? DynamicAddressesBottomSheetConfig.DisableWithConsolidation + ?: DynamicAddressesBottomSheetConfig.DisableWithConsolidation( + onDisableClick = ::onDisableClick, + onRefreshFee = ::loadDisableFee, + onReadMoreClick = ::onReadMoreClick, + ) + } + + // TODO: Replace with actual DA documentation URL + private fun onReadMoreClick() { + // Stub: open documentation about Dynamic Addresses consolidation + } + + private fun onDisableClick() { + val network = cryptoCurrencyStatusProvider()?.currency?.network ?: return + coroutineScope.launch(dispatchers.main) { + _bottomSheetConfig.value = disableWithConsolidationConfig().copy( + isSending = true, + ) + + val txData = createConsolidationTransactionUseCase(userWalletId, network).fold( + ifLeft = { error -> + TangemLogger.e("Failed to create consolidation tx: ${error.message}") + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( + onDismissClick = dismissBottomSheet, + ) + return@launch + }, + ifRight = { it }, + ) + + sendTransactionUseCase( + txData = txData, + userWallet = userWallet, + network = network, + ).fold( + ifLeft = { error -> + if (error is SendTransactionError.UserCancelledError) { + dismissBottomSheet() + } else { + TangemLogger.e("Failed to send consolidation tx: $error") + _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( + onDismissClick = dismissBottomSheet, + ) + } + }, + ifRight = { + try { + dynamicAddressesRepository.disable(userWalletId, network) + } catch (e: Exception) { + TangemLogger.e("Failed to disable DA after consolidation: ${e.message}") + } + dismissBottomSheet() + onDynamicAddressesStateChanged() + uiMessageSender.send( + SnackbarMessage( + message = resourceReference(R.string.dynamic_addresses_disabled_popup_title), + ), + ) + }, + ) + } + } + + // endregion + + // region Common + + private suspend fun isXpubAlreadyDerived(network: Network): Boolean { return isXpubDerivedUseCase(userWalletId, network) } private fun isUserCancellation(error: Throwable): Boolean { return error is TangemSdkError.UserCancelled || error.cause is TangemSdkError.UserCancelled } + + // endregion + + @AssistedFactory + interface Factory { + @Suppress("LongParameterList") + fun create( + userWallet: UserWallet, + cryptoCurrencyStatusProvider: Provider, + appCurrencyProvider: Provider, + coroutineScope: CoroutineScope, + @Assisted("showBottomSheet") showBottomSheet: () -> Unit, + @Assisted("dismissBottomSheet") dismissBottomSheet: () -> Unit, + @Assisted("onDynamicAddressesStateChanged") onDynamicAddressesStateChanged: () -> Unit, + ): DynamicAddressesDelegate + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index f5def6e3a1..bb946cf2a8 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 @@ -11,10 +11,7 @@ import com.tangem.blockchain.common.address.AddressType import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles import com.tangem.domain.dynamicaddresses.DynamicAddressesSupportedBlockchains -import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase -import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase -import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.common.routing.AppRoute import com.tangem.common.routing.AppRouter import com.tangem.common.ui.bottomsheet.receive.AddressModel @@ -170,11 +167,9 @@ internal class TokenDetailsModel @Inject constructor( private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val yieldSupplyGetRewardsBalanceUseCase: YieldSupplyGetRewardsBalanceUseCase, private val signCloreMessageUseCase: SignCloreMessageUseCase, - private val enableDynamicAddressesUseCase: EnableDynamicAddressesUseCase, private val isXpubSupportedUseCase: IsXpubSupportedUseCase, - private val isXpubDerivedUseCase: IsXpubDerivedUseCase, - private val dynamicAddressesRepository: DynamicAddressesRepository, private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, + private val dynamicAddressesDelegateFactory: DynamicAddressesDelegate.Factory, private val dialogFactory: TokenDetailsDialogFactory, private val userWalletsListRepository: UserWalletsListRepository, private val singleAccountListSupplier: SingleAccountListSupplier, @@ -246,21 +241,16 @@ internal class TokenDetailsModel @Inject constructor( // region Dynamic Addresses val dynamicAddressesDelegate by lazy(mode = LazyThreadSafetyMode.NONE) { - DynamicAddressesDelegate( - enableDynamicAddressesUseCase = enableDynamicAddressesUseCase, - isXpubDerivedUseCase = isXpubDerivedUseCase, - dynamicAddressesRepository = dynamicAddressesRepository, - getExtendedPublicKeyUseCase = getExtendedPublicKeyForCurrencyUseCase, - uiMessageSender = uiMessageSender, - userWalletId = userWalletId, - network = cryptoCurrency.network, + dynamicAddressesDelegateFactory.create( + userWallet = userWallet, + cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus }, + appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, coroutineScope = modelScope, - dispatchers = dispatchers, showBottomSheet = { bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.DynamicAddresses) }, dismissBottomSheet = bottomSheetNavigation::dismiss, - onDynamicAddressesEnabled = ::onDynamicAddressesEnabled, + onDynamicAddressesStateChanged = ::onDynamicAddressesStateChanged, ) } // endregion Dynamic Addresses @@ -707,7 +697,7 @@ internal class TokenDetailsModel @Inject constructor( override fun onDynamicAddressesClick() = dynamicAddressesDelegate.onDynamicAddressesClick() - private fun onDynamicAddressesEnabled() { + private fun onDynamicAddressesStateChanged() { updateTopBarMenu() modelScope.launch(dispatchers.main) { cryptoCurrencyBalanceFetcher.invokeAndAwait(userWalletId = userWalletId, currency = cryptoCurrency) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheet.kt index a12557f768..556fd79f30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheet.kt @@ -4,12 +4,14 @@ import androidx.compose.runtime.Composable import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheet import com.tangem.core.ui.components.bottomsheets.modal.TangemModalBottomSheetTitle +import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.R as CoreR @Composable internal fun DynamicAddressesBottomSheet(config: TangemBottomSheetConfig) { TangemModalBottomSheet( config = config, + containerColor = TangemTheme.colors.background.tertiary, title = { TangemModalBottomSheetTitle( endIconRes = CoreR.drawable.ic_close_24, @@ -19,10 +21,18 @@ internal fun DynamicAddressesBottomSheet(config: TangemBottomSheetConfig) { ) { content -> when (content) { is DynamicAddressesBottomSheetConfig.Enable -> DynamicAddressesEnableContent(content = content) - is DynamicAddressesBottomSheetConfig.Unavailable -> DynamicAddressesUnavailableContent(content = content) - is DynamicAddressesBottomSheetConfig.ServiceUnavailable -> DynamicAddressesServiceUnavailableContent( - content = content, - ) + is DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation -> { + DynamicAddressesDisableWithoutConsolidationContent(content = content) + } + is DynamicAddressesBottomSheetConfig.DisableWithConsolidation -> { + DynamicAddressesDisableWithConsolidationContent(content = content) + } + is DynamicAddressesBottomSheetConfig.ConflictingCustomTokens -> { + DynamicAddressesConflictingCustomTokensContent(content = content) + } + is DynamicAddressesBottomSheetConfig.ServiceUnavailable -> { + DynamicAddressesServiceUnavailableContent(content = content) + } } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetConfig.kt index a228fb29b2..7270a74d1e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetConfig.kt @@ -12,11 +12,33 @@ internal sealed class DynamicAddressesBottomSheetConfig : TangemBottomSheetConfi val onEnableClick: () -> Unit, ) : DynamicAddressesBottomSheetConfig() - data class Unavailable( - val onGotItClick: () -> Unit, + data class DisableWithoutConsolidation( + val onDisableClick: () -> Unit, + val onReadMoreClick: () -> Unit, + ) : DynamicAddressesBottomSheetConfig() + + data class DisableWithConsolidation( + val feeState: DisableFeeState = DisableFeeState.Loading, + val isSending: Boolean = false, + val onDisableClick: () -> Unit, + val onRefreshFee: () -> Unit, + val onReadMoreClick: () -> Unit, + ) : DynamicAddressesBottomSheetConfig() + + sealed interface DisableFeeState { + data object Loading : DisableFeeState + data class Content( + val feeSymbol: String, + val fiatFormatted: String, + ) : DisableFeeState + data object Error : DisableFeeState + } + + data class ConflictingCustomTokens( + val onDismissClick: () -> Unit, ) : DynamicAddressesBottomSheetConfig() data class ServiceUnavailable( - val onGotItClick: () -> Unit, + val onDismissClick: () -> Unit, ) : DynamicAddressesBottomSheetConfig() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetContent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetContent.kt index fb1eaa7605..1b40922d1d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetContent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/dynamicaddresses/DynamicAddressesBottomSheetContent.kt @@ -1,5 +1,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses +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.Row @@ -14,12 +16,28 @@ 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.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.PrimaryButtonIconEnd +import com.tangem.core.ui.components.TextShimmer +import com.tangem.core.ui.components.audits.AuditLabel +import com.tangem.core.ui.components.audits.AuditLabelUM +import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationConfig +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.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.res.R import com.tangem.core.ui.R as CoreR +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.dynamicaddresses.DynamicAddressesBottomSheetConfig.DisableFeeState @Composable internal fun DynamicAddressesEnableContent(content: DynamicAddressesBottomSheetConfig.Enable) { @@ -72,13 +90,13 @@ internal fun DynamicAddressesEnableContent(content: DynamicAddressesBottomSheetC Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24)) - PrimaryButton( + PrimaryButtonIconEnd( text = stringResourceSafe(id = R.string.dynamic_addresses_enter_main_button_title), + iconResId = if (content.isCardScanRequired) CoreR.drawable.ic_tangem_24 else null, onClick = content.onEnableClick, modifier = Modifier.fillMaxWidth(), showProgress = content.isLoading, enabled = !content.isLoading, - // TODO add card icon when isCardScanRequired ) Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) @@ -86,12 +104,205 @@ internal fun DynamicAddressesEnableContent(content: DynamicAddressesBottomSheetC } @Composable -internal fun DynamicAddressesUnavailableContent(content: DynamicAddressesBottomSheetConfig.Unavailable) { +internal fun DynamicAddressesDisableWithoutConsolidationContent( + content: DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + DisableHeader() + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24)) + + PrimaryButtonIconEnd( + text = stringResourceSafe(id = R.string.dynamic_addresses_disable_main_button_title), + iconResId = null, + onClick = content.onDisableClick, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) + } +} + +@Composable +internal fun DynamicAddressesDisableWithConsolidationContent( + content: DynamicAddressesBottomSheetConfig.DisableWithConsolidation, +) { + val isConfirmEnabled = content.feeState is DisableFeeState.Content && !content.isSending + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing16), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + DisableHeader() + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) + + DisableFeeBlock( + feeState = content.feeState, + onReadMoreClick = content.onReadMoreClick, + ) + + if (content.feeState is DisableFeeState.Error) { + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12)) + + Notification( + config = NotificationConfig( + title = resourceReference(R.string.send_fee_unreachable_error_title), + subtitle = resourceReference(R.string.send_fee_unreachable_error_text), + iconResId = CoreR.drawable.ic_alert_24, + iconTint = NotificationConfig.IconTint.Warning, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(R.string.warning_button_refresh), + onClick = content.onRefreshFee, + ), + ), + containerColor = TangemTheme.colors.background.action, + ) + } + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing24)) + + PrimaryButtonIconEnd( + text = stringResourceSafe(id = R.string.dynamic_addresses_disable_main_button_title), + iconResId = CoreR.drawable.ic_tangem_24, + onClick = content.onDisableClick, + modifier = Modifier.fillMaxWidth(), + showProgress = content.isSending, + enabled = isConfirmEnabled, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing16)) + } +} + +@Composable +private fun DisableHeader() { + Icon( + painter = painterResource(id = CoreR.drawable.ic_dynamic_addresses_bottomsheet_enable_unavailable), + contentDescription = null, + modifier = Modifier.size(TangemTheme.dimens.size44), + tint = TangemTheme.colors.icon.attention, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12)) + + Text( + text = stringResourceSafe(id = R.string.dynamic_addresses_disable_title), + style = TangemTheme.typography.h3, + color = TangemTheme.colors.text.primary1, + textAlign = TextAlign.Center, + ) + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8)) + + Text( + text = stringResourceSafe(id = R.string.dynamic_addresses_disable_description), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, + ) +} + +@Composable +private fun DisableFeeBlock(feeState: DisableFeeState, onReadMoreClick: () -> Unit) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = TangemTheme.colors.background.action, + shape = TangemTheme.shapes.roundedCornersXMedium, + ) + .padding(TangemTheme.dimens.spacing12), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResourceSafe(id = R.string.common_network_fee_title), + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + when (feeState) { + is DisableFeeState.Loading -> TextShimmer( + style = TangemTheme.typography.body2, + modifier = Modifier.size( + width = TangemTheme.dimens.size80, + height = TangemTheme.dimens.spacing16, + ), + ) + is DisableFeeState.Content -> Row( + verticalAlignment = Alignment.CenterVertically, + ) { + AuditLabel( + state = AuditLabelUM( + text = stringReference(feeState.feeSymbol), + type = AuditLabelUM.Type.General, + ), + ) + Text( + text = feeState.fiatFormatted, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), + ) + } + is DisableFeeState.Error -> Text( + text = "\u2014", + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.primary1, + ) + } + } + + Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing8)) + DisableFeeDescription(onReadMoreClick = onReadMoreClick) + } +} + +@Composable +private fun DisableFeeDescription(onReadMoreClick: () -> Unit) { + val readMoreText = stringResourceSafe(id = R.string.common_read_more) + val fullText = stringResourceSafe(id = R.string.dynamic_addresses_disable_fee_description) + + val annotatedString = buildAnnotatedString { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(fullText) + append(" ") + } + withLink( + link = LinkAnnotation.Clickable( + tag = "read_more", + linkInteractionListener = { onReadMoreClick() }, + ), + ) { + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + append(readMoreText) + } + } + } + + Text( + text = annotatedString, + style = TangemTheme.typography.caption2, + ) +} + +@Composable +internal fun DynamicAddressesConflictingCustomTokensContent( + content: DynamicAddressesBottomSheetConfig.ConflictingCustomTokens, +) { ErrorContent( titleRes = R.string.dynamic_addresses_error_has_custom_token_title, descriptionRes = R.string.dynamic_addresses_error_has_custom_token_description, buttonTextRes = R.string.common_got_it, - onButtonClick = content.onGotItClick, + onButtonClick = content.onDismissClick, ) } @@ -101,7 +312,7 @@ internal fun DynamicAddressesServiceUnavailableContent(content: DynamicAddresses titleRes = R.string.dynamic_addresses_error_service_unavailable_title, descriptionRes = R.string.dynamic_addresses_error_service_unavailable_description, buttonTextRes = R.string.common_got_it, - onButtonClick = content.onGotItClick, + onButtonClick = content.onDismissClick, ) } @@ -117,7 +328,7 @@ private fun ErrorContent(titleRes: Int, descriptionRes: Int, buttonTextRes: Int, painter = painterResource(id = CoreR.drawable.ic_dynamic_addresses_bottomsheet_enable_unavailable), contentDescription = null, modifier = Modifier.size(TangemTheme.dimens.size44), - tint = TangemTheme.colors.icon.warning, + tint = TangemTheme.colors.icon.attention, ) Spacer(modifier = Modifier.height(TangemTheme.dimens.spacing12)) @@ -176,4 +387,143 @@ private fun FeatureItem(iconRes: Int, title: String, description: String) { ) } } -} \ No newline at end of file +} + +// region Previews + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_Enable() { + TangemThemePreview { + DynamicAddressesEnableContent( + content = DynamicAddressesBottomSheetConfig.Enable( + isCardScanRequired = false, + onEnableClick = {}, + ), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_EnableWithCardScan() { + TangemThemePreview { + DynamicAddressesEnableContent( + content = DynamicAddressesBottomSheetConfig.Enable( + isCardScanRequired = true, + onEnableClick = {}, + ), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_DisableWithoutConsolidation() { + TangemThemePreview { + DynamicAddressesDisableWithoutConsolidationContent( + content = DynamicAddressesBottomSheetConfig.DisableWithoutConsolidation( + onDisableClick = {}, + onReadMoreClick = {}, + ), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_DisableFeeLoading() { + TangemThemePreview { + DynamicAddressesDisableWithConsolidationContent( + content = DynamicAddressesBottomSheetConfig.DisableWithConsolidation( + feeState = DisableFeeState.Loading, + onDisableClick = {}, + onRefreshFee = {}, + onReadMoreClick = {}, + ), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_DisableFeeLoaded() { + TangemThemePreview { + DynamicAddressesDisableWithConsolidationContent( + content = DynamicAddressesBottomSheetConfig.DisableWithConsolidation( + feeState = DisableFeeState.Content( + feeSymbol = "BTC", + fiatFormatted = "~$0.12", + ), + onDisableClick = {}, + onRefreshFee = {}, + onReadMoreClick = {}, + ), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_DisableFeeError() { + TangemThemePreview { + DynamicAddressesDisableWithConsolidationContent( + content = DynamicAddressesBottomSheetConfig.DisableWithConsolidation( + feeState = DisableFeeState.Error, + onDisableClick = {}, + onRefreshFee = {}, + onReadMoreClick = {}, + ), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_DisableSending() { + TangemThemePreview { + DynamicAddressesDisableWithConsolidationContent( + content = DynamicAddressesBottomSheetConfig.DisableWithConsolidation( + feeState = DisableFeeState.Content( + feeSymbol = "BTC", + fiatFormatted = "~$0.12", + ), + isSending = true, + onDisableClick = {}, + onRefreshFee = {}, + onReadMoreClick = {}, + ), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_ConflictingCustomTokens() { + TangemThemePreview { + DynamicAddressesConflictingCustomTokensContent( + content = DynamicAddressesBottomSheetConfig.ConflictingCustomTokens(onDismissClick = {}), + ) + } +} + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun Preview_ServiceUnavailable() { + TangemThemePreview { + DynamicAddressesServiceUnavailableContent( + content = DynamicAddressesBottomSheetConfig.ServiceUnavailable(onDismissClick = {}), + ) + } +} + +// endregion \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 165d042410..d9b87d6b95 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1482" +tangemBlockchainSdk = "develop-1486" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-602" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 4570bc334c149fceb93e9164396291981465f674 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2026 13:26:27 +0400 Subject: [PATCH 15/32] Updated on 2026-08-14 --- .../exchangeServices/DefaultRampManager.kt | 2 +- .../com/tangem/common/StakingBalanceExt.kt | 4 +- .../common/currency/CryptoCurrencyFactory.kt | 2 +- .../DefaultCardCryptoCurrencyFactory.kt | 4 +- .../currency/UserTokensResponseFactory.kt | 2 +- .../DefaultDynamicAddressesRepository.kt | 4 +- .../DefaultCustomTokensRepository.kt | 2 +- .../SimpleNetworkStatusConverterTest.kt | 4 +- .../tangem/data/nft/DefaultNFTRepository.kt | 2 +- .../data/onramp/DefaultOnrampRepository.kt | 8 +- .../DefaultQrScanningEventsRepositoryTest.kt | 6 +- .../data/staking/DefaultStakeKitRepository.kt | 2 +- .../data/swap/DefaultSwapRepositoryV2.kt | 18 ++-- .../data/swap/converter/TokenInfoConverter.kt | 2 +- .../repository/DefaultCurrenciesRepository.kt | 2 +- .../repository/DefaultTokenSyncRepository.kt | 10 +- .../transaction/DefaultAllowanceRepository.kt | 2 +- .../DefaultTransactionRepository.kt | 2 +- .../sign/BlockAidChainNameConverter.kt | 2 +- .../supply/DefaultYieldSupplyRepository.kt | 10 +- ...ultYieldSupplyTransactionRepositoryTest.kt | 2 +- .../usecase/ArchiveCryptoPortfolioUseCase.kt | 2 +- .../usecase/ManageCryptoCurrenciesUseCase.kt | 10 +- .../ArchiveCryptoPortfolioUseCaseTest.kt | 2 +- .../earn/usecase/GetEarnNetworksUseCase.kt | 2 +- .../currency/CryptoCurrencyExtensions.kt | 2 +- .../tangem/domain/models/network/Network.kt | 5 - .../tokens/wallet/WalletBalanceFetcher.kt | 2 +- .../usecase/GetEthSpecificFeeUseCase.kt | 4 +- .../portfolio/add/AvailableToAddData.kt | 4 +- .../details/MarketsTokenDetailsModel.kt | 2 +- .../model/CustomTokenFormModel.kt | 2 +- .../managetokens/model/ManageTokensModel.kt | 2 +- .../model/OnboardingManageTokensModel.kt | 2 +- .../list/CustomTokenFormUseCasesFacade.kt | 2 +- .../utils/list/ManageTokensUseCasesFacade.kt | 2 +- .../portfolio/model/OnrampAddTokenModel.kt | 2 +- .../model/AvailableSwapPairsModel.kt | 6 +- .../notifications/model/NotificationsModel.kt | 2 +- .../deeplink/DefaultStakingDeepLinkHandler.kt | 2 +- .../StakingBalanceEntryConverter.kt | 2 +- .../AddStakingNotificationsTransformer.kt | 4 +- .../SwapChooseContentStateTransformer.kt | 2 +- .../converters/LeastTokenInfoConverter.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 56 +++++------ .../impl/model/MarketBlockDelegate.kt | 2 +- .../tangem/feature/swap/model/SwapModel.kt | 6 +- .../tangem/feature/swap/ui/StateBuilder.kt | 20 ++-- .../DefaultTokenDetailsDeepLinkHandler.kt | 2 +- .../TokenDetailsNotificationConverter.kt | 2 +- .../utils/TokenListAnalyticsSender.kt | 2 +- .../YieldSupplyPromoBannerConverterTest.kt | 12 +-- .../model/YieldSupplyNotificationsModel.kt | 2 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 98 +++++++++---------- 54 files changed, 178 insertions(+), 183 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt index 285f2c46e9..198f3b63d8 100644 --- a/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt +++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/DefaultRampManager.kt @@ -188,7 +188,7 @@ internal class DefaultRampManager( private fun CryptoCurrency.findAssetPredicate(assetId: ExpressAsset.ID): Boolean { val currencyAssedId = ExpressAsset.ID( - networkId = this.network.backendId, + networkId = this.network.rawId, contractAddress = (this as? CryptoCurrency.Token)?.contractAddress, ) diff --git a/common/src/main/kotlin/com/tangem/common/StakingBalanceExt.kt b/common/src/main/kotlin/com/tangem/common/StakingBalanceExt.kt index 9994416d24..7cf1eebdc6 100644 --- a/common/src/main/kotlin/com/tangem/common/StakingBalanceExt.kt +++ b/common/src/main/kotlin/com/tangem/common/StakingBalanceExt.kt @@ -47,7 +47,7 @@ fun StakingBalance.Data.getTotalStakingBalance(blockchainId: String): BigDecimal * StakeKit-specific extension to get total balance including rewards. */ private fun StakingBalance.Data.StakeKit.getTotalWithRewardsStakingBalanceStakeKit(blockchainId: String): BigDecimal { - return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { + return if (BlockchainUtils.isIncludeStakingTotalBalance(networkId = blockchainId)) { balance.items.sumOf { it.amount } } else { getRewardStakingBalance() @@ -58,7 +58,7 @@ private fun StakingBalance.Data.StakeKit.getTotalWithRewardsStakingBalanceStakeK * StakeKit-specific extension to get total staked balance excluding rewards. */ private fun StakingBalance.Data.StakeKit.getTotalStakingBalanceStakeKit(blockchainId: String): BigDecimal { - return if (BlockchainUtils.isIncludeStakingTotalBalance(blockchainId = blockchainId)) { + return if (BlockchainUtils.isIncludeStakingTotalBalance(networkId = blockchainId)) { balance.items .filterNot { it.type == BalanceType.REWARDS } .sumOf { it.amount } diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt index 4b7cffe26c..8773f976e7 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/CryptoCurrencyFactory.kt @@ -169,7 +169,7 @@ class CryptoCurrencyFactory( id = cryptoCurrency.id.rawCurrencyId?.value, ) val blockchain = - Blockchain.fromNetworkId(cryptoCurrency.network.backendId) ?: Blockchain.Unknown + Blockchain.fromNetworkId(cryptoCurrency.network.rawId) ?: Blockchain.Unknown val id = getTokenId(network, sdkToken) return CryptoCurrency.Token( id = id, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt index f56f43a761..c3a4e59b8d 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/DefaultCardCryptoCurrencyFactory.kt @@ -47,7 +47,7 @@ internal class DefaultCardCryptoCurrencyFactory( // check if the blockchain of single-currency wallet is the same as network val cardNetworkId = userWallet.scanResponse.cardTypesResolver.getBlockchain().toNetworkId() - val cardNetwork = networks.firstOrNull { it.backendId == cardNetworkId } + val cardNetwork = networks.firstOrNull { it.rawId == cardNetworkId } if (cardNetwork == null) return emptyMap() @@ -145,7 +145,7 @@ internal class DefaultCardCryptoCurrencyFactory( responseCryptoCurrenciesFactory.createCurrencies( tokens = accountDTO.tokens.orEmpty().filter { token -> networks.any { - it.backendId == token.networkId && it.derivationPath.value == token.derivationPath + it.rawId == token.networkId && it.derivationPath.value == token.derivationPath } }, userWallet = userWallet, diff --git a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt index 85bf2f102f..9bd1896c03 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/currency/UserTokensResponseFactory.kt @@ -40,7 +40,7 @@ class UserTokensResponseFactory @Inject constructor() { UserTokensResponse.Token( id = id.rawCurrencyId?.value, accountId = accountId?.value, - networkId = network.backendId, + networkId = network.rawId, derivationPath = network.derivationPath.value, name = name, symbol = symbol, diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt index a9d2bcd8b7..c2c6203803 100644 --- a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt @@ -87,7 +87,7 @@ internal class DefaultDynamicAddressesRepository( .flatMap { it.tokens.orEmpty() } .any { token -> val tokenDerivationPath = token.derivationPath ?: return@any false - token.networkId == network.backendId && + token.networkId == network.rawId && tokenDerivationPath != baseDerivationPath && hasNonZeroChangeOrIndex(tokenDerivationPath, baseDerivationPath) } @@ -151,7 +151,7 @@ internal class DefaultDynamicAddressesRepository( } private fun UserTokensResponse.Token.matchesNetwork(network: Network): Boolean { - return networkId == network.backendId && + return networkId == network.rawId && derivationPath == network.derivationPath.value && contractAddress == null } 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 dcf575b79f..db25b8597e 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 @@ -91,7 +91,7 @@ internal class DefaultCustomTokensRepository( val response = tangemTechApi.getCoins( contractAddress = contractAddress, - networkIds = network.backendId, + networkIds = network.rawId, active = true, ).getOrThrow() diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt index a7413a891e..0af092a1ca 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverterTest.kt @@ -75,7 +75,7 @@ internal class SimpleNetworkStatusConverterTest { ), expected = SimpleNetworkStatus( id = Network.ID( - value = network.backendId, + value = network.rawId, derivationPath = Network.DerivationPath.Card("card"), ), value = NetworkStatus.Verified( @@ -138,7 +138,7 @@ internal class SimpleNetworkStatusConverterTest { ), expected = SimpleNetworkStatus( id = Network.ID( - value = network.backendId, + value = network.rawId, derivationPath = Network.DerivationPath.Card("card"), ), value = NetworkStatus.NoAccount( diff --git a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt index 3baa26a3f4..d5e0ee4f95 100644 --- a/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt +++ b/data/nft/src/main/kotlin/com/tangem/data/nft/DefaultNFTRepository.kt @@ -568,7 +568,7 @@ internal class DefaultNFTRepository @Inject constructor( private fun Network.canHandleNFTs(userWalletId: UserWalletId): Boolean { val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) - val blockchain = Blockchain.fromNetworkId(backendId) ?: return false + val blockchain = Blockchain.fromNetworkId(rawId) ?: return false return blockchain.canHandleNFTs() && userWallet.canHandleToken(blockchain, excludedBlockchains) diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index 7fe880aa2b..7ab90c6574 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -249,7 +249,7 @@ internal class DefaultOnrampRepository( to = listOf( OnrampDestinationDTO( contractAddress = cryptoCurrency.getContractAddress(), - network = cryptoCurrency.network.backendId, + network = cryptoCurrency.network.rawId, ), ), ), @@ -314,7 +314,7 @@ internal class DefaultOnrampRepository( to = listOf( OnrampDestinationDTO( contractAddress = cryptoCurrency.getContractAddress(), - network = cryptoCurrency.network.backendId, + network = cryptoCurrency.network.rawId, ), ), ), @@ -363,7 +363,7 @@ internal class DefaultOnrampRepository( fromCurrencyCode = currency.code, fromPrecision = currency.precision, toContractAddress = cryptoCurrency.getContractAddress(), - toNetwork = cryptoCurrency.network.backendId, + toNetwork = cryptoCurrency.network.rawId, paymentMethod = paymentMethod.id, countryCode = country.code, fromAmount = fromAmount, @@ -436,7 +436,7 @@ internal class DefaultOnrampRepository( fromCurrencyCode = currency.code, fromPrecision = currency.precision, toContractAddress = cryptoCurrency.getContractAddress(), - toNetwork = cryptoCurrency.network.backendId, + toNetwork = cryptoCurrency.network.rawId, paymentMethod = quote.paymentMethod.id, countryCode = country.code, fromAmount = fromAmountString, diff --git a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt index f904a7a32b..4321199575 100644 --- a/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt +++ b/data/qr-scanning/src/test/java/com/tangem/data/qrscanning/DefaultQrScanningEventsRepositoryTest.kt @@ -78,7 +78,7 @@ internal class DefaultQrScanningEventsRepositoryTest { @Test fun testBip021() { every { network.id } returns Network.ID(value = "bitcoin", derivationPath = Network.DerivationPath.None) - every { network.backendId } returns "bitcoin" + every { network.rawId } returns "bitcoin" positiveCase( "$schema1:$address1", QrResult(address = address1), @@ -129,7 +129,7 @@ internal class DefaultQrScanningEventsRepositoryTest { @Test fun testErc681Coin() { every { network.id } returns Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None) - every { network.backendId } returns "ethereum" + every { network.rawId } returns "ethereum" positiveCase( address2, QrResult(address = address2), @@ -185,7 +185,7 @@ internal class DefaultQrScanningEventsRepositoryTest { @Test fun testErc681Token() { every { network.id } returns Network.ID(value = "ethereum", derivationPath = Network.DerivationPath.None) - every { network.backendId } returns "ethereum" + every { network.rawId } returns "ethereum" positiveCase( address2, QrResult(address = address2), 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 79dc265b4f..8f0fd77aec 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 @@ -411,7 +411,7 @@ internal class DefaultStakeKitRepository( } private fun getTronResource(network: Network): TronResource? { - val blockchain = Blockchain.fromNetworkId(network.backendId) + val blockchain = Blockchain.fromNetworkId(network.rawId) return if (blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet) { TronResource.ENERGY diff --git a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt index a2c353ef57..eea56a309b 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/DefaultSwapRepositoryV2.kt @@ -91,12 +91,12 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val statusFrom = cryptoCurrencyStatusList .firstOrNull { currencyStatus -> currencyStatus.currency.getContractAddress() == pair.from.contractAddress && - currencyStatus.currency.network.backendId == pair.from.network + currencyStatus.currency.network.rawId == pair.from.network } val statusTo = cryptoCurrencyStatusList .firstOrNull { currencyStatus -> currencyStatus.currency.getContractAddress() == pair.to.contractAddress && - currencyStatus.currency.network.backendId == pair.to.network + currencyStatus.currency.network.rawId == pair.to.network } val mappedProviders = pair.providers @@ -143,14 +143,14 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( cryptoCurrencyList .firstOrNull { currency -> currency.getContractAddress() == pair.from.contractAddress && - currency.network.backendId == pair.from.network + currency.network.rawId == pair.from.network } } val statusToDeferred = async { cryptoCurrencyList .firstOrNull { currency -> currency.getContractAddress() == pair.to.contractAddress && - currency.network.backendId == pair.to.network + currency.network.rawId == pair.to.network } } @@ -198,10 +198,10 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( } else { null }, - fromNetwork = fromCryptoCurrency.network.backendId, + fromNetwork = fromCryptoCurrency.network.rawId, fromContractAddress = fromCryptoCurrency.getContractAddress(), fromDecimals = fromCryptoCurrency.decimals, - toNetwork = toCryptoCurrency.network.backendId, + toNetwork = toCryptoCurrency.network.rawId, toContractAddress = toCryptoCurrency.getContractAddress(), toDecimals = toCryptoCurrency.decimals, providerId = provider.providerId, @@ -255,8 +255,8 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( val response = tangemExpressApi.getExchangeData( fromContractAddress = fromCurrency.getContractAddress(), toContractAddress = toCryptoCurrency.getContractAddress(), - fromNetwork = fromCurrency.network.backendId, - toNetwork = toCryptoCurrency.network.backendId, + fromNetwork = fromCurrency.network.rawId, + toNetwork = toCryptoCurrency.network.rawId, fromAddress = fromStatus.networkAddress?.defaultAddress?.value.orEmpty(), toAddress = toAddress, fromDecimals = fromCurrency.decimals, @@ -316,7 +316,7 @@ internal class DefaultSwapRepositoryV2 @Inject constructor( ), body = ExchangeSentRequestBody( txId = txId, - fromNetwork = currency.network.backendId, + fromNetwork = currency.network.rawId, fromAddress = status.networkAddress?.defaultAddress?.value.orEmpty(), payinAddress = payInAddress, payinExtraId = txExtraId, diff --git a/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt b/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt index 04e3203f28..64c6dae739 100644 --- a/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt +++ b/data/swap/src/main/java/com/tangem/data/swap/converter/TokenInfoConverter.kt @@ -10,7 +10,7 @@ class TokenInfoConverter : Converter { override fun convert(value: CryptoCurrency): LeastTokenInfo { return LeastTokenInfo( contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = value.network.backendId, + network = value.network.rawId, ) } 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 077f772d62..b04f24e429 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 @@ -134,7 +134,7 @@ internal class DefaultCurrenciesRepository( } override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean { - val blockchain = Blockchain.fromNetworkId(network.backendId) + val blockchain = Blockchain.fromNetworkId(network.rawId) return blockchain?.isNetworkFeeZero() == true } } \ No newline at end of file diff --git a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt index ed5dccaddf..d7eff8a3f1 100644 --- a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt +++ b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt @@ -183,7 +183,7 @@ internal class DefaultTokenSyncRepository( if (tokenBalances.isEmpty()) { return NetworkResult.Success( - networkId = network.backendId, + networkId = network.rawId, responseTokens = emptyList(), ) } @@ -195,11 +195,11 @@ internal class DefaultTokenSyncRepository( .map { it.toResponseToken() } NetworkResult.Success( - networkId = network.backendId, + networkId = network.rawId, responseTokens = responseTokens, ) } catch (e: Exception) { - NetworkResult.Error(networkId = network.backendId, cause = e) + NetworkResult.Error(networkId = network.rawId, cause = e) } } @@ -217,7 +217,7 @@ internal class DefaultTokenSyncRepository( val tokensToEnrich = tokenBalances.filter { !it.isNativeToken } val catalogMap = fetchCatalogInfo( - networkId = network.backendId, + networkId = network.rawId, contractAddresses = tokensToEnrich.mapNotNull(TokenBalance::contractAddress), ) @@ -240,7 +240,7 @@ internal class DefaultTokenSyncRepository( amount = balance.amount, isNativeToken = false, currencyId = coin.id, - networkId = network.backendId, + networkId = network.rawId, ) } } diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultAllowanceRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultAllowanceRepository.kt index 29395fce20..bdff8734da 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultAllowanceRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultAllowanceRepository.kt @@ -38,7 +38,7 @@ internal class DefaultAllowanceRepository( allowance >= requiredAmount -> AllowanceInfo.Enough(allowance) allowance > BigDecimal.ZERO && allowance < requiredAmount && BlockchainUtils.isTetherInEthereum( - blockchainId = cryptoCurrency.network.rawId, + networkId = cryptoCurrency.network.rawId, contractAddress = cryptoCurrency.contractAddress, ) -> AllowanceInfo.ResetNeeded(allowance, requiredAmount) else -> AllowanceInfo.NotEnough(allowance, requiredAmount) diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 06da7b3fa9..17c0b17828 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -312,7 +312,7 @@ internal class DefaultTransactionRepository( nonce: BigInteger?, gasLimit: BigInteger?, ): TransactionExtras { - val blockchain = Blockchain.fromNetworkId(networkId = network.backendId) + val blockchain = Blockchain.fromNetworkId(networkId = network.rawId) ?: error("Blockchain not found") return when { blockchain.isEvm() -> { diff --git a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt index c8491f5be6..7f934668f6 100644 --- a/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt +++ b/data/wallet-connect/src/main/kotlin/com/tangem/data/walletconnect/sign/BlockAidChainNameConverter.kt @@ -9,7 +9,7 @@ internal object BlockAidChainNameConverter : Converter { @Suppress("CyclomaticComplexMethod") override fun convert(value: Network): String? { - return when (Blockchain.fromNetworkId(value.backendId)) { + return when (Blockchain.fromNetworkId(value.rawId)) { Blockchain.Arbitrum -> "arbitrum" Blockchain.Avalanche -> "avalanche" Blockchain.AvalancheTestnet -> "avalanche-fuji" diff --git a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt index 22f5b7a42e..e8f613b613 100644 --- a/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt +++ b/data/yield-supply/src/main/java/com/tangem/data/yield/supply/DefaultYieldSupplyRepository.kt @@ -43,7 +43,7 @@ internal class DefaultYieldSupplyRepository( private val statusMapFlow = MutableStateFlow>(emptyMap()) - override suspend fun getCachedMarkets(): List? = withContext(dispatchers.io) { + override suspend fun getCachedMarkets(): List = withContext(dispatchers.io) { val cache = store.getSyncOrNull().orEmpty() val domain = cache.map(YieldMarketTokenConverter::convert) domain.enrichNetworkIds() @@ -62,14 +62,14 @@ internal class DefaultYieldSupplyRepository( } override suspend fun getTokenStatus(cryptoCurrencyToken: CryptoCurrency.Token): YieldMarketToken { - val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.rawId)?.getChainId() ?: error("Chain id is required for evm's") val response = yieldSupplyApi.getYieldTokenStatus(chainId, cryptoCurrencyToken.contractAddress).getOrThrow() return YieldMarketTokenConverter.convert(response) } override suspend fun getTokenChart(cryptoCurrencyToken: CryptoCurrency.Token): YieldSupplyMarketChartData { - val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.rawId)?.getChainId() ?: error("Chain id is required for evm's") val response = yieldSupplyApi.getYieldTokenChart(chainId, cryptoCurrencyToken.contractAddress).getOrThrow() return YieldTokenChartConverter.convert(response) @@ -99,7 +99,7 @@ internal class DefaultYieldSupplyRepository( cryptoCurrencyToken: CryptoCurrency.Token, address: String, ): Boolean = withContext(dispatchers.io) { - val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.rawId)?.getChainId() ?: error("Chain id is required for evm's") yieldSupplyApi.activateYieldModule( body = YieldSupplyChangeTokenStatusBody( @@ -113,7 +113,7 @@ internal class DefaultYieldSupplyRepository( override suspend fun deactivateProtocol(cryptoCurrencyToken: CryptoCurrency.Token, address: String): Boolean = withContext(dispatchers.io) { - val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.backendId)?.getChainId() + val chainId = Blockchain.fromNetworkId(cryptoCurrencyToken.network.rawId)?.getChainId() ?: error("Chain id is required for evm's") yieldSupplyApi.deactivateYieldModule( YieldSupplyChangeTokenStatusBody( diff --git a/data/yield-supply/src/test/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepositoryTest.kt b/data/yield-supply/src/test/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepositoryTest.kt index 6d5251e8df..310c568308 100644 --- a/data/yield-supply/src/test/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepositoryTest.kt +++ b/data/yield-supply/src/test/java/com/tangem/data/yield/supply/DefaultYieldSupplyTransactionRepositoryTest.kt @@ -36,7 +36,7 @@ class DefaultYieldSupplyTransactionRepositoryTest { private val userWalletId = mockk() private val cryptoCurrency = mockk(relaxed = true) { every { network.id } returns networkId - every { network.backendId } returns networkId.rawId.value + every { network.rawId } returns networkId.rawId.value every { contractAddress } returns mockedContractAddress } private val cryptoCurrencyStatus = mockk(relaxed = true) { diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt index de9a515a80..b3bc1bf1c0 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCase.kt @@ -80,7 +80,7 @@ class ArchiveCryptoPortfolioUseCase( val hasNotReferralToken = statuses.none { status -> val currency = status.currency - currency.network.backendId == referralToken.networkId && + currency.network.rawId == referralToken.networkId && (currency as? CryptoCurrency.Token)?.contractAddress == referralToken.contractAddress && status.value.networkAddress?.availableAddresses?.any { it.value == address } == true } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt index 3561434daf..82f735a3c9 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/usecase/ManageCryptoCurrenciesUseCase.kt @@ -168,7 +168,7 @@ class ManageCryptoCurrenciesUseCase( val foundToken = accountStatus.tokenList.flattenCurrencies() .mapNotNull { it.currency as? CryptoCurrency.Token } .firstOrNull { token -> - token.network.backendId == networkId && + token.network.rawId == networkId && !token.isCustom && token.contractAddress.equals(contractAddress, true) } @@ -361,7 +361,7 @@ class ManageCryptoCurrenciesUseCase( launch { val assetIds = currencies.mapTo(hashSetOf()) { currency -> ExpressAsset.ID( - networkId = currency.network.backendId, + networkId = currency.network.rawId, contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, ) } @@ -388,19 +388,19 @@ class ManageCryptoCurrenciesUseCase( ) { constructor(network: Network) : this( - networkId = network.backendId, + networkId = network.rawId, derivationPath = network.derivationPath, contractAddress = null, ) constructor(currency: CryptoCurrency) : this( - networkId = currency.network.backendId, + networkId = currency.network.rawId, derivationPath = currency.network.derivationPath, contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, ) constructor(status: CryptoCurrencyStatus) : this( - networkId = status.currency.network.backendId, + networkId = status.currency.network.rawId, derivationPath = status.currency.network.derivationPath, contractAddress = (status.currency as? CryptoCurrency.Token)?.contractAddress, ) diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt index d3257d26ed..1656279ce6 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/usecase/ArchiveCryptoPortfolioUseCaseTest.kt @@ -143,7 +143,7 @@ class ArchiveCryptoPortfolioUseCaseTest { val defaultAddress = "0xABC" val cryptoCurrency = mockk { - every { this@mockk.network.backendId } returns token.networkId + every { this@mockk.network.rawId } returns token.networkId every { this@mockk.contractAddress } returns token.contractAddress!! } 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 e6a93c5605..eb82e7af92 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 @@ -56,7 +56,7 @@ class GetEarnNetworksUseCase( accountLists .filter { it.userWalletId in unlockedWalletsId } .flatMap(AccountList::flattenCurrencies) - .mapTo(HashSet()) { it.network.backendId } + .mapTo(HashSet()) { it.network.rawId } } } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt index cc408905ee..945cc04f81 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/currency/CryptoCurrencyExtensions.kt @@ -3,7 +3,7 @@ package com.tangem.domain.models.currency import java.math.BigDecimal fun CryptoCurrency.Token.yieldSupplyKey(): String { - return "${network.backendId}_$contractAddress" + return "${network.rawId}_$contractAddress" } fun CryptoCurrencyStatus.hasNotSuppliedAmount(): Boolean { diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt index 53be78f1ff..489ede3a4d 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt @@ -35,11 +35,6 @@ data class Network( val nameResolvingType: NameResolvingType, ) { - /** Backend ID */ - @Deprecated("Will be removed later") - val backendId: String - get() = id.rawId.value - /** Raw ID */ val rawId: String get() = id.rawId.value 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 27a9af10ba..1cdfec9931 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 @@ -193,7 +193,7 @@ class WalletBalanceFetcher internal constructor( private suspend fun fetchExpressAssets(userWallet: UserWallet, currencies: Set) { val assetIds = currencies.mapTo(hashSetOf()) { currency -> ExpressAsset.ID( - networkId = currency.network.backendId, + networkId = currency.network.rawId, contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress, ) } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt index 1aec1f720e..ebb7a3bf0e 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/GetEthSpecificFeeUseCase.kt @@ -40,8 +40,8 @@ class GetEthSpecificFeeUseCase( ?: (walletManager as? EthereumWalletManager)?.getGasPriceValue() ?: error("not supported for ${cryptoCurrency.network}") - val blockchain = Blockchain.fromNetworkId(networkId = cryptoCurrency.network.backendId) - ?: error("unknown networkId ${cryptoCurrency.network.backendId}") + val blockchain = Blockchain.fromNetworkId(networkId = cryptoCurrency.network.rawId) + ?: error("unknown networkId ${cryptoCurrency.network.rawId}") val minimalFee = getEthLegacyFee( gasPrice = gasPriceResult, diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt index b868b9b8be..e97bb37f05 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt @@ -36,13 +36,13 @@ data class AvailableToAddAccount( get() = availableNetworks.size == 1 val availableToAddNetworks: Set = availableNetworks - .filter { available -> addedNetworks.none { added -> added.backendId == available.networkId } } + .filter { available -> addedNetworks.none { added -> added.rawId == available.networkId } } .toSet() val isAvailableToAdd: Boolean = availableToAddNetworks.isNotEmpty() val addedMarketNetworks: Set = availableNetworks - .filter { available -> addedNetworks.any { added -> added.backendId == available.networkId } } + .filter { available -> addedNetworks.any { added -> added.rawId == available.networkId } } .toSet() } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt index 85cb23c471..05f213308c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/market/details/MarketsTokenDetailsModel.kt @@ -526,7 +526,7 @@ internal class MarketsTokenDetailsModel @Inject constructor( val networks = newInfo.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, + networkId = network.networkId, excludedBlockchains = excludedBlockchains, hotExcludedBlockchains = hotWalletExcludedBlockchains, hasOnlyHotWallets = isAllWalletsIsHot, diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt index e62d320665..16c044adb6 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/CustomTokenFormModel.kt @@ -183,7 +183,7 @@ internal class CustomTokenFormModel @Inject constructor( ) = modelScope.launch { val isNeedColdWalletInteraction = coldWalletAndHasMissedDerivationsUseCase.invoke( userWalletId = params.mode.userWalletId, - networksWithDerivationPath = mapOf(currency.network.backendId to getDerivationPath().value), + networksWithDerivationPath = mapOf(currency.network.rawId to getDerivationPath().value), ) state.update { state -> diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt index 2c10b94d72..e767b7ab35 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/ManageTokensModel.kt @@ -293,7 +293,7 @@ internal class ManageTokensModel @Inject constructor( val networks = currenciesToAdd.values .flatten() .toSet() - .associate { network -> network.backendId to network.derivationPath.value } + .associate { network -> network.rawId to network.derivationPath.value } val isNeedToInteractWithColdWallet = useCasesFacade.needColdWalletInteraction(networks) state.update { state -> diff --git a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt index 2e0e1b9a56..ceb222b45e 100644 --- a/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt +++ b/features/manage-tokens/impl/src/main/kotlin/com/tangem/features/managetokens/model/OnboardingManageTokensModel.kt @@ -212,7 +212,7 @@ internal class OnboardingManageTokensModel @Inject constructor( val network = currenciesToAdd.values .flatten() .toSet() - .associate { network -> network.backendId to network.derivationPath.value } + .associate { network -> network.rawId to network.derivationPath.value } val shouldShowTangemIcon = useCasesFacade.needColdWalletInteraction(network = network) state.update { state -> state.copy( 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 96cc6f1e8a..e09585fdee 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 @@ -81,7 +81,7 @@ internal class CustomTokenFormUseCasesFacade @AssistedInject constructor( private fun CryptoCurrency.getAccountIndex(): Either = either { val currency = this@getAccountIndex - val blockchain = Blockchain.fromNetworkId(networkId = currency.network.backendId) + val blockchain = Blockchain.fromNetworkId(networkId = currency.network.rawId) if (blockchain == null) { val exception = IllegalStateException("Token has unknown networkId: ${currency.id}") TangemLogger.e("Error", exception) 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 63cab69a36..6271951a73 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 @@ -79,7 +79,7 @@ internal class ManageTokensUseCasesFacade @AssistedInject constructor( ?: return IllegalStateException("Account not found").left() (account.cryptoCurrencies + added - removed).any { currency -> - currency is CryptoCurrency.Token && currency.network.backendId == network.backendId && + currency is CryptoCurrency.Token && currency.network.rawId == network.rawId && currency.network.derivationPath == network.derivationPath } .right() diff --git a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt index 0432278b80..ea195b5280 100644 --- a/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt +++ b/features/onramp/impl/src/main/kotlin/com/tangem/features/onramp/hottokens/portfolio/model/OnrampAddTokenModel.kt @@ -50,7 +50,7 @@ internal class OnrampAddTokenModel @Inject constructor( .distinctUntilChanged() .mapLatest { tokenToAdd: AddHotCryptoData -> addTokenJob.join() - val backendId = tokenToAdd.cryptoCurrency.network.backendId + val backendId = tokenToAdd.cryptoCurrency.network.rawId val userWalletId = tokenToAdd.account.accountId.userWalletId val isTangemIconVisible = needColdWalletInteraction( walletId = userWalletId, 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 f3b60a2dcd..e949b69cf7 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 @@ -252,7 +252,7 @@ internal class AvailableSwapPairsModel @Inject constructor( is AccountStatus.CryptoPortfolio -> { val statuses = accountStatus.tokenList.flattenCurrencies() .filterNot { status -> - status.currency.network.backendId == selectedStatus?.currency?.network?.backendId && + status.currency.network.rawId == selectedStatus?.currency?.network?.rawId && status.currency.id.contractAddress == selectedStatus.currency.id.contractAddress } .filterByQuery(query = query) @@ -462,7 +462,7 @@ internal class AvailableSwapPairsModel @Inject constructor( private fun CryptoCurrencyStatus.toLeastTokenInfo(): LeastTokenInfo { return LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = currency.network.backendId, + network = currency.network.rawId, ) } @@ -607,7 +607,7 @@ internal class AvailableSwapPairsModel @Inject constructor( val networks = tokenMarket.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, + networkId = network.networkId, coinId = tokenMarket.id.value, contractAddress = network.contractAddress, excludedBlockchains = excludedBlockchains, diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt index 5cf7dc551b..cd6ae47ea5 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/subcomponents/notifications/model/NotificationsModel.kt @@ -251,7 +251,7 @@ internal class NotificationsModel @Inject constructor( addExceedsBalanceNotification( cryptoCurrencyWarning = currencyWarning, cryptoCurrencyStatus = cryptoCurrencyStatus, - shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(currency.network.backendId), + shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(currency.network.rawId), onClick = ::showTokenDetails, onAnalyticsEvent = { val event = NotificationsAnalyticEvents.NoticeNotEnoughFee( diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt index a12c16b790..648df734f4 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/deeplink/DefaultStakingDeepLinkHandler.kt @@ -54,7 +54,7 @@ internal class DefaultStakingDeepLinkHandler @AssistedInject constructor( ) .orEmpty() .firstOrNull { currency -> - val isNetwork = currency.network.backendId.equals(networkId, ignoreCase = true) + val isNetwork = currency.network.rawId.equals(networkId, ignoreCase = true) val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true isNetwork && isCurrency } diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt index a46b3a7c79..2cee0bc82d 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/converters/StakingBalanceEntryConverter.kt @@ -85,7 +85,7 @@ internal class StakingBalanceEntryConverter( private fun StakingBalanceEntry.getBalanceValue(): BigDecimal { val isIncludeStakingTotalBalance = BlockchainUtils.isIncludeStakingTotalBalance( - blockchainId = cryptoCurrencyStatus.currency.network.rawId, + networkId = cryptoCurrencyStatus.currency.network.rawId, ) return if (isIncludeStakingTotalBalance) { amount diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt index 8849435172..1786ed1195 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/notifications/AddStakingNotificationsTransformer.kt @@ -229,7 +229,7 @@ internal class AddStakingNotificationsTransformer( addExceedsBalanceNotification( cryptoCurrencyWarning = currencyWarning, cryptoCurrencyStatus = cryptoCurrencyStatus, - shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId), + shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum(network.rawId), onClick = prevState.clickIntents::openTokenDetails, onAnalyticsEvent = { prevState.clickIntents.onNotEnoughFeeNotificationShow() }, onResetAnalyticsEvent = { /*no-op*/ }, @@ -315,7 +315,7 @@ internal class AddStakingNotificationsTransformer( currencyName = name, feeName = name, feeSymbol = symbol, - mergeFeeNetworkName = BlockchainUtils.isArbitrum(network.backendId), + mergeFeeNetworkName = BlockchainUtils.isArbitrum(network.rawId), onClick = { onClick(this) }, ) } diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt index 3868504cd6..39b0819d33 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/choosetoken/fromSupported/model/transformers/SwapChooseContentStateTransformer.kt @@ -29,7 +29,7 @@ internal class SwapChooseContentStateTransformer( val isMain = cryptoCurrency is CryptoCurrency.Coin val subtitle = when { - BlockchainUtils.isL2Network(networkId = network.backendId) -> MAIN_NETWORK_L2_TYPE_NAME + BlockchainUtils.isL2Network(networkId = network.rawId) -> MAIN_NETWORK_L2_TYPE_NAME isMain -> MAIN_NETWORK_TYPE_NAME network.standardType !is Network.StandardType.Unspecified -> network.standardType.name else -> "" diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt index 12dc1705f0..cbb6bd7b04 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/LeastTokenInfoConverter.kt @@ -9,7 +9,7 @@ class LeastTokenInfoConverter : Converter { override fun convert(value: CryptoCurrency): LeastTokenInfo { return LeastTokenInfo( contractAddress = (value as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = value.network.backendId, + network = value.network.rawId, ) } } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index a8f4b68486..b90ffae536 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -162,7 +162,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( userWallet = userWallet, initialCurrency = LeastTokenInfo( contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = currency.network.backendId, + network = currency.network.rawId, ), currenciesList = walletAccountCurrencyStatusesExceptInitial.flatMap { accountStatus -> accountStatus.value.map { it.currency } @@ -199,7 +199,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private fun List.filterCurrencies(currency: CryptoCurrency) = this.filter { status -> - val isDifferentCurrency = status.currency.network.backendId != currency.network.backendId || + val isDifferentCurrency = status.currency.network.rawId != currency.network.rawId || status.currency.getContractAddress() != currency.getContractAddress() val hasValidStatus = @@ -218,7 +218,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ): CurrenciesGroup { val filteredPairs = leastPairs.filter { pair -> tokenInfoForFilter(pair).contractAddress == currency.getContractAddress() && - tokenInfoForFilter(pair).network == currency.network.backendId + tokenInfoForFilter(pair).network == currency.network.rawId } val accountCurrencyList = cryptoCurrenciesList.map { (accountEntry, currencyStatusList) -> @@ -259,7 +259,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( return swapPairsLeastList.firstNotNullOfOrNull { pair -> val listTokenInfo = tokenInfoForAvailable(pair) - if (cryptoCurrencyStatuses.currency.network.backendId == listTokenInfo.network && + if (cryptoCurrencyStatuses.currency.network.rawId == listTokenInfo.network && cryptoCurrencyStatuses.currency.getContractAddress() == listTokenInfo.contractAddress && isAvailableForSwap ) { @@ -359,7 +359,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( is Account.Payment -> true else -> isBalanceEnough(fromToken, amount, null) } - val networkId = fromToken.currency.network.backendId + val networkId = fromToken.currency.network.rawId return supervisorScope { providers.map { provider -> @@ -453,9 +453,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( val maybeQuotes = repository.findBestQuote( userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), - fromNetwork = fromToken.currency.network.backendId, + fromNetwork = fromToken.currency.network.rawId, toContractAddress = toToken.currency.getContractAddress(), - toNetwork = toToken.currency.network.backendId, + toNetwork = toToken.currency.network.rawId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.currency.decimals, @@ -527,9 +527,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( val maybeQuotes = repository.findBestQuote( userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), - fromNetwork = fromToken.currency.network.backendId, + fromNetwork = fromToken.currency.network.rawId, toContractAddress = toToken.currency.getContractAddress(), - toNetwork = toToken.currency.network.backendId, + toNetwork = toToken.currency.network.rawId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.currency.decimals, @@ -672,7 +672,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( FeePaidCurrency.Coin -> { val nativeBalance = walletManagersFacade.getNativeTokenBalance( userWalletId = userWalletId, - networkId = fromTokenStatus.currency.network.backendId, + networkId = fromTokenStatus.currency.network.rawId, derivationPath = fromTokenStatus.currency.network.derivationPath.value, ) @@ -780,7 +780,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } ExchangeProviderType.DEX, ExchangeProviderType.DEX_BRIDGE -> { - val networkId = currencyToSend.currency.network.backendId + val networkId = currencyToSend.currency.network.rawId if (isSolana(networkId)) { onSwapSolanaDex( provider = swapProvider, @@ -902,7 +902,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( repository.exchangeSent( userWallet = userWallet, txId = swapData.transaction.txId, - fromNetwork = currencyToSendStatus.currency.network.backendId, + fromNetwork = currencyToSendStatus.currency.network.rawId, fromAddress = fromAddress, payInAddress = payInAddress, txHash = txHash, @@ -968,10 +968,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( val exchangeData = repository.getExchangeData( userWallet = userWallet, fromContractAddress = currencyToSend.currency.getContractAddress(), - fromNetwork = currencyToSend.currency.network.backendId, + fromNetwork = currencyToSend.currency.network.rawId, toContractAddress = currencyToGet.currency.getContractAddress(), fromAddress = fromAddress, - toNetwork = currencyToGet.currency.network.backendId, + toNetwork = currencyToGet.currency.network.rawId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = currencyToGet.currency.decimals, @@ -1016,7 +1016,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ), exchangeData = TangemPayWithdrawExchangeState( txId = exchangeDataCex.txId, - fromNetwork = currencyToSend.currency.network.backendId, + fromNetwork = currencyToSend.currency.network.rawId, fromAddress = networkAddress?.defaultAddress?.value.orEmpty(), payInAddress = exchangeData.transaction.txTo, payInExtraId = exchangeDataCex.txExtraId, @@ -1080,7 +1080,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( repository.exchangeSent( userWallet = userWallet, txId = exchangeDataCex.txId, - fromNetwork = currencyToSend.currency.network.backendId, + fromNetwork = currencyToSend.currency.network.rawId, fromAddress = cexFromAddress, payInAddress = getPayoutAddress(txData), txHash = txHash, @@ -1222,10 +1222,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( repository.getExchangeData( userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), - fromNetwork = fromToken.currency.network.backendId, + fromNetwork = fromToken.currency.network.rawId, toContractAddress = toToken.currency.getContractAddress(), fromAddress = dexFromAddress, - toNetwork = toToken.currency.network.backendId, + toNetwork = toToken.currency.network.rawId, fromAmount = swapAmount.toStringWithRightOffset(), fromDecimals = swapAmount.decimals, toDecimals = toToken.currency.decimals, @@ -1235,7 +1235,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( refundAddress = fromToken.value.networkAddress?.defaultAddress?.value, expressOperationType = ExpressOperationType.SWAP, ).map { swapData -> - val networkId = fromToken.currency.network.backendId + val networkId = fromToken.currency.network.rawId val transaction = swapData.transaction as ExpressTransactionModel.DEX loadFeeForDex( @@ -1361,9 +1361,9 @@ internal class SwapInteractorImpl @AssistedInject constructor( val quotes = repository.findBestQuote( userWallet = userWallet, fromContractAddress = fromToken.getContractAddress(), - fromNetwork = fromToken.network.backendId, + fromNetwork = fromToken.network.rawId, toContractAddress = toToken.getContractAddress(), - toNetwork = toToken.network.backendId, + toNetwork = toToken.network.rawId, fromAmount = amountToRequest.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.decimals, @@ -1739,10 +1739,10 @@ internal class SwapInteractorImpl @AssistedInject constructor( return repository.getExchangeData( userWallet = userWallet, fromContractAddress = fromToken.currency.getContractAddress(), - fromNetwork = fromToken.currency.network.backendId, + fromNetwork = fromToken.currency.network.rawId, toContractAddress = toToken.currency.getContractAddress(), fromAddress = dexFromAddress, - toNetwork = toToken.currency.network.backendId, + toNetwork = toToken.currency.network.rawId, fromAmount = amount.toStringWithRightOffset(), fromDecimals = amount.decimals, toDecimals = toToken.currency.decimals, @@ -1896,7 +1896,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( ): Either = either { val nativeBalance = walletManagersFacade.getNativeTokenBalance( userWalletId = userWalletId, - networkId = network.backendId, + networkId = network.rawId, derivationPath = fromToken.network.derivationPath.value, ) @@ -2059,14 +2059,14 @@ internal class SwapInteractorImpl @AssistedInject constructor( ) } // setting up amount for approve with given amount for swap [SwapApproveType.Limited] - val fromAddress = requireNotNull( + requireNotNull( fromTokenStatus.value.networkAddress?.defaultAddress?.value, - ) { "networkAddress cant be null" } + ) { "networkAddress cannot be null" } val allowanceInfo = getAllowanceInfoUseCase( userWalletId = userWalletId, cryptoCurrency = fromToken, - spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" }, + spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cannot be null" }, requiredAmount = swapAmount.value, ).getOrNull() @@ -2229,7 +2229,7 @@ internal class SwapInteractorImpl @AssistedInject constructor( } private fun createNativeAmountForDex(txValueAmount: String, network: Network): Amount { - val nativeDecimals = Blockchain.fromNetworkId(network.backendId)?.decimals() + val nativeDecimals = Blockchain.fromNetworkId(network.rawId)?.decimals() ?: error("Blockchain not found") val decimalValue = txValueAmount.toBigDecimalOrNull()?.movePointLeft(nativeDecimals) ?: error("txValue parse error") diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt index d83753ba54..0a0340e417 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt @@ -187,7 +187,7 @@ internal class MarketBlockDelegate @AssistedInject constructor( val networks = tokenMarket.networks?.filter { network -> BlockchainUtils.isSupportedNetworkId( - blockchainId = network.networkId, + networkId = network.networkId, coinId = tokenMarket.id.value, contractAddress = network.contractAddress, excludedBlockchains = excludedBlockchains, 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 41203dc1f7..5416aaad86 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 @@ -880,11 +880,11 @@ internal class SwapModel @Inject constructor( private fun sendErrorAnalyticsEvent(error: ExpressDataError, provider: SwapProvider) { val receiveToken = dataState.toCryptoCurrency?.currency?.let { currency -> - "${currency.network.backendId}:${currency.symbol}" + "${currency.network.rawId}:${currency.symbol}" } analyticsErrorEventHandler.sendErrorEvent( SwapEvents.NoticeProviderError( - sendToken = "${initialCurrencyFrom.network.backendId}:${initialCurrencyFrom.symbol}", + sendToken = "${initialCurrencyFrom.network.rawId}:${initialCurrencyFrom.symbol}", receiveToken = receiveToken.orEmpty(), provider = provider, errorCode = error.code, @@ -1174,7 +1174,7 @@ internal class SwapModel @Inject constructor( } runCatching(dispatchers.io) { swapInteractor.givePermissionToSwap( - networkId = fromToken.network.backendId, + networkId = fromToken.network.rawId, permissionOptions = PermissionOptions( approveData = approveDataModel, forTokenContractAddress = (fromToken as? CryptoCurrency.Token)?.contractAddress.orEmpty(), 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 0ebbb39c1e..7df83970a1 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 @@ -90,7 +90,7 @@ internal class StateBuilder( token = null, tokenIconUrl = initialCurrencyFrom.iconUrl, tokenCurrency = initialCurrencyFrom.symbol, - coinId = initialCurrencyFrom.network.backendId, + coinId = initialCurrencyFrom.network.rawId, canSelectAnotherToken = false, isNotNativeToken = initialCurrencyFrom is CryptoCurrency.Token, balance = "", @@ -108,7 +108,7 @@ internal class StateBuilder( balance = "", isNotNativeToken = initialCurrencyTo is CryptoCurrency.Token, networkIconRes = initialCurrencyTo?.let { getActiveIconRes(it.network.rawId) }, - coinId = initialCurrencyTo?.network?.backendId, + coinId = initialCurrencyTo?.network?.rawId, isBalanceHidden = true, ), fee = FeeItemState.Empty, @@ -145,7 +145,7 @@ internal class StateBuilder( amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), token = fromToken, tokenIconUrl = fromToken.currency.iconUrl, - coinId = fromToken.currency.network.backendId, + coinId = fromToken.currency.network.rawId, isNotNativeToken = fromToken.currency is CryptoCurrency.Token, tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, @@ -196,7 +196,7 @@ internal class StateBuilder( amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), token = fromToken, tokenIconUrl = fromToken.currency.iconUrl, - coinId = fromToken.currency.network.backendId, + coinId = fromToken.currency.network.rawId, isNotNativeToken = fromToken.currency is CryptoCurrency.Token, tokenCurrency = fromToken.currency.symbol, canSelectAnotherToken = canSelectSendToken, @@ -214,7 +214,7 @@ internal class StateBuilder( amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), token = toToken, tokenIconUrl = toToken.currency.iconUrl, - coinId = toToken.currency.network.backendId, + coinId = toToken.currency.network.rawId, isNotNativeToken = toToken.currency is CryptoCurrency.Token, tokenCurrency = toToken.currency.symbol, canSelectAnotherToken = canSelectReceiveToken, @@ -266,7 +266,7 @@ internal class StateBuilder( token = uiStateHolder.sendCardData.token, tokenIconUrl = fromToken.iconUrl, tokenCurrency = fromToken.symbol, - coinId = fromToken.network.backendId, + coinId = fromToken.network.rawId, isNotNativeToken = fromToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectSendToken, balance = if (!canSelectSendToken) uiStateHolder.sendCardData.balance else "", @@ -282,7 +282,7 @@ internal class StateBuilder( token = uiStateHolder.receiveCardData.token, tokenIconUrl = toToken.iconUrl, tokenCurrency = toToken.symbol, - coinId = toToken.network.backendId, + coinId = toToken.network.rawId, isNotNativeToken = toToken is CryptoCurrency.Token, canSelectAnotherToken = canSelectReceiveToken, balance = if (!canSelectReceiveToken) uiStateHolder.receiveCardData.balance else "", @@ -374,7 +374,7 @@ internal class StateBuilder( amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat), token = fromCurrencyStatus, tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, - coinId = fromCurrencyStatus.currency.network.backendId, + coinId = fromCurrencyStatus.currency.network.rawId, isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, @@ -415,7 +415,7 @@ internal class StateBuilder( }, token = toCurrencyStatus, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, - coinId = toCurrencyStatus.currency.network.backendId, + coinId = toCurrencyStatus.currency.network.rawId, isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, @@ -535,7 +535,7 @@ internal class StateBuilder( amountEquivalent = getFormattedFiatAmount(BigDecimal.ZERO), token = toToken, tokenIconUrl = uiStateHolder.receiveCardData.tokenIconUrl, - coinId = toToken.currency.network.backendId, + coinId = toToken.currency.network.rawId, isNotNativeToken = uiStateHolder.receiveCardData.isNotNativeToken, tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt index d375886c77..3a3054beab 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/deeplink/DefaultTokenDetailsDeepLinkHandler.kt @@ -140,7 +140,7 @@ internal class DefaultTokenDetailsDeepLinkHandler @AssistedInject constructor( val derivationPath = queryParams[DERIVATION_PATH_KEY] getCryptoCurrencies(userWalletId = userWallet.walletId)?.firstOrNull { currency -> - val isNetwork = currency.network.backendId.equals(networkId, ignoreCase = true) + val isNetwork = currency.network.rawId.equals(networkId, ignoreCase = true) val isCurrency = currency.id.rawCurrencyId?.value?.equals(tokenId, ignoreCase = true) == true val isDefaultDerivation = currency.network.derivationPath is Network.DerivationPath.Card diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index a57f83eafb..802ec6e0bb 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -168,6 +168,6 @@ internal class TokenDetailsNotificationConverter( // workaround for networks that users have misunderstanding private fun CryptoCurrency.shouldMergeFeeNetworkName(): Boolean { - return Blockchain.fromNetworkId(this.network.backendId) == Blockchain.Arbitrum + return Blockchain.fromNetworkId(this.network.rawId) == Blockchain.Arbitrum } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt index d4d08e2e70..0a6847ed30 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/TokenListAnalyticsSender.kt @@ -150,7 +150,7 @@ internal class TokenListAnalyticsSender @Inject constructor( ) { // for now send only for Polkadot ecosystem blockchains // later dependency on Blockchain will be removed and use token name - when (val blockchain = Blockchain.fromNetworkId(currencyStatus.currency.network.backendId)) { + when (val blockchain = Blockchain.fromNetworkId(currencyStatus.currency.network.rawId)) { Blockchain.Polkadot, Blockchain.AlephZero, Blockchain.Kusama, diff --git a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt index 050f700004..cd26b7f591 100644 --- a/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt +++ b/features/wallet/impl/src/test/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/YieldSupplyPromoBannerConverterTest.kt @@ -35,7 +35,7 @@ class YieldSupplyPromoBannerConverterTest { tokenList = tokenList, ) val converter = YieldSupplyPromoBannerConverter( - yieldModuleApyMap = mapOf("${token.network.backendId}_${token.contractAddress}" to BigDecimal("0.10")), + yieldModuleApyMap = mapOf("${token.network.rawId}_${token.contractAddress}" to BigDecimal("0.10")), shouldShowMainPromo = false, ) @@ -71,7 +71,7 @@ class YieldSupplyPromoBannerConverterTest { tokenList = ungroupedTokenList(statusActive), ) val converter = YieldSupplyPromoBannerConverter( - yieldModuleApyMap = mapOf("${token.network.backendId}_${token.contractAddress}" to BigDecimal("0.12")), + yieldModuleApyMap = mapOf("${token.network.rawId}_${token.contractAddress}" to BigDecimal("0.12")), shouldShowMainPromo = true, ) @@ -90,8 +90,8 @@ class YieldSupplyPromoBannerConverterTest { val statusBig = createLoadedStatus(token = tokenBig, amount = BigDecimal("10.00"), isYieldActive = false) val apyMap = mapOf( - "${tokenSmall.network.backendId}_${tokenSmall.contractAddress.lowercase()}" to BigDecimal("0.05"), - "${tokenBig.network.backendId}_${tokenBig.contractAddress.uppercase()}" to BigDecimal("0.15"), + "${tokenSmall.network.rawId}_${tokenSmall.contractAddress.lowercase()}" to BigDecimal("0.05"), + "${tokenBig.network.rawId}_${tokenBig.contractAddress.uppercase()}" to BigDecimal("0.15"), ) val params = TokenConverterParams.Wallet( @@ -114,7 +114,7 @@ class YieldSupplyPromoBannerConverterTest { val token = createToken(networkId = nonEvmId, rawId = nonEvmId, contract = "rAbC123") val status = createLoadedStatus(token = token, amount = BigDecimal("3"), isYieldActive = false) - val mismatchedKey = "${token.network.backendId}_${token.contractAddress.lowercase()}" + val mismatchedKey = "${token.network.rawId}_${token.contractAddress.lowercase()}" val apyMap = mapOf(mismatchedKey to BigDecimal("0.07")) val params = TokenConverterParams.Wallet( @@ -140,7 +140,7 @@ class YieldSupplyPromoBannerConverterTest { tokenList = ungroupedTokenList(status), ) val converter = YieldSupplyPromoBannerConverter( - yieldModuleApyMap = mapOf("${token.network.backendId}_${token.contractAddress}" to BigDecimal("0.10")), + yieldModuleApyMap = mapOf("${token.network.rawId}_${token.contractAddress}" to BigDecimal("0.10")), shouldShowMainPromo = true, ) diff --git a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt index 122487f1e7..a08903b346 100644 --- a/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt +++ b/features/yield-supply/impl/src/main/java/com/tangem/features/yield/supply/impl/subcomponents/notifications/model/YieldSupplyNotificationsModel.kt @@ -61,7 +61,7 @@ internal class YieldSupplyNotificationsModel @Inject constructor( cryptoCurrencyWarning = cryptoCurrencyWarning, cryptoCurrencyStatus = cryptoCurrencyStatus, shouldMergeFeeNetworkName = BlockchainUtils.isArbitrum( - blockchainId = cryptoCurrencyStatus.currency.network.backendId, + networkId = cryptoCurrencyStatus.currency.network.rawId, ), onClick = ::openTokenDetails, onAnalyticsEvent = { /*no-op*/ }, 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 cf3897b510..fe00a42eb6 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 @@ -37,103 +37,103 @@ object BlockchainUtils { } /** If current [networkId] is Bitcoin */ - fun isBitcoin(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isBitcoin(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet } - /** If current [networkId] is use custom fee */ - fun isUseBitcoinFeeConverter(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() - return isBitcoin(blockchainId) || blockchain == Blockchain.Fact0rn + /** Checks if the current [blockchainId] uses a custom fee converter */ + fun isUseBitcoinFeeConverter(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() + return isBitcoin(networkId) || blockchain == Blockchain.Fact0rn } - /** If current [blockchainId] is Tezos */ - fun isTezos(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + /** If current [networkId] is Tezos */ + fun isTezos(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Tezos } - fun isCardano(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isCardano(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Cardano } - /** If current [blockchainId] is BeaconChain */ - fun isBeaconChain(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + /** If current [networkId] is BeaconChain */ + fun isBeaconChain(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Binance || blockchain == Blockchain.BinanceTestnet } - /** If current [blockchainId] is Polygon */ - fun isPolygonChain(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + /** If current [networkId] is Polygon */ + fun isPolygonChain(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Polygon || blockchain == Blockchain.PolygonTestnet } - fun isTron(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isTron(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Tron || blockchain == Blockchain.TronTestnet } - fun isTon(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isTon(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet } fun isSupportedNetworkId( - blockchainId: String, + networkId: String, excludedBlockchains: ExcludedBlockchains, hotExcludedBlockchains: Set, hasOnlyHotWallets: Boolean = false, coinId: String? = null, contractAddress: String? = null, ): Boolean { - val blockchain = blockchainId.toBlockchain() ?: return false + val blockchain = networkId.toBlockchain() ?: return false 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 + if (coinId != null && !isNotBlockedByTerraV1Filter(networkId, coinId)) return false } return true } - fun isArbitrum(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isArbitrum(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Arbitrum } - fun isSolana(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isSolana(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Solana } - fun isPolkadot(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isPolkadot(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Polkadot || blockchain == Blockchain.PolkadotTestnet } - fun isCosmos(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isCosmos(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Cosmos || blockchain == Blockchain.CosmosTestnet } - fun isBSC(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isBSC(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.BSC || blockchain == Blockchain.BSCTestnet } - fun isEthereum(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isEthereum(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain == Blockchain.Ethereum || blockchain == Blockchain.EthereumTestnet } - fun isClore(blockchainId: String): Boolean { - return blockchainId.toBlockchain() == Blockchain.Clore + fun isClore(networkId: String): Boolean { + return networkId.toBlockchain() == Blockchain.Clore } data class BlockchainInfo( @@ -162,29 +162,29 @@ object BlockchainUtils { /** * Blockchains not affecting total balance counting on errors */ - fun isIncludeToBalanceOnError(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isIncludeToBalanceOnError(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return when (blockchain) { Blockchain.Binance, Blockchain.BinanceTestnet -> true else -> false } } - fun isIncludeStakingTotalBalance(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isIncludeStakingTotalBalance(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain != Blockchain.Cardano } - fun isStakingRewardUnavailable(blockchainId: String, isCoin: Boolean): Boolean { - val isP2PEthPool = isEthereum(blockchainId) && isCoin + fun isStakingRewardUnavailable(networkId: String, isCoin: Boolean): Boolean { + val isP2PEthPool = isEthereum(networkId) && isCoin - return isSolana(blockchainId) || isBSC(blockchainId) || isTon(blockchainId) || isP2PEthPool + return isSolana(networkId) || isBSC(networkId) || isTon(networkId) || isP2PEthPool } /** Checks if the blockchain uses case-insensitive contract addresses */ - fun isCaseInsensitiveContractAddress(blockchainId: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isCaseInsensitiveContractAddress(networkId: String): Boolean { + val blockchain = networkId.toBlockchain() return blockchain?.isEvm() == true } @@ -222,8 +222,8 @@ object BlockchainUtils { /** * Checks if the given coin is Tether on Ethereum network, which may require special handling in some cases. */ - fun isTetherInEthereum(blockchainId: String, contractAddress: String): Boolean { - val blockchain = blockchainId.toBlockchain() + fun isTetherInEthereum(networkId: String, contractAddress: String): Boolean { + val blockchain = networkId.toBlockchain() return (blockchain == Blockchain.Ethereum || blockchain == Blockchain.EthereumTestnet) && contractAddress.equals(TETHER_CONTRACT_ADDRESS, ignoreCase = true) } From 602e20a1d2b035234e7f05168ecebad1ae65beac Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2026 15:21:55 +0500 Subject: [PATCH 16/32] Updated on 2026-08-14 --- app/build.gradle.kts | 4 +- .../di/domain/AssetsDiscoveryDomainModule.kt | 52 ++++++++ .../tap/di/domain/TokenSyncDomainModule.kt | 50 -------- .../configs/feature_toggles_config.json | 2 +- .../local/preferences/PreferencesKeys.kt | 2 +- .../build.gradle.kts | 7 +- .../DefaultAssetsDiscoveryFacade.kt | 86 +++++++++++++ .../di/AssetsDiscoveryDataModule.kt | 61 +++++++++ .../DefaultAssetsDiscoveryRepository.kt} | 118 +++++++++--------- .../store/AssetsDiscoveryStore.kt} | 4 +- .../store/AssetsDiscoveryStoreFactory.kt} | 10 +- .../store/DefaultAssetsDiscoveryStore.kt} | 6 +- .../data/tokensync/di/TokenSyncDataModule.kt | 49 -------- .../DefaultWalletManagersFacade.kt | 12 -- .../build.gradle.kts | 5 +- .../assetsdiscovery/AssetsDiscoveryFacade.kt | 15 +++ .../model/AssetsDiscoveryProgress.kt} | 10 +- .../repository/AssetsDiscoveryRepository.kt | 25 ++++ ...owledgeAssetsDiscoveryCompletionUseCase.kt | 13 ++ .../usecase/ObserveAssetsDiscoveryUseCase.kt | 15 +++ .../usecase/StartAssetsDiscoveryUseCase.kt} | 26 ++-- .../repository/TokenSyncRepository.kt | 25 ---- .../AcknowledgeTokenSyncCompletionUseCase.kt | 13 -- .../usecase/ObserveTokenSyncUseCase.kt | 15 --- .../walletmanager/WalletManagersFacade.kt | 3 - .../preview/PreviewDetailsComponent.kt | 2 +- .../hotwallet/HotWalletFeatureToggles.kt | 2 +- features/hot-wallet/impl/build.gradle.kts | 2 +- .../DefaultHotWalletFeatureToggles.kt | 4 +- .../HotAccessCodeRequestModel.kt | 8 +- .../model/AddExistingWalletImportModel.kt | 8 +- .../forgetwallet/ForgetWalletModel.kt | 8 +- .../wallet-settings/impl/build.gradle.kts | 2 +- .../model/WalletSettingsModel.kt | 8 +- features/wallet/impl/build.gradle.kts | 2 +- .../wallet/child/wallet/model/WalletModel.kt | 12 +- .../intents/WalletWarningsClickIntents.kt | 16 +-- .../utils/WalletWarningsAnalyticsSender.kt | 2 +- .../domain/GetMultiWalletWarningsFactory.kt | 37 +++--- .../domain/WalletAdditionalInfoFactory.kt | 8 +- .../implementors/MultiWalletContentLoader.kt | 6 +- .../state/model/AssetsDiscoveryProgressUM.kt | 13 ++ .../wallet/state/model/TokenSyncProgressUM.kt | 13 -- .../wallet/state/model/WalletNotification.kt | 2 +- .../wallet/state/model/WalletState.kt | 6 +- ... SetAssetsDiscoveryProgressTransformer.kt} | 8 +- .../SetTokenListErrorTransformer.kt | 2 +- .../transformers/SetTokenListTransformer.kt | 2 +- .../UpdateWalletCardsCountTransformer.kt | 6 +- ...criber.kt => AssetsDiscoverySubscriber.kt} | 26 ++-- .../blockchainsdk/BlockchainSDKFactory.kt | 4 + .../DefaultBlockchainSDKFactory.kt | 16 +++ settings.gradle.kts | 4 +- 53 files changed, 489 insertions(+), 368 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/di/domain/AssetsDiscoveryDomainModule.kt delete mode 100644 app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt rename data/{tokensync => assetsdiscovery}/build.gradle.kts (78%) create mode 100644 data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/DefaultAssetsDiscoveryFacade.kt create mode 100644 data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/di/AssetsDiscoveryDataModule.kt rename data/{tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt => assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/repository/DefaultAssetsDiscoveryRepository.kt} (72%) rename data/{tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStore.kt => assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/AssetsDiscoveryStore.kt} (73%) rename data/{tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStoreFactory.kt => assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/AssetsDiscoveryStoreFactory.kt} (86%) rename data/{tokensync/src/main/java/com/tangem/data/tokensync/store/DefaultTokenSyncStore.kt => assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/DefaultAssetsDiscoveryStore.kt} (84%) delete mode 100644 data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt rename domain/{tokensync => assetsdiscovery}/build.gradle.kts (73%) create mode 100644 domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/AssetsDiscoveryFacade.kt rename domain/{tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt => assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/model/AssetsDiscoveryProgress.kt} (56%) create mode 100644 domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/repository/AssetsDiscoveryRepository.kt create mode 100644 domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/AcknowledgeAssetsDiscoveryCompletionUseCase.kt create mode 100644 domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/ObserveAssetsDiscoveryUseCase.kt rename domain/{tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt => assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/StartAssetsDiscoveryUseCase.kt} (71%) delete mode 100644 domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt delete mode 100644 domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/AcknowledgeTokenSyncCompletionUseCase.kt delete mode 100644 domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/ObserveTokenSyncUseCase.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/AssetsDiscoveryProgressUM.kt delete mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/{SetTokenSyncProgressTransformer.kt => SetAssetsDiscoveryProgressTransformer.kt} (88%) rename features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/{TokenSyncSubscriber.kt => AssetsDiscoverySubscriber.kt} (56%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c7411dc8a..5def3358f3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -161,7 +161,7 @@ dependencies { implementation(projects.domain.hotWallet) implementation(projects.domain.news) implementation(projects.domain.earn) - implementation(projects.domain.tokensync) + implementation(projects.domain.assetsdiscovery) implementation(projects.domain.search) implementation(projects.common) @@ -192,7 +192,7 @@ dependencies { implementation(projects.data.common) implementation(projects.data.settings) implementation(projects.data.tokens) - implementation(projects.data.tokensync) + implementation(projects.data.assetsdiscovery) implementation(projects.data.txhistory) implementation(projects.data.wallets) implementation(projects.data.analytics) diff --git a/app/src/main/java/com/tangem/tap/di/domain/AssetsDiscoveryDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/AssetsDiscoveryDomainModule.kt new file mode 100644 index 0000000000..1745d88c51 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/di/domain/AssetsDiscoveryDomainModule.kt @@ -0,0 +1,52 @@ +package com.tangem.tap.di.domain + +import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase +import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository +import com.tangem.domain.assetsdiscovery.usecase.AcknowledgeAssetsDiscoveryCompletionUseCase +import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase +import com.tangem.domain.assetsdiscovery.usecase.StartAssetsDiscoveryUseCase +import com.tangem.utils.coroutines.AppCoroutineScope +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 AssetsDiscoveryDomainModule { + + @Provides + @Singleton + fun provideObserveAssetsDiscoveryUseCase( + assetsDiscoveryRepository: AssetsDiscoveryRepository, + ): ObserveAssetsDiscoveryUseCase { + return ObserveAssetsDiscoveryUseCase( + assetsDiscoveryRepository = assetsDiscoveryRepository, + ) + } + + @Provides + @Singleton + fun provideAcknowledgeAssetsDiscoveryCompletionUseCase( + assetsDiscoveryRepository: AssetsDiscoveryRepository, + ): AcknowledgeAssetsDiscoveryCompletionUseCase { + return AcknowledgeAssetsDiscoveryCompletionUseCase( + assetsDiscoveryRepository = assetsDiscoveryRepository, + ) + } + + @Provides + @Singleton + fun provideStartAssetsDiscoveryUseCase( + assetsDiscoveryRepository: AssetsDiscoveryRepository, + manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, + appCoroutineScope: AppCoroutineScope, + ): StartAssetsDiscoveryUseCase { + return StartAssetsDiscoveryUseCase( + assetsDiscoveryRepository = assetsDiscoveryRepository, + manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase, + appCoroutineScope = appCoroutineScope, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt deleted file mode 100644 index 49da21bf6d..0000000000 --- a/app/src/main/java/com/tangem/tap/di/domain/TokenSyncDomainModule.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.tangem.tap.di.domain - -import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase -import com.tangem.domain.tokensync.repository.TokenSyncRepository -import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase -import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase -import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase -import com.tangem.utils.coroutines.AppCoroutineScope -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 TokenSyncDomainModule { - - @Provides - @Singleton - fun provideObserveTokenSyncUseCase(tokenSyncRepository: TokenSyncRepository): ObserveTokenSyncUseCase { - return ObserveTokenSyncUseCase( - tokenSyncRepository = tokenSyncRepository, - ) - } - - @Provides - @Singleton - fun provideAcknowledgeTokenSyncCompletionUseCase( - tokenSyncRepository: TokenSyncRepository, - ): AcknowledgeTokenSyncCompletionUseCase { - return AcknowledgeTokenSyncCompletionUseCase( - tokenSyncRepository = tokenSyncRepository, - ) - } - - @Provides - @Singleton - fun provideStartTokenSyncUseCase( - tokenSyncRepository: TokenSyncRepository, - manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, - appCoroutineScope: AppCoroutineScope, - ): StartTokenSyncUseCase { - return StartTokenSyncUseCase( - tokenSyncRepository = tokenSyncRepository, - manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase, - appCoroutineScope = appCoroutineScope, - ) - } -} \ 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 1a45030b37..0154a6d8a0 100644 --- a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json +++ b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json @@ -73,7 +73,7 @@ "version": "undefined" }, { - "name": "TOKEN_SYNC_ENABLED", + "name": "ASSETS_DISCOVERY_ENABLED", "version": "undefined" }, { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index be52c28b41..f03cdfb336 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -136,7 +136,7 @@ object PreferencesKeys { val HAS_HAD_FIRST_TOP_UP_KEY by lazy { stringPreferencesKey(name = "hasHadFirstTopUp") } - val PENDING_DISCOVERY_SYNC_KEY by lazy { stringPreferencesKey(name = "pendingDiscoverySync") } + val PENDING_ASSETS_DISCOVERY_KEY by lazy { stringPreferencesKey(name = "pendingAssetsDiscovery") } // region Notifications val NOTIFICATIONS_APPLICATION_ID_KEY by lazy { stringPreferencesKey(name = "notificationsApplicationId") } diff --git a/data/tokensync/build.gradle.kts b/data/assetsdiscovery/build.gradle.kts similarity index 78% rename from data/tokensync/build.gradle.kts rename to data/assetsdiscovery/build.gradle.kts index 973df2ab9a..d043536d23 100644 --- a/data/tokensync/build.gradle.kts +++ b/data/assetsdiscovery/build.gradle.kts @@ -7,22 +7,25 @@ plugins { } android { - namespace = "com.tangem.data.tokensync" + namespace = "com.tangem.data.assetsdiscovery" } dependencies { - api(projects.domain.tokensync) + api(projects.domain.assetsdiscovery) implementation(projects.domain.tokens) implementation(projects.domain.tokens.models) implementation(projects.domain.models) implementation(projects.domain.walletManager) implementation(projects.domain.wallets) implementation(projects.data.common) + implementation(projects.data.walletManager) implementation(projects.libs.blockchainSdk) + implementation(projects.libs.tangemSdkApi) implementation(projects.core.datasource) implementation(projects.core.utils) implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) implementation(deps.androidx.datastore) diff --git a/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/DefaultAssetsDiscoveryFacade.kt b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/DefaultAssetsDiscoveryFacade.kt new file mode 100644 index 0000000000..412cb02e9b --- /dev/null +++ b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/DefaultAssetsDiscoveryFacade.kt @@ -0,0 +1,86 @@ +package com.tangem.data.assetsdiscovery + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.data.walletmanager.extensions.makePublicKey +import com.tangem.domain.assetsdiscovery.AssetsDiscoveryFacade +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.domain.common.wallets.getSyncStrict +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.wallets.config.curvesConfig +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.withContext +import javax.inject.Inject + +internal class DefaultAssetsDiscoveryFacade @Inject constructor( + private val blockchainSDKFactory: BlockchainSDKFactory, + private val userWalletsListRepository: UserWalletsListRepository, + private val dispatchers: CoroutineDispatcherProvider, +) : AssetsDiscoveryFacade { + + override suspend fun getAssetsDiscoveryService( + userWalletId: UserWalletId, + network: Network, + ): AssetsDiscoveryFacade.AssetsDiscoveryServiceInfo? = withContext(dispatchers.io) { + val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) + if (userWallet !is UserWallet.Hot) return@withContext null + + val assetsDiscoveryServiceFactory = blockchainSDKFactory.getAssetsDiscoveryServiceFactorySync() + ?: return@withContext null + + val blockchain = network.toBlockchain() + val address = makeAddress(userWallet, blockchain, network.derivationPath.value) + ?: return@withContext null + + AssetsDiscoveryFacade.AssetsDiscoveryServiceInfo( + address = address, + service = assetsDiscoveryServiceFactory.create(blockchain), + ) + } + + private fun makeAddress(hotWallet: UserWallet.Hot, blockchain: Blockchain, derivationPath: String?): String? { + val curve = hotWallet.curvesConfig.primaryCurve(blockchain) + val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve } + ?: return null + + val path = derivationPath?.let { DerivationPath(rawPath = it) } + + val publicKey = if (path != null) { + makePublicKey( + seedKey = selectedWallet.publicKey, + blockchain = blockchain, + derivationPath = path, + derivedWalletKeys = selectedWallet.derivedKeys, + isWallet2 = true, + ) ?: return null + } else { + null + } + + return try { + val addresses = if (publicKey != null) { + blockchain.makeAddresses( + walletPublicKey = publicKey.blockchainKey, + pairPublicKey = null, + curve = selectedWallet.curve, + ) + } else { + blockchain.makeAddresses( + walletPublicKey = selectedWallet.publicKey, + pairPublicKey = null, + curve = selectedWallet.curve, + ) + } + addresses.find { it.type == AddressType.Default }?.value + } catch (e: Throwable) { + TangemLogger.w("Failed to derive address for $blockchain", e) + null + } + } +} \ No newline at end of file diff --git a/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/di/AssetsDiscoveryDataModule.kt b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/di/AssetsDiscoveryDataModule.kt new file mode 100644 index 0000000000..ac451f953c --- /dev/null +++ b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/di/AssetsDiscoveryDataModule.kt @@ -0,0 +1,61 @@ +package com.tangem.data.assetsdiscovery.di + +import com.tangem.blockchainsdk.BlockchainSDKFactory +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.assetsdiscovery.DefaultAssetsDiscoveryFacade +import com.tangem.data.assetsdiscovery.repository.DefaultAssetsDiscoveryRepository +import com.tangem.data.assetsdiscovery.store.AssetsDiscoveryStoreFactory +import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory +import com.tangem.data.common.network.NetworkFactory +import com.tangem.datasource.api.tangemTech.TangemTechApi +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.domain.assetsdiscovery.AssetsDiscoveryFacade +import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository +import com.tangem.domain.common.wallets.UserWalletsListRepository +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object AssetsDiscoveryDataModule { + + @Provides + @Singleton + fun provideAssetsDiscoveryFacade( + blockchainSDKFactory: BlockchainSDKFactory, + userWalletsListRepository: UserWalletsListRepository, + dispatchers: CoroutineDispatcherProvider, + ): AssetsDiscoveryFacade = DefaultAssetsDiscoveryFacade( + blockchainSDKFactory = blockchainSDKFactory, + userWalletsListRepository = userWalletsListRepository, + dispatchers = dispatchers, + ) + + @Provides + @Singleton + fun provideAssetsDiscoveryRepository( + assetsDiscoveryFacade: AssetsDiscoveryFacade, + tangemTechApi: TangemTechApi, + userWalletsListRepository: UserWalletsListRepository, + networkFactory: NetworkFactory, + appPreferencesStore: AppPreferencesStore, + assetsDiscoveryStoreFactory: AssetsDiscoveryStoreFactory, + responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, + dispatchers: CoroutineDispatcherProvider, + excludedBlockchains: ExcludedBlockchains, + ): AssetsDiscoveryRepository = DefaultAssetsDiscoveryRepository( + assetsDiscoveryFacade = assetsDiscoveryFacade, + tangemTechApi = tangemTechApi, + userWalletsListRepository = userWalletsListRepository, + networkFactory = networkFactory, + appPreferencesStore = appPreferencesStore, + assetsDiscoveryStoreFactory = assetsDiscoveryStoreFactory, + responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, + dispatchers = dispatchers, + excludedBlockchains = excludedBlockchains, + ) +} \ No newline at end of file diff --git a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/repository/DefaultAssetsDiscoveryRepository.kt similarity index 72% rename from data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt rename to data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/repository/DefaultAssetsDiscoveryRepository.kt index d7eff8a3f1..1f05ac7237 100644 --- a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/repository/DefaultTokenSyncRepository.kt +++ b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/repository/DefaultAssetsDiscoveryRepository.kt @@ -1,12 +1,12 @@ -package com.tangem.data.tokensync.repository +package com.tangem.data.assetsdiscovery.repository +import com.tangem.blockchain.assetsdiscovery.models.DiscoveredAsset import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.tokenbalance.models.TokenBalance import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.data.assetsdiscovery.store.AssetsDiscoveryStore +import com.tangem.data.assetsdiscovery.store.AssetsDiscoveryStoreFactory import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory import com.tangem.data.common.network.NetworkFactory -import com.tangem.data.tokensync.store.TokenSyncStore -import com.tangem.data.tokensync.store.TokenSyncStoreFactory import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.tangemTech.TangemTechApi import com.tangem.datasource.api.tangemTech.models.CoinsResponse @@ -14,15 +14,15 @@ import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectMapSync +import com.tangem.domain.assetsdiscovery.AssetsDiscoveryFacade +import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress +import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.common.wallets.getSyncStrict 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.tokensync.model.TokenSyncProgress -import com.tangem.domain.tokensync.repository.TokenSyncRepository -import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async @@ -30,42 +30,39 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import java.math.BigDecimal import java.util.concurrent.ConcurrentHashMap @Suppress("LongParameterList") -internal class DefaultTokenSyncRepository( - private val walletManagersFacade: WalletManagersFacade, +internal class DefaultAssetsDiscoveryRepository( + private val assetsDiscoveryFacade: AssetsDiscoveryFacade, private val tangemTechApi: TangemTechApi, private val userWalletsListRepository: UserWalletsListRepository, private val networkFactory: NetworkFactory, private val appPreferencesStore: AppPreferencesStore, - private val tokenSyncStoreFactory: TokenSyncStoreFactory, + private val assetsDiscoveryStoreFactory: AssetsDiscoveryStoreFactory, private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, private val dispatchers: CoroutineDispatcherProvider, private val excludedBlockchains: ExcludedBlockchains, -) : TokenSyncRepository { +) : AssetsDiscoveryRepository { - private val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS) - private val progressStates = ConcurrentHashMap>() + private val progressStates = ConcurrentHashMap>() - override fun observeSyncProgress(userWalletId: UserWalletId): Flow { + override fun observeDiscoveryProgress(userWalletId: UserWalletId): Flow { return getProgressFlow(userWalletId) } override fun acknowledgeCompletion(userWalletId: UserWalletId) { val key = userWalletId.stringValue val stateFlow = progressStates[key] ?: return - stateFlow.value = TokenSyncProgress.Idle + stateFlow.value = AssetsDiscoveryProgress.Idle progressStates.remove(key, stateFlow) } override suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List { - val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) - val storedTokens = tokenSyncStore.get() + val assetsDiscoveryStore = assetsDiscoveryStoreFactory.provide(userWalletId) + val storedTokens = assetsDiscoveryStore.get() if (storedTokens.isEmpty()) return emptyList() val userWallet = userWalletsListRepository.getSyncStrict(userWalletId) @@ -77,34 +74,34 @@ internal class DefaultTokenSyncRepository( } override suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) { - val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) - tokenSyncStore.clear() + val assetsDiscoveryStore = assetsDiscoveryStoreFactory.provide(userWalletId) + assetsDiscoveryStore.clear() } override suspend fun clearPendingFlag(userWalletId: UserWalletId) { setPendingFlag(userWalletId, value = false) } - override suspend fun getPendingSyncWalletIds(): List { + override suspend fun getPendingDiscoveryWalletIds(): List { val pendingMap = appPreferencesStore - .getObjectMapSync(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY) + .getObjectMapSync(PreferencesKeys.PENDING_ASSETS_DISCOVERY_KEY) return pendingMap .filter { it.value } .map { UserWalletId(it.key) } } - override suspend fun runSync(userWalletId: UserWalletId) { + override suspend fun runDiscovery(userWalletId: UserWalletId) { val networks = getSupportedNetworks(userWalletId) if (networks.isEmpty()) return setPendingFlag(userWalletId, value = true) - val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId) - tokenSyncStore.clear() + val assetsDiscoveryStore = assetsDiscoveryStoreFactory.provide(userWalletId) + assetsDiscoveryStore.clear() val batches = networks.chunked(MAX_CONCURRENT_REQUESTS) var completedNetworks = 0 - getProgressFlow(userWalletId).value = TokenSyncProgress.InProgress( + getProgressFlow(userWalletId).value = AssetsDiscoveryProgress.InProgress( completedNetworks = 0, totalNetworks = networks.size, ) @@ -114,25 +111,23 @@ internal class DefaultTokenSyncRepository( completedNetworks = handleBatchResults( userWalletId = userWalletId, results = batchResults, - tokenSyncStore = tokenSyncStore, + assetsDiscoveryStore = assetsDiscoveryStore, completedNetworks = completedNetworks, totalNetworks = networks.size, ) } } - override suspend fun completeSync(userWalletId: UserWalletId) { + override suspend fun completeDiscovery(userWalletId: UserWalletId) { setPendingFlag(userWalletId, value = false) - getProgressFlow(userWalletId).value = TokenSyncProgress.Completed + getProgressFlow(userWalletId).value = AssetsDiscoveryProgress.Completed } private suspend fun processBatch(userWalletId: UserWalletId, batch: List): List { return coroutineScope { batch.map { network -> async(dispatchers.io) { - semaphore.withPermit { - processNetwork(userWalletId, network) - } + processNetwork(userWalletId, network) } }.awaitAll() } @@ -141,7 +136,7 @@ internal class DefaultTokenSyncRepository( private suspend fun handleBatchResults( userWalletId: UserWalletId, results: List, - tokenSyncStore: TokenSyncStore, + assetsDiscoveryStore: AssetsDiscoveryStore, completedNetworks: Int, totalNetworks: Int, ): Int { @@ -150,8 +145,8 @@ internal class DefaultTokenSyncRepository( for (result in results) { completed++ - handleNetworkResult(result, tokenSyncStore) - progressFlow.value = TokenSyncProgress.InProgress( + handleNetworkResult(result, assetsDiscoveryStore) + progressFlow.value = AssetsDiscoveryProgress.InProgress( completedNetworks = completed, totalNetworks = totalNetworks, ) @@ -160,35 +155,35 @@ internal class DefaultTokenSyncRepository( return completed } - private suspend fun handleNetworkResult(result: NetworkResult, tokenSyncStore: TokenSyncStore) { + private suspend fun handleNetworkResult(result: NetworkResult, assetsDiscoveryStore: AssetsDiscoveryStore) { when (result) { is NetworkResult.Success -> { if (result.responseTokens.isNotEmpty()) { try { - tokenSyncStore.append(result.responseTokens) + assetsDiscoveryStore.append(result.responseTokens) } catch (e: Exception) { TangemLogger.e("Failed to store discovered tokens for network: ${result.networkId}", e) } } } is NetworkResult.Error -> { - TangemLogger.e("Token sync failed for network: ${result.networkId}", result.cause) + TangemLogger.e("Assets discovery failed for network: ${result.networkId}", result.cause) } } } private suspend fun processNetwork(userWalletId: UserWalletId, network: Network): NetworkResult { return try { - val tokenBalances = fetchAndFilterTokenBalances(userWalletId, network) + val discoveredAssets = discoverAndFilterAssets(userWalletId, network) - if (tokenBalances.isEmpty()) { + if (discoveredAssets.isEmpty()) { return NetworkResult.Success( networkId = network.rawId, responseTokens = emptyList(), ) } - val enrichedTokens = enrichTokensWithCatalog(tokenBalances, network) + val enrichedTokens = enrichTokensWithCatalog(discoveredAssets, network) val responseTokens = enrichedTokens .filter { it.contractAddress != null } @@ -203,36 +198,35 @@ internal class DefaultTokenSyncRepository( } } - private suspend fun fetchAndFilterTokenBalances(userWalletId: UserWalletId, network: Network): List { - return withContext(dispatchers.io) { - walletManagersFacade.getTokenBalances(userWalletId, network) + private suspend fun discoverAndFilterAssets(userWalletId: UserWalletId, network: Network): List = + withContext(dispatchers.io) { + val providerInfo = assetsDiscoveryFacade.getAssetsDiscoveryService(userWalletId, network) + ?: return@withContext emptyList() + providerInfo.service.discoverAssets(providerInfo.address) .filter { it.amount > BigDecimal.ZERO } } - } private suspend fun enrichTokensWithCatalog( - tokenBalances: List, + assets: List, network: Network, - ): List = withContext(dispatchers.io) { - val tokensToEnrich = tokenBalances.filter { !it.isNativeToken } + ): List = withContext(dispatchers.io) { + val tokensToEnrich = assets.filterIsInstance() val catalogMap = fetchCatalogInfo( networkId = network.rawId, - contractAddresses = tokensToEnrich.mapNotNull(TokenBalance::contractAddress), + contractAddresses = tokensToEnrich.map { it.contractAddress }, ) - tokenBalances.mapNotNull { balance -> - if (balance.isNativeToken) return@mapNotNull null - - val contractAddressLower = balance.contractAddress?.lowercase() - val coin = contractAddressLower?.let { catalogMap[it] } ?: return@mapNotNull null + tokensToEnrich.mapNotNull { balance -> + val contractAddressLower = balance.contractAddress.lowercase() + val coin = catalogMap[contractAddressLower] ?: return@mapNotNull null val decimals = coin.networks .find { it.contractAddress?.lowercase() == contractAddressLower } ?.decimalCount ?.toInt() ?: 0 - DiscoveredToken( + EnrichedDiscoveredAsset( contractAddress = balance.contractAddress, symbol = coin.symbol, name = coin.name, @@ -290,23 +284,23 @@ internal class DefaultTokenSyncRepository( } } - private fun getProgressFlow(userWalletId: UserWalletId): MutableStateFlow { + private fun getProgressFlow(userWalletId: UserWalletId): MutableStateFlow { return progressStates.getOrPut(userWalletId.stringValue) { - MutableStateFlow(TokenSyncProgress.Idle) + MutableStateFlow(AssetsDiscoveryProgress.Idle) } } private suspend fun setPendingFlag(userWalletId: UserWalletId, value: Boolean) { appPreferencesStore.editData { prefs -> prefs.setObjectMap( - key = PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY, - value = prefs.getObjectMap(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY) + key = PreferencesKeys.PENDING_ASSETS_DISCOVERY_KEY, + value = prefs.getObjectMap(PreferencesKeys.PENDING_ASSETS_DISCOVERY_KEY) .plus(userWalletId.stringValue to value), ) } } - private fun DiscoveredToken.toResponseToken(): UserTokensResponse.Token { + private fun EnrichedDiscoveredAsset.toResponseToken(): UserTokensResponse.Token { return UserTokensResponse.Token( id = currencyId, networkId = networkId, @@ -317,7 +311,7 @@ internal class DefaultTokenSyncRepository( ) } - private data class DiscoveredToken( + private data class EnrichedDiscoveredAsset( val contractAddress: String?, val symbol: String, val name: String, diff --git a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStore.kt b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/AssetsDiscoveryStore.kt similarity index 73% rename from data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStore.kt rename to data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/AssetsDiscoveryStore.kt index 919b551121..fa1b8c658e 100644 --- a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStore.kt +++ b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/AssetsDiscoveryStore.kt @@ -1,8 +1,8 @@ -package com.tangem.data.tokensync.store +package com.tangem.data.assetsdiscovery.store import com.tangem.datasource.api.tangemTech.models.UserTokensResponse -interface TokenSyncStore { +interface AssetsDiscoveryStore { suspend fun get(): List diff --git a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStoreFactory.kt b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/AssetsDiscoveryStoreFactory.kt similarity index 86% rename from data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStoreFactory.kt rename to data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/AssetsDiscoveryStoreFactory.kt index 43c191b61c..914c387d7d 100644 --- a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/TokenSyncStoreFactory.kt +++ b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/AssetsDiscoveryStoreFactory.kt @@ -1,4 +1,4 @@ -package com.tangem.data.tokensync.store +package com.tangem.data.assetsdiscovery.store import android.content.Context import androidx.datastore.core.DataStore @@ -17,18 +17,18 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class TokenSyncStoreFactory @Inject constructor( +class AssetsDiscoveryStoreFactory @Inject constructor( @NetworkMoshi private val moshi: Moshi, @ApplicationContext private val context: Context, private val appScope: AppCoroutineScope, ) { - private val stores = ConcurrentHashMap() + private val stores = ConcurrentHashMap() - fun provide(userWalletId: UserWalletId): TokenSyncStore { + fun provide(userWalletId: UserWalletId): AssetsDiscoveryStore { val userWalletStringId = userWalletId.formatted() return stores.computeIfAbsent(userWalletStringId) { - DefaultTokenSyncStore( + DefaultAssetsDiscoveryStore( persistenceStore = createPersistenceStore( fileName = "token_sync_$userWalletStringId", types = listTypes(), diff --git a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/DefaultTokenSyncStore.kt b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/DefaultAssetsDiscoveryStore.kt similarity index 84% rename from data/tokensync/src/main/java/com/tangem/data/tokensync/store/DefaultTokenSyncStore.kt rename to data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/DefaultAssetsDiscoveryStore.kt index d498d28ca3..9bc4d75f7f 100644 --- a/data/tokensync/src/main/java/com/tangem/data/tokensync/store/DefaultTokenSyncStore.kt +++ b/data/assetsdiscovery/src/main/kotlin/com/tangem/data/assetsdiscovery/store/DefaultAssetsDiscoveryStore.kt @@ -1,12 +1,12 @@ -package com.tangem.data.tokensync.store +package com.tangem.data.assetsdiscovery.store import androidx.datastore.core.DataStore import com.tangem.datasource.api.tangemTech.models.UserTokensResponse import kotlinx.coroutines.flow.firstOrNull -internal class DefaultTokenSyncStore( +internal class DefaultAssetsDiscoveryStore( private val persistenceStore: DataStore>, -) : TokenSyncStore { +) : AssetsDiscoveryStore { override suspend fun get(): List { return persistenceStore.data.firstOrNull().orEmpty() diff --git a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt b/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt deleted file mode 100644 index 525a7612fa..0000000000 --- a/data/tokensync/src/main/kotlin/com/tangem/data/tokensync/di/TokenSyncDataModule.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.tangem.data.tokensync.di - -import com.tangem.blockchainsdk.utils.ExcludedBlockchains -import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory -import com.tangem.data.common.network.NetworkFactory -import com.tangem.data.tokensync.repository.DefaultTokenSyncRepository -import com.tangem.data.tokensync.store.TokenSyncStoreFactory -import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.local.preferences.AppPreferencesStore -import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.domain.tokensync.repository.TokenSyncRepository -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.utils.coroutines.CoroutineDispatcherProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -internal object TokenSyncDataModule { - - @Provides - @Singleton - fun provideTokenSyncRepository( - walletManagersFacade: WalletManagersFacade, - tangemTechApi: TangemTechApi, - userWalletsListRepository: UserWalletsListRepository, - networkFactory: NetworkFactory, - appPreferencesStore: AppPreferencesStore, - tokenSyncStoreFactory: TokenSyncStoreFactory, - responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory, - dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, - ): TokenSyncRepository { - return DefaultTokenSyncRepository( - walletManagersFacade = walletManagersFacade, - tangemTechApi = tangemTechApi, - userWalletsListRepository = userWalletsListRepository, - networkFactory = networkFactory, - appPreferencesStore = appPreferencesStore, - tokenSyncStoreFactory = tokenSyncStoreFactory, - responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory, - dispatchers = dispatchers, - excludedBlockchains = excludedBlockchains, - ) - } -} \ No newline at end of file diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 117bfacb59..1055b56685 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -19,7 +19,6 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.nft.models.NFTAsset import com.tangem.blockchain.nft.models.NFTCollection -import com.tangem.blockchain.tokenbalance.models.TokenBalance import com.tangem.blockchain.transactionhistory.models.TransactionHistoryRequest import com.tangem.blockchain.yieldsupply.YieldSupplyContractCallDataProviderFactory import com.tangem.blockchainsdk.BlockchainSDKFactory @@ -850,17 +849,6 @@ internal class DefaultWalletManagersFacade @Inject constructor( return blockchain.getNFTExploreUrl(assetIdentifier) } - override suspend fun getTokenBalances(userWalletId: UserWalletId, network: Network): List { - val blockchain = network.toBlockchain() - val walletManager = getOrCreateWalletManager( - userWalletId = userWalletId, - blockchain = blockchain, - derivationPath = network.derivationPath.value, - ) ?: return emptyList() - val address = walletManager.wallet.address - return walletManager.getTokenBalances(address) - } - override suspend fun isAccountInitialized(userWalletId: UserWalletId, network: Network): Boolean { val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) val initializableAccountWalletManger = walletManager as? InitializableAccount ?: return true diff --git a/domain/tokensync/build.gradle.kts b/domain/assetsdiscovery/build.gradle.kts similarity index 73% rename from domain/tokensync/build.gradle.kts rename to domain/assetsdiscovery/build.gradle.kts index 35464995f3..6bea487412 100644 --- a/domain/tokensync/build.gradle.kts +++ b/domain/assetsdiscovery/build.gradle.kts @@ -5,7 +5,7 @@ plugins { } android { - namespace = "com.tangem.domain.tokensync" + namespace = "com.tangem.domain.assetsdiscovery" } dependencies { @@ -14,6 +14,9 @@ dependencies { implementation(projects.domain.account.status) implementation(projects.core.utils) + implementation(projects.libs.blockchainSdk) + implementation(tangemDeps.blockchain) + implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) } \ No newline at end of file diff --git a/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/AssetsDiscoveryFacade.kt b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/AssetsDiscoveryFacade.kt new file mode 100644 index 0000000000..ef873ecd3b --- /dev/null +++ b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/AssetsDiscoveryFacade.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.assetsdiscovery + +import com.tangem.blockchain.assetsdiscovery.AssetsDiscoveryService +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId + +interface AssetsDiscoveryFacade { + + suspend fun getAssetsDiscoveryService(userWalletId: UserWalletId, network: Network): AssetsDiscoveryServiceInfo? + + data class AssetsDiscoveryServiceInfo( + val address: String, + val service: AssetsDiscoveryService, + ) +} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/model/AssetsDiscoveryProgress.kt similarity index 56% rename from domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt rename to domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/model/AssetsDiscoveryProgress.kt index ee78b42a70..3b3d2cb7a4 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/model/TokenSyncProgress.kt +++ b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/model/AssetsDiscoveryProgress.kt @@ -1,13 +1,13 @@ -package com.tangem.domain.tokensync.model +package com.tangem.domain.assetsdiscovery.model -sealed class TokenSyncProgress { +sealed class AssetsDiscoveryProgress { - data object Idle : TokenSyncProgress() + data object Idle : AssetsDiscoveryProgress() data class InProgress( val completedNetworks: Int, val totalNetworks: Int, - ) : TokenSyncProgress() { + ) : AssetsDiscoveryProgress() { val progressPercent: Int get() = if (totalNetworks > 0) { completedNetworks * 100 / totalNetworks @@ -16,5 +16,5 @@ sealed class TokenSyncProgress { } } - data object Completed : TokenSyncProgress() + data object Completed : AssetsDiscoveryProgress() } \ No newline at end of file diff --git a/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/repository/AssetsDiscoveryRepository.kt b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/repository/AssetsDiscoveryRepository.kt new file mode 100644 index 0000000000..cf9020c3c1 --- /dev/null +++ b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/repository/AssetsDiscoveryRepository.kt @@ -0,0 +1,25 @@ +package com.tangem.domain.assetsdiscovery.repository + +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress +import kotlinx.coroutines.flow.Flow + +interface AssetsDiscoveryRepository { + + suspend fun runDiscovery(userWalletId: UserWalletId) + + suspend fun completeDiscovery(userWalletId: UserWalletId) + + suspend fun getPendingDiscoveryWalletIds(): List + + fun observeDiscoveryProgress(userWalletId: UserWalletId): Flow + + fun acknowledgeCompletion(userWalletId: UserWalletId) + + suspend fun clearPendingFlag(userWalletId: UserWalletId) + + suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List + + suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) +} \ No newline at end of file diff --git a/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/AcknowledgeAssetsDiscoveryCompletionUseCase.kt b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/AcknowledgeAssetsDiscoveryCompletionUseCase.kt new file mode 100644 index 0000000000..2ad99c87d0 --- /dev/null +++ b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/AcknowledgeAssetsDiscoveryCompletionUseCase.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.assetsdiscovery.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository + +class AcknowledgeAssetsDiscoveryCompletionUseCase( + private val assetsDiscoveryRepository: AssetsDiscoveryRepository, +) { + + operator fun invoke(userWalletId: UserWalletId) { + assetsDiscoveryRepository.acknowledgeCompletion(userWalletId) + } +} \ No newline at end of file diff --git a/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/ObserveAssetsDiscoveryUseCase.kt b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/ObserveAssetsDiscoveryUseCase.kt new file mode 100644 index 0000000000..1ee927739c --- /dev/null +++ b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/ObserveAssetsDiscoveryUseCase.kt @@ -0,0 +1,15 @@ +package com.tangem.domain.assetsdiscovery.usecase + +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress +import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository +import kotlinx.coroutines.flow.Flow + +class ObserveAssetsDiscoveryUseCase( + private val assetsDiscoveryRepository: AssetsDiscoveryRepository, +) { + + operator fun invoke(userWalletId: UserWalletId): Flow { + return assetsDiscoveryRepository.observeDiscoveryProgress(userWalletId) + } +} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/StartAssetsDiscoveryUseCase.kt similarity index 71% rename from domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt rename to domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/StartAssetsDiscoveryUseCase.kt index d0a1efdfa5..11df5ba962 100644 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/StartTokenSyncUseCase.kt +++ b/domain/assetsdiscovery/src/main/kotlin/com/tangem/domain/assetsdiscovery/usecase/StartAssetsDiscoveryUseCase.kt @@ -1,18 +1,18 @@ -package com.tangem.domain.tokensync.usecase +package com.tangem.domain.assetsdiscovery.usecase import arrow.core.Either import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase import com.tangem.domain.models.account.AccountId import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokensync.repository.TokenSyncRepository +import com.tangem.domain.assetsdiscovery.repository.AssetsDiscoveryRepository import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.Job import kotlinx.coroutines.launch import java.util.concurrent.ConcurrentHashMap -class StartTokenSyncUseCase( - private val tokenSyncRepository: TokenSyncRepository, +class StartAssetsDiscoveryUseCase( + private val assetsDiscoveryRepository: AssetsDiscoveryRepository, private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase, private val appCoroutineScope: AppCoroutineScope, ) { @@ -23,9 +23,9 @@ class StartTokenSyncUseCase( activeSyncJobs[userWalletId]?.cancel() activeSyncJobs[userWalletId] = appCoroutineScope.launch { try { - tokenSyncRepository.runSync(userWalletId) + assetsDiscoveryRepository.runDiscovery(userWalletId) applyDiscoveredTokens(userWalletId) - tokenSyncRepository.completeSync(userWalletId) + assetsDiscoveryRepository.completeDiscovery(userWalletId) } catch (e: Exception) { TangemLogger.e("Token sync failed for wallet: $userWalletId", e) } finally { @@ -36,18 +36,18 @@ class StartTokenSyncUseCase( suspend fun cancel(userWalletId: UserWalletId): Either = Either.catch { activeSyncJobs.remove(userWalletId)?.cancel() - tokenSyncRepository.clearPendingFlag(userWalletId) - tokenSyncRepository.clearDiscoveredTokens(userWalletId) + assetsDiscoveryRepository.clearPendingFlag(userWalletId) + assetsDiscoveryRepository.clearDiscoveredTokens(userWalletId) } - fun applyPendingSyncs() { + fun applyPendingAssetsDiscovery() { appCoroutineScope.launch { try { - val pendingIds = tokenSyncRepository.getPendingSyncWalletIds() + val pendingIds = assetsDiscoveryRepository.getPendingDiscoveryWalletIds() for (walletId in pendingIds) { val isApplied = applyDiscoveredTokens(walletId) if (isApplied) { - tokenSyncRepository.clearPendingFlag(walletId) + assetsDiscoveryRepository.clearPendingFlag(walletId) } } } catch (e: Exception) { @@ -57,7 +57,7 @@ class StartTokenSyncUseCase( } private suspend fun applyDiscoveredTokens(userWalletId: UserWalletId): Boolean { - val currencies = tokenSyncRepository.getDiscoveredCurrencies(userWalletId) + val currencies = assetsDiscoveryRepository.getDiscoveredCurrencies(userWalletId) if (currencies.isEmpty()) return true @@ -67,7 +67,7 @@ class StartTokenSyncUseCase( add = currencies, ).fold( ifRight = { - tokenSyncRepository.clearDiscoveredTokens(userWalletId) + assetsDiscoveryRepository.clearDiscoveredTokens(userWalletId) true }, ifLeft = { error -> diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt deleted file mode 100644 index b56da08053..0000000000 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/repository/TokenSyncRepository.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.tangem.domain.tokensync.repository - -import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokensync.model.TokenSyncProgress -import kotlinx.coroutines.flow.Flow - -interface TokenSyncRepository { - - suspend fun runSync(userWalletId: UserWalletId) - - suspend fun completeSync(userWalletId: UserWalletId) - - suspend fun getPendingSyncWalletIds(): List - - fun observeSyncProgress(userWalletId: UserWalletId): Flow - - fun acknowledgeCompletion(userWalletId: UserWalletId) - - suspend fun clearPendingFlag(userWalletId: UserWalletId) - - suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List - - suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) -} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/AcknowledgeTokenSyncCompletionUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/AcknowledgeTokenSyncCompletionUseCase.kt deleted file mode 100644 index 35be72c523..0000000000 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/AcknowledgeTokenSyncCompletionUseCase.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.domain.tokensync.usecase - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokensync.repository.TokenSyncRepository - -class AcknowledgeTokenSyncCompletionUseCase( - private val tokenSyncRepository: TokenSyncRepository, -) { - - operator fun invoke(userWalletId: UserWalletId) { - tokenSyncRepository.acknowledgeCompletion(userWalletId) - } -} \ No newline at end of file diff --git a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/ObserveTokenSyncUseCase.kt b/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/ObserveTokenSyncUseCase.kt deleted file mode 100644 index cddfbb9d7f..0000000000 --- a/domain/tokensync/src/main/java/com/tangem/domain/tokensync/usecase/ObserveTokenSyncUseCase.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.domain.tokensync.usecase - -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.tokensync.model.TokenSyncProgress -import com.tangem.domain.tokensync.repository.TokenSyncRepository -import kotlinx.coroutines.flow.Flow - -class ObserveTokenSyncUseCase( - private val tokenSyncRepository: TokenSyncRepository, -) { - - operator fun invoke(userWalletId: UserWalletId): Flow { - return tokenSyncRepository.observeSyncProgress(userWalletId) - } -} \ No newline at end of file diff --git a/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt index 04112d2530..af4b9462bb 100644 --- a/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -12,7 +12,6 @@ import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult import com.tangem.blockchain.nft.models.NFTAsset import com.tangem.blockchain.nft.models.NFTCollection -import com.tangem.blockchain.tokenbalance.models.TokenBalance import com.tangem.blockchainsdk.models.UpdateWalletManagerResult import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network @@ -282,8 +281,6 @@ interface WalletManagersFacade { suspend fun getNFTExploreUrl(network: Network, assetIdentifier: NFTAsset.Identifier): String? - suspend fun getTokenBalances(userWalletId: UserWalletId, network: Network): List - /** * If wallet manager implements [InitializableAccount] then returns [InitializableAccount.isAccountInitialized] * value. Otherwise always return true diff --git a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt index 552842c6e5..f8cde8a2e6 100644 --- a/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt +++ b/features/details/impl/src/main/kotlin/com/tangem/features/details/component/preview/PreviewDetailsComponent.kt @@ -22,7 +22,7 @@ internal class PreviewDetailsComponent : DetailsComponent { router = DummyRouter(), hotWalletFeatureToggles = object : HotWalletFeatureToggles { override val isWalletCreationRestrictionEnabled: Boolean = true - override val isTokenSyncEnabled: Boolean = true + override val isAssetsDiscoveryEnabled: Boolean = true }, ).buildAll( isWalletConnectAvailable = true, diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt index 84d2ec41e4..aeb22534d7 100644 --- a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletFeatureToggles.kt @@ -2,5 +2,5 @@ package com.tangem.features.hotwallet interface HotWalletFeatureToggles { val isWalletCreationRestrictionEnabled: Boolean - val isTokenSyncEnabled: Boolean + val isAssetsDiscoveryEnabled: Boolean } \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index 0daf9f2d1e..9d2eadef6e 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { implementation(projects.domain.feedback) implementation(projects.domain.feedback.models) implementation(projects.domain.hotWallet) - implementation(projects.domain.tokensync) + implementation(projects.domain.assetsdiscovery) /** Common */ implementation(projects.common.ui) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt index 0eefa161bd..bff219fd75 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/DefaultHotWalletFeatureToggles.kt @@ -10,6 +10,6 @@ internal class DefaultHotWalletFeatureToggles( override val isWalletCreationRestrictionEnabled: Boolean get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.HOT_WALLET_CREATION_RESTRICTION_ENABLED) - override val isTokenSyncEnabled: Boolean - get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.TOKEN_SYNC_ENABLED) + override val isAssetsDiscoveryEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.ASSETS_DISCOVERY_ENABLED) } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt index c71cf787d8..0a4666a24b 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -11,7 +11,7 @@ import com.tangem.core.ui.extensions.wrappedList import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.settings.CanUseBiometryUseCase -import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase +import com.tangem.domain.assetsdiscovery.usecase.StartAssetsDiscoveryUseCase import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS @@ -39,7 +39,7 @@ internal class HotAccessCodeRequestModel @Inject constructor( private val userWalletsListRepository: UserWalletsListRepository, private val canUseBiometryUseCase: CanUseBiometryUseCase, private val analyticsEventHandler: AnalyticsEventHandler, - private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { @@ -220,8 +220,8 @@ internal class HotAccessCodeRequestModel @Inject constructor( val userWallet = userWalletsListRepository.userWalletsSync() .firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return - if (hotWalletFeatureToggles.isTokenSyncEnabled) { - startTokenSyncUseCase.cancel(userWallet.walletId) + if (hotWalletFeatureToggles.isAssetsDiscoveryEnabled) { + startAssetsDiscoveryUseCase.cancel(userWallet.walletId) } userWalletsListRepository.delete(listOf(userWallet.walletId)) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index ed40d1e958..ce2c9cb8f9 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -17,7 +17,7 @@ import com.tangem.core.ui.message.bottomSheetMessage import com.tangem.crypto.bip39.Mnemonic import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.common.wallets.error.SaveWalletError -import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase +import com.tangem.domain.assetsdiscovery.usecase.StartAssetsDiscoveryUseCase import com.tangem.domain.wallets.builder.HotUserWalletBuilder import com.tangem.domain.wallets.usecase.SaveWalletUseCase import com.tangem.features.hotwallet.HotWalletFeatureToggles @@ -42,7 +42,7 @@ internal class AddExistingWalletImportModel @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, - private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, private val analyticsEventHandler: AnalyticsEventHandler, @@ -114,8 +114,8 @@ internal class AddExistingWalletImportModel @Inject constructor( .onRight { setImportProgress(false) - if (hotWalletFeatureToggles.isTokenSyncEnabled) { - startTokenSyncUseCase(userWallet.walletId) + if (hotWalletFeatureToggles.isAssetsDiscoveryEnabled) { + startAssetsDiscoveryUseCase(userWallet.walletId) } analyticsEventHandler.send( diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt index fa801e998e..fea68a0a31 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/forgetwallet/ForgetWalletModel.kt @@ -12,7 +12,7 @@ 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.SnackbarMessage -import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase +import com.tangem.domain.assetsdiscovery.usecase.StartAssetsDiscoveryUseCase import com.tangem.domain.wallets.usecase.DeleteWalletUseCase import com.tangem.features.hotwallet.ForgetWalletComponent import com.tangem.features.hotwallet.HotWalletFeatureToggles @@ -33,7 +33,7 @@ internal class ForgetWalletModel @Inject constructor( private val router: Router, private val deleteWalletUseCase: DeleteWalletUseCase, private val uiMessageSender: UiMessageSender, - private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { @@ -84,8 +84,8 @@ internal class ForgetWalletModel @Inject constructor( private fun forgetWallet() { modelScope.launch { - if (hotWalletFeatureToggles.isTokenSyncEnabled) { - startTokenSyncUseCase.cancel(params.userWalletId) + if (hotWalletFeatureToggles.isAssetsDiscoveryEnabled) { + startAssetsDiscoveryUseCase.cancel(params.userWalletId) } val hasUserWallets = deleteWalletUseCase(params.userWalletId) diff --git a/features/wallet-settings/impl/build.gradle.kts b/features/wallet-settings/impl/build.gradle.kts index ec188e4f5e..51e2d587f8 100644 --- a/features/wallet-settings/impl/build.gradle.kts +++ b/features/wallet-settings/impl/build.gradle.kts @@ -50,7 +50,7 @@ dependencies { implementation(projects.domain.settings) implementation(projects.domain.notifications.models) implementation(projects.domain.notifications) - implementation(projects.domain.tokensync) + implementation(projects.domain.assetsdiscovery) /* AndroidX */ implementation(deps.androidx.fragment.ktx) diff --git a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt index 4391d1ca87..f40b8d6490 100644 --- a/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt +++ b/features/wallet-settings/impl/src/main/kotlin/com/tangem/feature/walletsettings/model/WalletSettingsModel.kt @@ -37,7 +37,7 @@ import com.tangem.domain.nft.EnableWalletNFTUseCase import com.tangem.domain.nft.GetWalletNFTEnabledUseCase import com.tangem.domain.notifications.repository.NotificationsRepository import com.tangem.domain.settings.repositories.PermissionRepository -import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase +import com.tangem.domain.assetsdiscovery.usecase.StartAssetsDiscoveryUseCase import com.tangem.domain.wallets.analytics.Settings import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction @@ -90,7 +90,7 @@ internal class WalletSettingsModel @Inject constructor( private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase, private val singleAccountListSupplier: SingleAccountListSupplier, private val accountListSortingSaver: AccountListSortingSaver, - private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) : Model() { @@ -256,8 +256,8 @@ internal class WalletSettingsModel @Inject constructor( val userWallet = getUserWalletUseCase(params.userWalletId) .getOrNull() - if (userWallet is UserWallet.Hot && hotWalletFeatureToggles.isTokenSyncEnabled) { - startTokenSyncUseCase.cancel(params.userWalletId) + if (userWallet is UserWallet.Hot && hotWalletFeatureToggles.isAssetsDiscoveryEnabled) { + startAssetsDiscoveryUseCase.cancel(params.userWalletId) } val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { error -> diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index d612025527..70e66ddf64 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -125,7 +125,7 @@ dependencies { implementation(projects.domain.yieldSupply.models) implementation(projects.domain.appTheme) implementation(projects.domain.appTheme.models) - implementation(projects.domain.tokensync) + implementation(projects.domain.assetsdiscovery) /** Feature Apis */ implementation(projects.features.details.api) 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 0c5b95efcd..b1e648a65f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -38,7 +38,7 @@ import com.tangem.domain.settings.* import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.walletconnect.WcPairService import com.tangem.domain.walletconnect.model.WcPairRequest -import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase +import com.tangem.domain.assetsdiscovery.usecase.StartAssetsDiscoveryUseCase import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.domain.wallets.usecase.* import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase @@ -126,7 +126,7 @@ internal class WalletModel @Inject constructor( private val tangemPayFeatureToggles: TangemPayFeatureToggles, private val uiMessageSender: UiMessageSender, private val hotWalletFeatureToggles: HotWalletFeatureToggles, - private val startTokenSyncUseCase: StartTokenSyncUseCase, + private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase, val screenLifecycleProvider: ScreenLifecycleProvider, val innerWalletRouter: InnerWalletRouter, ) : Model() { @@ -159,7 +159,7 @@ internal class WalletModel @Inject constructor( subscribeTangemPayOnWalletState() subscribeToMainScreenQrScanning() enableNotificationsIfNeeded() - applyPendingTokenSyncs() + applyPendingAssetsDiscovery() clickIntents.initialize(innerWalletRouter, modelScope) @@ -840,9 +840,9 @@ internal class WalletModel @Inject constructor( } } - private fun applyPendingTokenSyncs() { - if (hotWalletFeatureToggles.isTokenSyncEnabled) { - startTokenSyncUseCase.applyPendingSyncs() + private fun applyPendingAssetsDiscovery() { + if (hotWalletFeatureToggles.isAssetsDiscoveryEnabled) { + startAssetsDiscoveryUseCase.applyPendingAssetsDiscovery() } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt index 665908c863..f3642a626e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletWarningsClickIntents.kt @@ -40,7 +40,7 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked import com.tangem.domain.tokens.model.details.NavigationAction -import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase +import com.tangem.domain.assetsdiscovery.usecase.AcknowledgeAssetsDiscoveryCompletionUseCase import com.tangem.domain.wallets.usecase.* import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic @@ -97,9 +97,9 @@ internal interface WalletWarningsClickIntents { fun onCloseUpgradeBannerClick(userWalletId: UserWalletId) - fun onDismissTokenSyncNotification(userWalletId: UserWalletId) + fun onDismissAssetsDiscoveryNotification(userWalletId: UserWalletId) - fun onTokenSyncManageClick(userWalletId: UserWalletId) + fun onAssetsDiscoveryManageClick(userWalletId: UserWalletId) } @Suppress("LargeClass", "LongParameterList", "TooManyFunctions") @@ -132,7 +132,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( private val uiMessageSender: UiMessageSender, private val reviewManager: ReviewManager, private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase, - private val acknowledgeTokenSyncCompletionUseCase: AcknowledgeTokenSyncCompletionUseCase, + private val acknowledgeAssetsDiscoveryCompletionUseCase: AcknowledgeAssetsDiscoveryCompletionUseCase, ) : BaseWalletClickIntents(), WalletWarningsClickIntents { override fun onAddBackupCardClick() { @@ -508,12 +508,12 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( } } - override fun onDismissTokenSyncNotification(userWalletId: UserWalletId) { - acknowledgeTokenSyncCompletionUseCase(userWalletId) + override fun onDismissAssetsDiscoveryNotification(userWalletId: UserWalletId) { + acknowledgeAssetsDiscoveryCompletionUseCase(userWalletId) } - override fun onTokenSyncManageClick(userWalletId: UserWalletId) { - acknowledgeTokenSyncCompletionUseCase(userWalletId) + override fun onAssetsDiscoveryManageClick(userWalletId: UserWalletId) { + acknowledgeAssetsDiscoveryCompletionUseCase(userWalletId) router.openManageTokensScreen( AccountId.forMainCryptoPortfolio(userWalletId), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 96f7c46639..4c57e85428 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -120,7 +120,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.TangemPayRefreshNeeded -> null is WalletNotification.Warning.TangemPayUnreachable -> null is WalletNotification.UpgradeHotWalletPromo -> null - is WalletNotification.TokenSyncCompleted -> null + is WalletNotification.AssetsDiscoveryCompleted -> null is WalletNotification.CreateTangemPayAccount -> null } } 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 f0d03bf32a..4dc9493025 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 @@ -27,8 +27,8 @@ 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.tokensync.model.TokenSyncProgress -import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase +import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress +import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType @@ -65,7 +65,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase, private val getUpgradeBannerClosureTimestampUseCase: GetUpgradeBannerClosureTimestampUseCase, private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase, - private val observeTokenSyncUseCase: ObserveTokenSyncUseCase, + private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, ) { @@ -75,11 +75,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val params = SingleAccountStatusListProducer.Params(userWallet.walletId) val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params) - val tokenSyncProgressFlow = if (hotWalletFeatureToggles.isTokenSyncEnabled && userWallet is UserWallet.Hot) { - observeTokenSyncUseCase(userWallet.walletId).distinctUntilChanged() - } else { - flowOf(TokenSyncProgress.Idle) - } + val assetsDiscoveryProgressFlow = + if (hotWalletFeatureToggles.isAssetsDiscoveryEnabled && userWallet is UserWallet.Hot) { + observeAssetsDiscoveryUseCase(userWallet.walletId).distinctUntilChanged() + } else { + flowOf(AssetsDiscoveryProgress.Idle) + } return combine( accountStatusListFlow, @@ -96,7 +97,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( .distinctUntilChanged(), getUpgradeBannerClosureTimestampUseCase(userWallet.walletId) .distinctUntilChanged(), - tokenSyncProgressFlow, + assetsDiscoveryProgressFlow, ) { array -> array } .map { array -> val accountStatusList = array[0] as AccountStatusList @@ -108,7 +109,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( val shouldShowYieldPromo = array[6] as Boolean val shouldShowUpgradeBanner = array[7] as Boolean val closureTimestamp = array[8] as? Long - val tokenSyncProgress = array[9] as TokenSyncProgress + val assetsDiscoveryProgress = array[9] as AssetsDiscoveryProgress val flattenCurrencies = accountStatusList.flattenCurrencies() val paymentAccountStatus = accountStatusList.accountStatuses @@ -153,9 +154,9 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( clickIntents = clickIntents, ) - addTokenSyncCompletedNotification( + addAssetsDiscoveryCompletedNotification( userWallet = userWallet, - tokenSyncProgress = tokenSyncProgress, + assetsDiscoveryProgress = assetsDiscoveryProgress, clickIntents = clickIntents, ) @@ -404,17 +405,17 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( // } // } - private fun MutableList.addTokenSyncCompletedNotification( + private fun MutableList.addAssetsDiscoveryCompletedNotification( userWallet: UserWallet, - tokenSyncProgress: TokenSyncProgress, + assetsDiscoveryProgress: AssetsDiscoveryProgress, clickIntents: WalletClickIntents, ) { addIf( - element = WalletNotification.TokenSyncCompleted( - onCloseClick = { clickIntents.onDismissTokenSyncNotification(userWallet.walletId) }, - onManageTokensClick = { clickIntents.onTokenSyncManageClick(userWallet.walletId) }, + element = WalletNotification.AssetsDiscoveryCompleted( + onCloseClick = { clickIntents.onDismissAssetsDiscoveryNotification(userWallet.walletId) }, + onManageTokensClick = { clickIntents.onAssetsDiscoveryManageClick(userWallet.walletId) }, ), - condition = tokenSyncProgress is TokenSyncProgress.Completed, + condition = assetsDiscoveryProgress is AssetsDiscoveryProgress.Completed, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt index 838ea96770..b131f971d8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/WalletAdditionalInfoFactory.kt @@ -10,7 +10,7 @@ import com.tangem.domain.card.common.util.cardTypesResolver import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.impl.R -import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM +import com.tangem.feature.wallet.presentation.wallet.state.model.AssetsDiscoveryProgressUM import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo import java.math.BigDecimal @@ -32,7 +32,7 @@ internal object WalletAdditionalInfoFactory { fun resolve( wallet: UserWallet, currencyAmount: BigDecimal? = null, - syncProgress: TokenSyncProgressUM = TokenSyncProgressUM.Idle, + syncProgress: AssetsDiscoveryProgressUM = AssetsDiscoveryProgressUM.Idle, ): WalletAdditionalInfo { return when (wallet) { is UserWallet.Cold -> { @@ -46,8 +46,8 @@ internal object WalletAdditionalInfoFactory { } } - private fun UserWallet.Hot.resolveAdditionalInfo(syncProgress: TokenSyncProgressUM): WalletAdditionalInfo { - val content = if (syncProgress is TokenSyncProgressUM.InProgress) { + private fun UserWallet.Hot.resolveAdditionalInfo(syncProgress: AssetsDiscoveryProgressUM): WalletAdditionalInfo { + val content = if (syncProgress is AssetsDiscoveryProgressUM.InProgress) { WalletAdditionalInfo.Content.SyncProgress(syncProgress.progressPercent) } else { WalletAdditionalInfo.Content.Text( 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 045a9c47bf..b6ea0fcf2a 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 @@ -18,7 +18,7 @@ internal class MultiWalletContentLoader @AssistedInject constructor( private val walletNotificationsSubscriberFactory: WalletNotificationsSubscriber.Factory, private val multiWalletActionButtonsSubscriberFactory: MultiWalletActionButtonsSubscriber.Factory, private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory, - private val tokenSyncSubscriberFactory: TokenSyncSubscriber.Factory, + private val assetsDiscoverySubscriberFactory: AssetsDiscoverySubscriber.Factory, private val tokenListAnalyticsSubscriberFactory: TokenListAnalyticsSubscriber.Factory, private val designFeatureToggles: DesignFeatureToggles, private val hotWalletFeatureToggles: HotWalletFeatureToggles, @@ -36,8 +36,8 @@ internal class MultiWalletContentLoader @AssistedInject constructor( multiWalletActionButtonsSubscriberFactory.create(userWallet), tangemPayMainSubscriberFactory.create(userWallet), tokenListAnalyticsSubscriberFactory.create(userWallet), - if (hotWalletFeatureToggles.isTokenSyncEnabled && userWallet is UserWallet.Hot) { - tokenSyncSubscriberFactory.create(userWallet) + if (hotWalletFeatureToggles.isAssetsDiscoveryEnabled && userWallet is UserWallet.Hot) { + assetsDiscoverySubscriberFactory.create(userWallet) } else { null }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/AssetsDiscoveryProgressUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/AssetsDiscoveryProgressUM.kt new file mode 100644 index 0000000000..e66ccd7258 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/AssetsDiscoveryProgressUM.kt @@ -0,0 +1,13 @@ +package com.tangem.feature.wallet.presentation.wallet.state.model + +import androidx.compose.runtime.Immutable + +@Immutable +internal sealed class AssetsDiscoveryProgressUM { + + data object Idle : AssetsDiscoveryProgressUM() + + data class InProgress(val progressPercent: Int) : AssetsDiscoveryProgressUM() + + data object Completed : AssetsDiscoveryProgressUM() +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt deleted file mode 100644 index 2bcd1e9487..0000000000 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/TokenSyncProgressUM.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.feature.wallet.presentation.wallet.state.model - -import androidx.compose.runtime.Immutable - -@Immutable -internal sealed class TokenSyncProgressUM { - - data object Idle : TokenSyncProgressUM() - - data class InProgress(val progressPercent: Int) : TokenSyncProgressUM() - - data object Completed : TokenSyncProgressUM() -} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index c960e5ba67..ecdd0aa9e5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -435,7 +435,7 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) - data class TokenSyncCompleted( + data class AssetsDiscoveryCompleted( val onCloseClick: () -> Unit, val onManageTokensClick: () -> Unit, ) : WalletNotification( 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 3903da422a..82b4da90cc 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 @@ -27,7 +27,7 @@ internal sealed interface WalletState : WalletStateHolder { abstract val tangemPayState: TangemPayState abstract val tangemPayMainUM: TangemPayMainUM abstract val isTangemPayRefactorEnabled: Boolean // TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED - abstract val tokenSyncProgressUM: TokenSyncProgressUM + abstract val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM data class Content( override val pullToRefreshConfig: PullToRefreshConfig, @@ -41,7 +41,7 @@ internal sealed interface WalletState : WalletStateHolder { override val tangemPayState: TangemPayState, override val tangemPayMainUM: TangemPayMainUM, override val isTangemPayRefactorEnabled: Boolean, - override val tokenSyncProgressUM: TokenSyncProgressUM = TokenSyncProgressUM.Idle, + override val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM = AssetsDiscoveryProgressUM.Idle, ) : MultiCurrency() data class Locked( @@ -63,7 +63,7 @@ internal sealed interface WalletState : WalletStateHolder { override val tangemPayState: TangemPayState = TangemPayState.Empty override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty override val isTangemPayRefactorEnabled: Boolean = false - override val tokenSyncProgressUM: TokenSyncProgressUM = TokenSyncProgressUM.Idle + override val assetsDiscoveryProgressUM: AssetsDiscoveryProgressUM = AssetsDiscoveryProgressUM.Idle } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetAssetsDiscoveryProgressTransformer.kt similarity index 88% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetAssetsDiscoveryProgressTransformer.kt index 0dfbaf6dd9..e5dbe43b05 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenSyncProgressTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetAssetsDiscoveryProgressTransformer.kt @@ -2,21 +2,21 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers import com.tangem.domain.models.wallet.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory -import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM +import com.tangem.feature.wallet.presentation.wallet.state.model.AssetsDiscoveryProgressUM 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 -internal class SetTokenSyncProgressTransformer( +internal class SetAssetsDiscoveryProgressTransformer( private val userWallet: UserWallet, - private val progress: TokenSyncProgressUM, + private val progress: AssetsDiscoveryProgressUM, ) : WalletStateTransformer(userWallet.walletId) { override fun transform(prevState: WalletState): WalletState { return when (prevState) { is WalletState.MultiCurrency.Content -> prevState.copy( walletCardState = updateCardState(prevState.walletCardState), - tokenSyncProgressUM = progress, + assetsDiscoveryProgressUM = progress, ) else -> prevState } 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 ef5698e378..06d71e8d28 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 @@ -61,7 +61,7 @@ internal class SetTokenListErrorTransformer( walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState(), tokensListUM = WalletTokensListUM.Empty( onEmptyClick = { - clickIntents.onTokenSyncManageClick(walletUM.walletsBalanceUM.id) + clickIntents.onAssetsDiscoveryManageClick(walletUM.walletsBalanceUM.id) }, ), buttons = walletUM.disableButtons(), 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 35b381f033..f3d9013ff0 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 @@ -123,7 +123,7 @@ internal class SetTokenListTransformer( if (params !is TokenConverterParams.Account) { return WalletTokensListUM.Empty( onEmptyClick = { - clickIntents.onTokenSyncManageClick(userWallet.walletId) + clickIntents.onAssetsDiscoveryManageClick(userWallet.walletId) }, ) } 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 86cc55373e..19d722f039 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 @@ -4,7 +4,7 @@ import com.tangem.domain.card.common.util.getCardsCount import com.tangem.domain.models.wallet.UserWallet 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.TokenSyncProgressUM +import com.tangem.feature.wallet.presentation.wallet.state.model.AssetsDiscoveryProgressUM 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 @@ -19,7 +19,7 @@ internal class UpdateWalletCardsCountTransformer( return when (prevState) { is WalletState.MultiCurrency.Content -> { prevState.copy( - walletCardState = prevState.walletCardState.toUpdatedState(prevState.tokenSyncProgressUM), + walletCardState = prevState.walletCardState.toUpdatedState(prevState.assetsDiscoveryProgressUM), ) } is WalletState.SingleCurrency.Content -> { @@ -39,7 +39,7 @@ internal class UpdateWalletCardsCountTransformer( } private fun WalletCardState.toUpdatedState( - syncProgress: TokenSyncProgressUM = TokenSyncProgressUM.Idle, + syncProgress: AssetsDiscoveryProgressUM = AssetsDiscoveryProgressUM.Idle, ): WalletCardState { return when (this) { is WalletCardState.Content -> copy( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AssetsDiscoverySubscriber.kt similarity index 56% rename from features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt rename to features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AssetsDiscoverySubscriber.kt index 1095805674..ba021b62af 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/TokenSyncSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/AssetsDiscoverySubscriber.kt @@ -1,11 +1,11 @@ package com.tangem.feature.wallet.presentation.wallet.subscribers import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.tokensync.model.TokenSyncProgress -import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase +import com.tangem.domain.assetsdiscovery.model.AssetsDiscoveryProgress +import com.tangem.domain.assetsdiscovery.usecase.ObserveAssetsDiscoveryUseCase import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController -import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM -import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenSyncProgressTransformer +import com.tangem.feature.wallet.presentation.wallet.state.model.AssetsDiscoveryProgressUM +import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetAssetsDiscoveryProgressTransformer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -13,25 +13,25 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.onEach -internal class TokenSyncSubscriber @AssistedInject constructor( +internal class AssetsDiscoverySubscriber @AssistedInject constructor( @Assisted private val userWallet: UserWallet, private val stateController: WalletStateController, - private val observeTokenSyncUseCase: ObserveTokenSyncUseCase, + private val observeAssetsDiscoveryUseCase: ObserveAssetsDiscoveryUseCase, ) : WalletSubscriber() { override fun create(coroutineScope: CoroutineScope): Flow<*> { - return observeTokenSyncUseCase(userWallet.walletId) + return observeAssetsDiscoveryUseCase(userWallet.walletId) .onEach { current -> handleProgress(userWallet, current) } } - private fun handleProgress(userWallet: UserWallet, current: TokenSyncProgress) { + private fun handleProgress(userWallet: UserWallet, current: AssetsDiscoveryProgress) { val progressUM = when (current) { - is TokenSyncProgress.InProgress -> TokenSyncProgressUM.InProgress(current.progressPercent) - is TokenSyncProgress.Completed -> TokenSyncProgressUM.Completed - is TokenSyncProgress.Idle -> TokenSyncProgressUM.Idle + is AssetsDiscoveryProgress.InProgress -> AssetsDiscoveryProgressUM.InProgress(current.progressPercent) + is AssetsDiscoveryProgress.Completed -> AssetsDiscoveryProgressUM.Completed + is AssetsDiscoveryProgress.Idle -> AssetsDiscoveryProgressUM.Idle } stateController.update( - SetTokenSyncProgressTransformer( + SetAssetsDiscoveryProgressTransformer( userWallet = userWallet, progress = progressUM, ), @@ -40,6 +40,6 @@ internal class TokenSyncSubscriber @AssistedInject constructor( @AssistedFactory interface Factory { - fun create(userWallet: UserWallet): TokenSyncSubscriber + fun create(userWallet: UserWallet): AssetsDiscoverySubscriber } } \ No newline at end of file diff --git a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt index ffe4eec03d..cffad48379 100644 --- a/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt +++ b/libs/blockchain-sdk/src/main/java/com/tangem/blockchainsdk/BlockchainSDKFactory.kt @@ -1,5 +1,6 @@ package com.tangem.blockchainsdk +import com.tangem.blockchain.assetsdiscovery.AssetsDiscoveryServiceFactory import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.memo.MemoValidatorFactory @@ -18,4 +19,7 @@ interface BlockchainSDKFactory { /** Get [MemoValidatorFactory] synchronously */ suspend fun getMemoValidatorFactorySync(): MemoValidatorFactory? + + /** Get [AssetsDiscoveryServiceFactory] synchronously */ + suspend fun getAssetsDiscoveryServiceFactorySync(): AssetsDiscoveryServiceFactory? } \ No newline at end of file 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 ed8efd0ebe..f08da11c5a 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,5 +1,6 @@ package com.tangem.blockchainsdk +import com.tangem.blockchain.assetsdiscovery.AssetsDiscoveryServiceFactory import com.tangem.blockchain.common.BlockchainSdkConfig import com.tangem.blockchain.common.WalletManagerFactory import com.tangem.blockchain.common.memo.MemoValidatorFactory @@ -34,6 +35,8 @@ internal class DefaultBlockchainSDKFactory( private val walletManagerFactory: Flow = createWalletManagerFactory() private val memoValidatorFactory: Flow = createMemoValidatorFactory() + private val assetsDiscoveryServiceFactory: Flow = + createAssetsDiscoveryServiceFactory() override suspend fun init() { coroutineScope { @@ -45,6 +48,9 @@ internal class DefaultBlockchainSDKFactory( override suspend fun getMemoValidatorFactorySync(): MemoValidatorFactory? = memoValidatorFactory.firstOrNull() + override suspend fun getAssetsDiscoveryServiceFactorySync(): AssetsDiscoveryServiceFactory? = + assetsDiscoveryServiceFactory.firstOrNull() + private fun createWalletManagerFactory(): Flow { return combine( flow = flowOf(blockchainSdkConfig), @@ -56,6 +62,16 @@ internal class DefaultBlockchainSDKFactory( .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) } + private fun createAssetsDiscoveryServiceFactory(): Flow { + return combine( + flow = flowOf(blockchainSdkConfig), + flow2 = blockchainProvidersTypesManager.get(), + ) { config, providerTypes -> + AssetsDiscoveryServiceFactory(config = config, providerTypes = providerTypes) + } + .stateIn(scope = mainScope, started = SharingStarted.Eagerly, initialValue = null) + } + private fun createMemoValidatorFactory(): Flow { return combine( flow = flowOf(blockchainSdkConfig), diff --git a/settings.gradle.kts b/settings.gradle.kts index 9d4a5fcb46..35213a3a98 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -340,7 +340,7 @@ include(":domain:dynamic-addresses:models") include(":domain:settings") include(":domain:tokens") include(":domain:tokens:models") -include(":domain:tokensync") +include(":domain:assetsdiscovery") include(":domain:wallets") include(":domain:wallets:models") include(":domain:txhistory") @@ -409,7 +409,7 @@ include(":data:balance-hiding") include(":data:common") include(":data:card") include(":data:tokens") -include(":data:tokensync") +include(":data:assetsdiscovery") include(":data:settings") include(":data:txhistory") include(":data:wallets") From c83a46cc1ff099924b46dc3ae24a02cf06aa0906 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2026 13:28:12 +0200 Subject: [PATCH 17/32] Updated on 2026-08-14 --- .../marketprice/MarketPriceBlock.kt | 1 + .../marketprice/PriceChangeInPercent.kt | 129 +++++++++- .../marketprice/PriceChangeState.kt | 9 +- .../main/res/drawable/ic_alert_triange_24.xml | 10 + .../main/res/drawable/ic_chewron_up_20.xml | 9 + .../features/feed/model/search/SearchModel.kt | 14 ++ .../converter/UserAssetSearchItemConverter.kt | 130 ++++++++-- .../feed/ui/components/LayeringIcons.kt | 99 ++++++++ .../features/feed/ui/search/SearchContent.kt | 191 +++++++------- .../ui/search/components/BalanceColumn.kt | 175 +++++++++++++ .../search/components/GroupedUserAssetItem.kt | 84 +++++++ .../search/components/SingleUserAssetItem.kt | 117 +++++++++ .../ui/search/preview/SearchContentPreview.kt | 11 +- .../ui/search/preview/UserAssetItemPreview.kt | 238 ++++++++++++++++++ .../features/feed/ui/search/state/SearchUM.kt | 35 ++- .../utils/EntryContentAnimationTransitions.kt | 6 +- .../block/impl/ui/TokenMarketBlockLegacy.kt | 1 + 17 files changed, 1116 insertions(+), 143 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_alert_triange_24.xml create mode 100644 core/ui/src/main/res/drawable/ic_chewron_up_20.xml create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt create mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt index 92785ec693..dc99632334 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/MarketPriceBlock.kt @@ -121,6 +121,7 @@ private fun PriceBlock(state: MarketPriceBlockState, priceWidthDp: Dp) { PriceChangeInPercent( valueInPercent = marketPriceBlockState.priceChangeConfig.valueInPercent, type = marketPriceBlockState.priceChangeConfig.type, + textStyle = TangemTheme.typography.body2, ) } } else { diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt index a202d8bf21..c4882dcdc8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeInPercent.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector @@ -13,15 +14,43 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import com.tangem.core.ui.R +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview +import com.tangem.core.ui.res.TangemThemePreviewRedesign @Composable fun PriceChangeInPercent( valueInPercent: String, type: PriceChangeType, + textStyle: TextStyle, + modifier: Modifier = Modifier, + isDisabled: Boolean = false, +) { + if (LocalRedesignEnabled.current) { + PriceChangeInPercentV2( + modifier = modifier, + valueInPercent = valueInPercent, + type = type, + textStyle = textStyle, + isDisabled = isDisabled, + ) + } else { + PriceChangeInPercentV1( + modifier = modifier, + valueInPercent = valueInPercent, + type = type, + textStyle = textStyle, + ) + } +} + +@Composable +private fun PriceChangeInPercentV1( + valueInPercent: String, + type: PriceChangeType, + textStyle: TextStyle, modifier: Modifier = Modifier, - textStyle: TextStyle = TangemTheme.typography.body2, ) { if (valueInPercent.isBlank()) { Box(modifier) @@ -66,25 +95,87 @@ fun PriceChangeInPercent( } } +@Composable +private fun PriceChangeInPercentV2( + valueInPercent: String, + type: PriceChangeType, + textStyle: TextStyle, + isDisabled: Boolean, + modifier: Modifier = Modifier, +) { + if (valueInPercent.isBlank()) { + Box(modifier) + return + } + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x0_5), + ) { + Icon( + modifier = Modifier + .size(TangemTheme.dimens2.x3) + .align(Alignment.CenterVertically), + imageVector = ImageVector.vectorResource( + id = when (type) { + PriceChangeType.UP -> R.drawable.ic_arrow_up_8 + PriceChangeType.DOWN -> R.drawable.ic_arrow_down_8 + PriceChangeType.NEUTRAL -> R.drawable.ic_elipse_8 + }, + ), + tint = if (isDisabled) { + TangemTheme.colors2.graphic.neutral.tertiary + } else { + when (type) { + PriceChangeType.UP -> TangemTheme.colors2.markers.iconBlue + PriceChangeType.DOWN -> TangemTheme.colors2.markers.iconRed + PriceChangeType.NEUTRAL -> TangemTheme.colors2.markers.iconGray + } + }, + contentDescription = null, + ) + + Text( + text = valueInPercent, + color = if (isDisabled) { + TangemTheme.colors2.text.status.disabled + } else { + when (type) { + PriceChangeType.UP -> TangemTheme.colors2.text.status.accent + PriceChangeType.DOWN -> TangemTheme.colors2.text.status.warning + PriceChangeType.NEUTRAL -> TangemTheme.colors2.text.neutral.tertiary + } + }, + style = textStyle, + overflow = TextOverflow.Visible, + maxLines = 1, + ) + } +} + //region Preview @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun Preview() { +private fun PreviewV1() { TangemThemePreview { Column { PriceChangeInPercent( valueInPercent = "52.00%", type = PriceChangeType.NEUTRAL, + textStyle = TangemTheme.typography.body2, ) PriceChangeInPercent( valueInPercent = "52.00%", type = PriceChangeType.UP, + textStyle = TangemTheme.typography.body2, ) PriceChangeInPercent( valueInPercent = "52.00%", type = PriceChangeType.DOWN, + textStyle = TangemTheme.typography.body2, ) PriceChangeInPercent( valueInPercent = "52.00%", @@ -95,4 +186,38 @@ private fun Preview() { } } +@Preview +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +private fun PreviewV2() { + TangemThemePreviewRedesign { + CompositionLocalProvider(LocalRedesignEnabled provides true) { + Column { + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.NEUTRAL, + textStyle = TangemTheme.typography2.captionRegular12, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.UP, + textStyle = TangemTheme.typography2.captionRegular12, + isDisabled = true, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.DOWN, + textStyle = TangemTheme.typography2.captionRegular12, + isDisabled = false, + ) + PriceChangeInPercent( + valueInPercent = "52.00%", + type = PriceChangeType.DOWN, + textStyle = TangemTheme.typography2.captionRegular12, + ) + } + } + } +} + //endregion Preview \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt index 8db483f50c..768c0696cc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/marketprice/PriceChangeState.kt @@ -1,8 +1,11 @@ package com.tangem.core.ui.components.marketprice -sealed class PriceChangeState { +import androidx.compose.runtime.Immutable - data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState() +@Immutable +sealed interface PriceChangeState { - object Unknown : PriceChangeState() + data class Content(val valueInPercent: String, val type: PriceChangeType) : PriceChangeState + + data object Unknown : PriceChangeState } \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_alert_triange_24.xml b/core/ui/src/main/res/drawable/ic_alert_triange_24.xml new file mode 100644 index 0000000000..6442020ee2 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_alert_triange_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/core/ui/src/main/res/drawable/ic_chewron_up_20.xml b/core/ui/src/main/res/drawable/ic_chewron_up_20.xml new file mode 100644 index 0000000000..aeb0f01317 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_chewron_up_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt index f43e8c5766..c55b8f0ec8 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/SearchModel.kt @@ -1,6 +1,8 @@ package com.tangem.features.feed.model.search import arrow.core.getOrElse +import com.tangem.common.routing.AppRoute +import com.tangem.common.routing.AppRouter import com.tangem.common.ui.charts.state.MarketChartData import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.sorted @@ -19,6 +21,7 @@ import com.tangem.domain.markets.toSerializableParam import com.tangem.domain.search.usecase.ClearSearchHistoryUseCase import com.tangem.domain.search.usecase.GetSearchResultsUseCase import com.tangem.domain.search.usecase.SaveRecentSearchTokenUseCase +import com.tangem.domain.search.model.UserAssetSearchEntry import com.tangem.domain.search.usecase.SaveSearchQueryUseCase import com.tangem.features.feed.components.search.DefaultSearchComponent import com.tangem.features.feed.model.market.list.state.MarketsListUM @@ -58,6 +61,7 @@ internal class SearchModel @Inject constructor( private val saveRecentSearchTokenUseCase: SaveRecentSearchTokenUseCase, private val clearSearchHistoryUseCase: ClearSearchHistoryUseCase, private val getTokenPriceChartUseCase: GetTokenPriceChartUseCase, + private val appRouter: AppRouter, private val stateController: SearchStateController, ) : Model() { @@ -195,6 +199,15 @@ internal class SearchModel @Inject constructor( stateController.update(UpdateSearchBarQueryTransformer("")) } + private fun onSingleUserAssetClick(entry: UserAssetSearchEntry) { + appRouter.push( + AppRoute.CurrencyDetails( + userWalletId = entry.userWalletId, + currency = entry.currencyStatus.currency, + ), + ) + } + private fun subscribeToQueryChanges() { stateController.uiState .map { it.searchBar.query.trim() } @@ -231,6 +244,7 @@ internal class SearchModel @Inject constructor( val converter = UserAssetSearchItemConverter( appCurrency = appCurrency, isBalanceHidden = balanceHidden, + onSingleClick = ::onSingleUserAssetClick, ) searchResult.userAssets .map(converter::convert) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt index 4e742afdde..6f0134ca29 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/search/converter/UserAssetSearchItemConverter.kt @@ -1,23 +1,34 @@ package com.tangem.features.feed.model.search.converter import com.tangem.common.ui.account.toUM +import com.tangem.common.ui.markets.toMarketsListItemPriceAnnotated +import com.tangem.core.ui.components.currency.icon.CurrencyIconState 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.image.TangemIconUM +import com.tangem.core.ui.extensions.networkIconResId +import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format +import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.search.model.UserAssetSearchEntry import com.tangem.domain.search.model.UserAssetSearchItem +import com.tangem.features.feed.ui.search.state.BalanceDisplayState import com.tangem.features.feed.ui.search.state.UserAssetItemUM import com.tangem.utils.StringsSigns import com.tangem.utils.converter.Converter +import com.tangem.utils.extensions.orZero import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal internal class UserAssetSearchItemConverter( private val appCurrency: AppCurrency, private val isBalanceHidden: Boolean, + private val onSingleClick: (UserAssetSearchEntry) -> Unit, ) : Converter { override fun convert(value: UserAssetSearchItem): UserAssetItemUM { @@ -39,17 +50,56 @@ internal class UserAssetSearchItemConverter( tokenName = currency.name, tokenSymbol = currency.symbol, fiatRate = value.fiatRate?.format { fiat(appCurrency.code, appCurrency.symbol) }, - cryptoBalance = formatCryptoAmount(value.amount, currency.symbol, currency.decimals), - fiatBalance = formatFiatAmount(value.fiatAmount), + priceChangeState = when (value) { + is CryptoCurrencyStatus.Loading, + is CryptoCurrencyStatus.Unreachable, + is CryptoCurrencyStatus.MissedDerivation, + is CryptoCurrencyStatus.NoAmount, + -> PriceChangeState.Unknown + else -> PriceChangeState.Content( + type = PriceChangeType.fromBigDecimal(value.priceChange.orZero()), + valueInPercent = value.priceChange.format { percent() }, + ) + }, + balanceState = convertSingleBalanceState(value, currency.symbol, currency.decimals), isBalanceHidden = isBalanceHidden, - onClick = {}, + onClick = { onSingleClick(entry) }, ) } - private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped { - val totalFiat = item.entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } - val totalCrypto = item.entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO } + private fun convertSingleBalanceState( + value: CryptoCurrencyStatus.Value, + symbol: String, + decimals: Int, + ): BalanceDisplayState { + return when { + value is CryptoCurrencyStatus.Loading && value.amount != null -> + BalanceDisplayState.Flickering( + cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)), + fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated( + appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol, + ) ?: stringReference(StringsSigns.DASH_SIGN), + ) + value is CryptoCurrencyStatus.Loading -> BalanceDisplayState.Loading + value is CryptoCurrencyStatus.Unreachable -> BalanceDisplayState.Unreachable + value.isError && value.amount != null -> + BalanceDisplayState.Stale( + cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)), + fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated( + appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol, + ) ?: stringReference(StringsSigns.DASH_SIGN), + ) + value.isError -> BalanceDisplayState.Unreachable + else -> BalanceDisplayState.Loaded( + cryptoBalance = stringReference(formatCryptoAmount(value.amount, symbol, decimals)), + fiatBalance = value.fiatAmount?.toMarketsListItemPriceAnnotated( + appCurrencyCode = appCurrency.code, appCurrencySymbol = appCurrency.symbol, + ) ?: stringReference(StringsSigns.DASH_SIGN), + ) + } + } + private fun convertGrouped(item: UserAssetSearchItem.Grouped): UserAssetItemUM.Grouped { val firstCurrency = item.entries.first().currencyStatus.currency val children = item.entries.map { entry -> UserAssetItemUM.GroupedChild( @@ -57,36 +107,78 @@ internal class UserAssetSearchItemConverter( accountName = entry.accountName.toUM(), accountIcon = entry.accountIcon.value, accountColor = entry.accountIcon.color, - cryptoBalance = formatCryptoAmount( - entry.currencyStatus.value.amount, - entry.currencyStatus.currency.symbol, - entry.currencyStatus.currency.decimals, - ), - fiatBalance = formatFiatAmount(entry.currencyStatus.value.fiatAmount), + currencyStatus = entry.currencyStatus, ) }.toImmutableList() + val entryCurrencyStatus = item.entries.first().currencyStatus + return UserAssetItemUM.Grouped( id = "grouped_${item.tokenName}_${item.tokenSymbol}", icon = TangemIconUM.Currency( - currencyIconState = CryptoCurrencyToIconStateConverter().convert(item.entries.first().currencyStatus), + currencyIconState = CurrencyIconState.CoinIcon( + url = entryCurrencyStatus.currency.iconUrl, + fallbackResId = entryCurrencyStatus.currency.networkIconResId, + isGrayscale = entryCurrencyStatus.currency.network.isTestnet || entryCurrencyStatus.value.isError, + shouldShowCustomBadge = entryCurrencyStatus.currency.isCustom, + ), ), tokenName = item.tokenName, tokenSymbol = item.tokenSymbol, tokensCount = item.entries.size, - totalCryptoBalance = formatCryptoAmount(totalCrypto, firstCurrency.symbol, firstCurrency.decimals), - totalFiatBalance = formatFiatAmount(totalFiat), + balanceState = convertGroupedBalanceState(item.entries, firstCurrency.symbol, firstCurrency.decimals), isBalanceHidden = isBalanceHidden, children = children, onClick = {}, ) } + private fun convertGroupedBalanceState( + entries: List, + symbol: String, + decimals: Int, + ): BalanceDisplayState { + val hasAnyLoading = entries.any { it.currencyStatus.value is CryptoCurrencyStatus.Loading } + val hasAnyError = entries.any { it.currencyStatus.value.isError } + val hasAnyAmount = entries.any { it.currencyStatus.value.amount != null } + + val balance = when { + hasAnyLoading && !hasAnyAmount -> BalanceDisplayState.Loading + hasAnyLoading && hasAnyAmount -> computeGroupBalanceFlickering(entries, symbol, decimals) + hasAnyError && entries.size == 1 && !hasAnyAmount -> BalanceDisplayState.Unreachable + hasAnyError && entries.size > 1 -> computeGroupBalance(entries, symbol, decimals) + else -> computeGroupBalance(entries, symbol, decimals) + } + return balance + } + + private fun computeGroupBalance( + entries: List, + symbol: String, + decimals: Int, + ): BalanceDisplayState.Loaded { + val totalFiat = entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } + val totalCrypto = entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO } + return BalanceDisplayState.Loaded( + cryptoBalance = stringReference(formatCryptoAmount(totalCrypto, symbol, decimals)), + fiatBalance = totalFiat.toMarketsListItemPriceAnnotated(appCurrency.code, appCurrency.symbol), + ) + } + + private fun computeGroupBalanceFlickering( + entries: List, + symbol: String, + decimals: Int, + ): BalanceDisplayState.Flickering { + val totalFiat = entries.sumOf { it.currencyStatus.value.fiatAmount ?: BigDecimal.ZERO } + val totalCrypto = entries.sumOf { it.currencyStatus.value.amount ?: BigDecimal.ZERO } + return BalanceDisplayState.Flickering( + cryptoBalance = stringReference(formatCryptoAmount(totalCrypto, symbol, decimals)), + fiatBalance = totalFiat.toMarketsListItemPriceAnnotated(appCurrency.code, appCurrency.symbol), + ) + } + private fun formatCryptoAmount(amount: BigDecimal?, symbol: String, decimals: Int): String { return amount?.format { crypto(symbol, decimals) } ?: StringsSigns.DASH_SIGN } - - private fun formatFiatAmount(fiatAmount: BigDecimal?): String { - return fiatAmount?.format { fiat(appCurrency.code, appCurrency.symbol) } ?: StringsSigns.DASH_SIGN - } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt new file mode 100644 index 0000000000..1236344817 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/components/LayeringIcons.kt @@ -0,0 +1,99 @@ +package com.tangem.features.feed.ui.components + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +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.res.TangemColorPalette +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreview + +private const val MIN_STACKED_ICON_COUNT = 1 +private const val MAX_STACKED_ICON_COUNT = 3 +private const val FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 1 +private const val SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER = 2 +private const val MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER = 2 +private const val FIRST_BACK_LAYER_ICON_ALPHA = 0.4f +private const val SECOND_BACK_LAYER_ICON_ALPHA = 0.2f + +@Composable +fun LayeringIcons( + tangemIconUM: TangemIconUM, + modifier: Modifier = Modifier, + count: Int = MIN_STACKED_ICON_COUNT, + layerHorizontalShift: Dp = TangemTheme.dimens2.x1, + iconSize: Dp = TangemTheme.dimens2.x10, +) { + require(count > 0) + + val stackTrailingWidth = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER + + Box( + modifier = modifier.size( + width = iconSize + stackTrailingWidth, + height = iconSize, + ), + ) { + val baseIconModifier = Modifier + .align(Alignment.TopStart) + .size(iconSize) + + if (count >= MAX_STACKED_ICON_COUNT) { + TangemIcon( + tangemIconUM = tangemIconUM, + modifier = baseIconModifier + .offset(x = layerHorizontalShift * SECOND_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER) + .alpha(SECOND_BACK_LAYER_ICON_ALPHA), + ) + } + if (count >= MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER) { + TangemIcon( + tangemIconUM = tangemIconUM, + modifier = baseIconModifier + .offset(x = layerHorizontalShift * FIRST_BACK_LAYER_HORIZONTAL_OFFSET_MULTIPLIER) + .alpha(FIRST_BACK_LAYER_ICON_ALPHA), + ) + } + TangemIcon( + tangemIconUM = tangemIconUM, + modifier = baseIconModifier, + ) + } +} + +@Preview(showBackground = true, widthDp = 360) +@Composable +private fun LayeringIconsPreview() { + val previewCurrencyIcon = TangemIconUM.Currency( + currencyIconState = CurrencyIconState.TokenIcon( + url = null, + topBadgeIconResId = null, + fallbackTint = TangemColorPalette.Black, + fallbackBackground = TangemColorPalette.Meadow, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ) + TangemThemePreview { + Column(horizontalAlignment = Alignment.End) { + LayeringIcons(count = MIN_STACKED_ICON_COUNT, tangemIconUM = previewCurrencyIcon) + LayeringIcons( + count = MAX_STACKED_ICON_COUNT, + tangemIconUM = previewCurrencyIcon, + ) + LayeringIcons( + count = MIN_STACKED_COUNT_FOR_FIRST_BACK_LAYER, + tangemIconUM = previewCurrencyIcon, + ) + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt index 6d7118f812..73ab0434d4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/SearchContent.kt @@ -1,5 +1,6 @@ package com.tangem.features.feed.ui.search +import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* @@ -8,9 +9,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -27,16 +27,20 @@ import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerW import com.tangem.core.ui.components.list.InfiniteListHandler 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.clickableSingle import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.search.components.GroupedUserAssetItem +import com.tangem.features.feed.ui.search.components.SingleUserAssetItem import com.tangem.features.feed.ui.search.state.* +import kotlinx.collections.immutable.ImmutableList private const val PLACEHOLDER_COUNT = 10 private const val LOAD_MORE_THRESHOLD = 5 +private const val USER_ASSETS_LIMIT = 3 @Composable internal fun SearchContent( @@ -58,6 +62,14 @@ internal fun SearchContent( lazyListState.scrollToItem(0) } + var isUserAssetsExpanded by rememberSaveable { mutableStateOf(false) } + val shouldShowUserAssetsPortfolio = content is SearchContentUM.Results && content.userAssets.isNotEmpty() + LaunchedEffect(shouldShowUserAssetsPortfolio) { + if (!shouldShowUserAssetsPortfolio) { + isUserAssetsExpanded = false + } + } + LazyColumn( state = lazyListState, modifier = modifier @@ -80,6 +92,8 @@ internal fun SearchContent( ) is SearchContentUM.Results -> searchResultsItems( results = content, + isUserAssetsExpanded = isUserAssetsExpanded, + onUserAssetsExpandedChange = { isUserAssetsExpanded = it }, onResultMarketTokenClick = searchCallbacks.onResultMarketTokenClick, ) } @@ -147,18 +161,19 @@ private fun LazyListScope.searchHistoryItems( private fun LazyListScope.searchResultsItems( results: SearchContentUM.Results, + isUserAssetsExpanded: Boolean, + onUserAssetsExpandedChange: (Boolean) -> Unit, onResultMarketTokenClick: (MarketsListItemUM) -> Unit, ) { if (results.userAssets.isNotEmpty()) { item(key = "header_portfolio") { SectionHeader(title = stringResourceSafe(R.string.markets_search_portfolio_header)) } - items( - items = results.userAssets, - key = { it.id }, - ) { asset -> - UserAssetItem(asset) - } + userAssetsPortfolioItems( + assets = results.userAssets, + expanded = isUserAssetsExpanded, + onExpandedChange = onUserAssetsExpandedChange, + ) } when (val market = results.marketTokens) { @@ -175,6 +190,42 @@ private fun LazyListScope.searchResultsItems( } } +private fun LazyListScope.userAssetsPortfolioItems( + assets: ImmutableList, + expanded: Boolean, + onExpandedChange: (Boolean) -> Unit, +) { + val shouldShowToggle = assets.size > USER_ASSETS_LIMIT + val visibleCount = if (shouldShowToggle && !expanded) USER_ASSETS_LIMIT else assets.size + + items( + count = visibleCount, + key = { index -> "user_asset_${assets[index].id}" }, + ) { index -> + Column( + modifier = Modifier.animateItem( + fadeInSpec = tween(durationMillis = 300), + fadeOutSpec = tween(durationMillis = 250), + ), + ) { + UserAssetItem(assets[index]) + SpacerH(TangemTheme.dimens2.x2) + } + } + if (shouldShowToggle) { + item(key = "user_assets_show_toggle") { + ShowAllUserAssetsButton( + modifier = Modifier.animateItem( + fadeInSpec = tween(durationMillis = 300), + fadeOutSpec = tween(durationMillis = 250), + ), + isExpanded = expanded, + onClick = { onExpandedChange(!expanded) }, + ) + } + } +} + private fun LazyListScope.marketSearchResultItems( market: MarketSearchResultUM.Content, hasUserAssetsSection: Boolean, @@ -234,7 +285,7 @@ private fun LazyListScope.marketSearchResultNotFoundItem() { ) { Text( text = stringResourceSafe(R.string.common_no_results), - style = TangemTheme.typography2.bodyRegular14, + style = TangemTheme.typography2.subheadlineMedium14, color = TangemTheme.colors2.text.neutral.tertiary, ) } @@ -278,106 +329,34 @@ private fun TextHintItem(hint: TextHintItemUM, onHintClick: () -> Unit) { @Composable private fun UserAssetItem(asset: UserAssetItemUM) { when (asset) { - is UserAssetItemUM.Single -> SingleUserAssetItem(asset) - is UserAssetItemUM.Grouped -> GroupedUserAssetItem(asset) + is UserAssetItemUM.Single -> SingleUserAssetItem(item = asset) + is UserAssetItemUM.Grouped -> GroupedUserAssetItem(item = asset) } } @Composable -private fun SingleUserAssetItem(asset: UserAssetItemUM.Single) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = asset.onClick) - .padding(horizontal = 12.dp, vertical = 14.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), +private fun ShowAllUserAssetsButton(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, ) { - TangemIcon( - modifier = Modifier.size(40.dp), - tangemIconUM = asset.icon, + TangemButton( + buttonUM = TangemButtonUM( + text = if (isExpanded) { + resourceReference(R.string.feed_search_show_less_user_assets) + } else { + resourceReference(R.string.feed_search_show_all_user_assets) + }, + tangemIconUM = TangemIconUM.Icon( + iconRes = if (isExpanded) R.drawable.ic_chewron_up_20 else R.drawable.ic_chewron_down_20, + ), + iconPosition = TangemButtonIconPosition.End, + onClick = onClick, + type = TangemButtonType.Secondary, + size = TangemButtonSize.X7, + shape = TangemButtonShape.Rounded, + ), ) - Column(modifier = Modifier.weight(1f)) { - Text( - text = asset.tokenName, - style = TangemTheme.typography2.bodySemibold16, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - ) - Text( - text = asset.tokenSymbol, - style = TangemTheme.typography2.captionRegular13, - color = TangemTheme.colors2.text.neutral.tertiary, - maxLines = 1, - ) - } - if (!asset.isBalanceHidden) { - Column(horizontalAlignment = Alignment.End) { - Text( - text = asset.fiatBalance, - style = TangemTheme.typography2.bodySemibold16, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - ) - Text( - text = asset.cryptoBalance, - style = TangemTheme.typography2.captionRegular13, - color = TangemTheme.colors2.text.neutral.tertiary, - maxLines = 1, - ) - } - } - } -} - -// TODO [REDACTED_JIRA] update ui item to Portfolio block item -@Composable -private fun GroupedUserAssetItem(asset: UserAssetItemUM.Grouped) { - Column( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = asset.onClick) - .padding(horizontal = 12.dp, vertical = 14.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - TangemIcon( - modifier = Modifier.size(40.dp), - tangemIconUM = asset.icon, - ) - Column(modifier = Modifier.weight(1f)) { - Text( - text = asset.tokenName, - style = TangemTheme.typography2.bodySemibold16, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - ) - Text( - text = "${asset.tokenSymbol} · ${asset.tokensCount}", - style = TangemTheme.typography2.captionRegular13, - color = TangemTheme.colors2.text.neutral.tertiary, - maxLines = 1, - ) - } - if (!asset.isBalanceHidden) { - Column(horizontalAlignment = Alignment.End) { - Text( - text = asset.totalFiatBalance, - style = TangemTheme.typography2.bodySemibold16, - color = TangemTheme.colors2.text.neutral.primary, - maxLines = 1, - ) - Text( - text = asset.totalCryptoBalance, - style = TangemTheme.typography2.captionRegular13, - color = TangemTheme.colors2.text.neutral.tertiary, - maxLines = 1, - ) - } - } - } } } @@ -397,7 +376,7 @@ private fun ShowTokensUnder100kItem(onShowTokensClick: () -> Unit, modifier: Mod ) { Text( text = stringResourceSafe(R.string.markets_search_see_tokens_under_100k), - style = TangemTheme.typography2.bodyRegular14, + style = TangemTheme.typography2.subheadlineMedium14, color = TangemTheme.colors2.text.neutral.secondary, ) TangemButton( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt new file mode 100644 index 0000000000..552f07fcb3 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/BalanceColumn.kt @@ -0,0 +1,175 @@ +package com.tangem.features.feed.ui.search.components + +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.TextOverflow +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.flicker +import com.tangem.core.ui.extensions.resolveAnnotatedReference +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.feed.ui.search.state.BalanceDisplayState +import com.tangem.utils.StringsSigns + +@Composable +internal fun BalanceColumn( + balanceState: BalanceDisplayState, + isBalanceHidden: Boolean, + modifier: Modifier = Modifier, +) { + if (isBalanceHidden) { + HiddenBalance(modifier) + return + } + when (balanceState) { + is BalanceDisplayState.Loading -> LoadingBalanceColumn(modifier) + is BalanceDisplayState.Flickering -> FlickeringBalanceColumn(balanceState, modifier) + is BalanceDisplayState.Stale -> StaleBalanceColumn(balanceState, modifier) + is BalanceDisplayState.Unreachable -> UnreachableBalanceColumn(modifier) + is BalanceDisplayState.Loaded -> LoadedBalanceColumn(balanceState, modifier) + } +} + +@Composable +private fun BalanceColumnLayout(modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.spacedBy(4.dp), + content = content, + ) +} + +@Composable +private fun LoadingBalanceColumn(modifier: Modifier = Modifier) { + BalanceColumnLayout(modifier) { + RectangleShimmer(modifier = Modifier.size(width = 108.dp, height = 20.dp), radius = 20.dp) + RectangleShimmer(modifier = Modifier.size(width = 64.dp, height = 16.dp), radius = 20.dp) + } +} + +@Composable +private fun FlickeringBalanceColumn(state: BalanceDisplayState.Flickering, modifier: Modifier = Modifier) { + val flickerModifier = Modifier.flicker(isFlickering = true) + BalanceColumnLayout(modifier) { + Text( + modifier = flickerModifier, + text = state.fiatBalance.resolveAnnotatedReference(), + style = TangemTheme.typography2.bodyMedium16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + modifier = flickerModifier, + text = state.cryptoBalance.resolveReference(), + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun StaleBalanceColumn(state: BalanceDisplayState.Stale, modifier: Modifier = Modifier) { + BalanceColumnLayout(modifier) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_error_sync_24), + contentDescription = null, + tint = TangemTheme.colors2.markers.iconGray, + ) + SpacerW(TangemTheme.dimens2.x0_5) + Text( + text = state.fiatBalance.resolveAnnotatedReference(), + style = TangemTheme.typography2.bodyMedium16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + Text( + text = state.cryptoBalance.resolveReference(), + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + +@Composable +private fun UnreachableBalanceColumn(modifier: Modifier = Modifier) { + BalanceColumnLayout(modifier) { + Text( + text = StringsSigns.DASH_SIGN, + style = TangemTheme.typography2.bodyMedium16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResourceSafe(R.string.common_unreachable), + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.status.attention, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + SpacerW(TangemTheme.dimens2.x0_5) + Icon( + modifier = Modifier.size(16.dp), + painter = painterResource(id = R.drawable.ic_alert_triange_24), + contentDescription = null, + tint = TangemTheme.colors2.graphic.status.attention, + ) + } + } +} + +@Composable +private fun LoadedBalanceColumn(state: BalanceDisplayState.Loaded, modifier: Modifier = Modifier) { + BalanceColumnLayout(modifier) { + Text( + text = state.fiatBalance.resolveAnnotatedReference(), + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = state.cryptoBalance.resolveReference(), + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + ) + } +} + +@Composable +private fun HiddenBalance(modifier: Modifier = Modifier) { + BalanceColumnLayout(modifier) { + Text( + text = StringsSigns.THREE_STARS, + style = TangemTheme.typography2.bodySemibold16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + ) + Text( + text = StringsSigns.THREE_STARS, + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt new file mode 100644 index 0000000000..11504f43d5 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/GroupedUserAssetItem.kt @@ -0,0 +1,84 @@ +package com.tangem.features.feed.ui.search.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.layoutId +import androidx.compose.ui.text.style.TextOverflow +import com.tangem.core.ui.ds.button.* +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId +import com.tangem.core.ui.extensions.pluralStringResourceSafe +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.impl.R +import com.tangem.features.feed.ui.components.LayeringIcons +import com.tangem.features.feed.ui.search.state.UserAssetItemUM + +@Composable +internal fun GroupedUserAssetItem(item: UserAssetItemUM.Grouped, modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors2.surface.level3, + shape = RoundedCornerShape(TangemTheme.dimens2.x5), + ), + ) { + TangemRowContainer( + modifier = Modifier.clickable(onClick = item.onClick), + content = { + LayeringIcons( + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.HEAD) + .padding(end = TangemTheme.dimens2.x1), + tangemIconUM = item.icon, + count = item.tokensCount, + ) + + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP), + text = item.tokenName, + style = TangemTheme.typography2.bodyMedium16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM), + text = pluralStringResourceSafe(R.plurals.common_tokens_count, item.tokensCount, item.tokensCount), + style = TangemTheme.typography2.captionMedium12, + color = TangemTheme.colors2.text.neutral.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + + BalanceColumn( + modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP), + balanceState = item.balanceState, + isBalanceHidden = item.isBalanceHidden, + ) + + TangemButton( + modifier = Modifier + .padding(start = TangemTheme.dimens2.x2) + .layoutId(TangemRowLayoutId.TAIL), + buttonUM = TangemButtonUM( + type = TangemButtonType.Secondary, + tangemIconUM = TangemIconUM.Icon( + iconRes = R.drawable.ic_chevron_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, + ), + shape = TangemButtonShape.Rounded, + size = TangemButtonSize.X10, + onClick = item.onClick, + ), + ) + }, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt new file mode 100644 index 0000000000..cf255f66a5 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/components/SingleUserAssetItem.kt @@ -0,0 +1,117 @@ +package com.tangem.features.feed.ui.search.components + +import androidx.compose.animation.AnimatedContent +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.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.layout.layoutId +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.marketprice.PriceChangeInPercent +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.ds.image.TangemIcon +import com.tangem.core.ui.ds.row.TangemRowContainer +import com.tangem.core.ui.ds.row.TangemRowLayoutId +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.feed.ui.search.state.BalanceDisplayState +import com.tangem.features.feed.ui.search.state.UserAssetItemUM + +@Composable +fun SingleUserAssetItem(item: UserAssetItemUM.Single, modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors2.surface.level3, + shape = RoundedCornerShape(TangemTheme.dimens2.x5), + ), + ) { + TangemRowContainer( + modifier = Modifier.clickable(onClick = item.onClick), + content = { + TangemIcon( + modifier = Modifier + .layoutId(layoutId = TangemRowLayoutId.HEAD) + .size(40.dp) + .padding(end = TangemTheme.dimens2.x1), + tangemIconUM = item.icon, + ) + + Text( + modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP), + text = item.tokenName, + style = TangemTheme.typography2.bodyMedium16, + color = TangemTheme.colors2.text.neutral.primary, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + + PriceBlock( + modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM), + priceChangeState = item.priceChangeState, + fiatRate = item.fiatRate, + balanceState = item.balanceState, + ) + + BalanceColumn( + modifier = Modifier.layoutId(layoutId = TangemRowLayoutId.END_TOP), + balanceState = item.balanceState, + isBalanceHidden = item.isBalanceHidden, + ) + }, + ) + } +} + +@Composable +private fun PriceBlock( + priceChangeState: PriceChangeState, + fiatRate: String?, + balanceState: BalanceDisplayState, + modifier: Modifier = Modifier, +) { + val isDisabled = remember(balanceState) { + balanceState is BalanceDisplayState.Unreachable + } + + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + ) { + if (fiatRate != null) { + Text( + text = fiatRate, + style = TangemTheme.typography2.captionMedium12, + color = if (isDisabled) { + TangemTheme.colors2.text.status.disabled + } else { + TangemTheme.colors2.text.neutral.secondary + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + AnimatedContent( + targetState = priceChangeState, + contentKey = { it::class }, + ) { animatedState -> + when (animatedState) { + is PriceChangeState.Content -> { + PriceChangeInPercent( + valueInPercent = animatedState.valueInPercent, + type = animatedState.type, + textStyle = TangemTheme.typography2.captionMedium12, + isDisabled = isDisabled, + ) + } + PriceChangeState.Unknown -> Unit + } + } + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt index e5cd68f79c..58334e7bce 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/SearchContentPreview.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp 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.PriceChangeState import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.ds.image.TangemIconUM import com.tangem.core.ui.extensions.stringReference @@ -217,8 +218,14 @@ internal object SearchContentPreviewFixtures { tokenName = name, tokenSymbol = symbol, fiatRate = "$98,765.43", - cryptoBalance = "1.234 $symbol", - fiatBalance = "$121,876.50", + priceChangeState = PriceChangeState.Content( + type = PriceChangeType.UP, + valueInPercent = "+2.34%", + ), + balanceState = BalanceDisplayState.Loaded( + cryptoBalance = stringReference("1.234 $symbol"), + fiatBalance = stringReference("$121,876.50"), + ), isBalanceHidden = false, onClick = {}, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt new file mode 100644 index 0000000000..f2f8302d45 --- /dev/null +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/preview/UserAssetItemPreview.kt @@ -0,0 +1,238 @@ +package com.tangem.features.feed.ui.search.preview + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.currency.icon.CurrencyIconState +import com.tangem.core.ui.components.marketprice.PriceChangeState +import com.tangem.core.ui.components.marketprice.PriceChangeType +import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.features.feed.ui.search.components.GroupedUserAssetItem +import com.tangem.features.feed.ui.search.components.SingleUserAssetItem +import com.tangem.features.feed.ui.search.state.BalanceDisplayState +import com.tangem.features.feed.ui.search.state.UserAssetItemUM +import kotlinx.collections.immutable.persistentListOf + +/** Labeled UI state for [SingleUserAssetItem] previews (dropdown label in Studio). */ +internal data class SingleUserAssetItemPreviewScenario( + val title: String, + val item: UserAssetItemUM.Single, +) + +/** Labeled UI state for [GroupedUserAssetItem] previews. */ +internal data class GroupedUserAssetItemPreviewScenario( + val title: String, + val item: UserAssetItemUM.Grouped, +) + +@Suppress("StringLiteralDuplication") +internal object UserAssetItemPreviewFixtures { + + private val sampleIcon = TangemIconUM.Currency( + CurrencyIconState.CoinIcon( + url = null, + fallbackResId = com.tangem.core.ui.R.drawable.ic_ethereumpow_22, + isGrayscale = false, + shouldShowCustomBadge = false, + ), + ) + + private val cryptoRef = stringReference("1.234 ETH") + private val fiatRef = stringReference("$121,876.50") + + private val samplePriceChange = PriceChangeState.Content( + valueInPercent = "+2.34%", + type = PriceChangeType.UP, + ) + + private fun balanceLoaded() = BalanceDisplayState.Loaded( + cryptoBalance = cryptoRef, + fiatBalance = fiatRef, + ) + + private fun balanceFlickering() = BalanceDisplayState.Flickering( + cryptoBalance = cryptoRef, + fiatBalance = fiatRef, + ) + + private fun balanceStale() = BalanceDisplayState.Stale( + cryptoBalance = cryptoRef, + fiatBalance = fiatRef, + ) + + private fun balanceLoading() = BalanceDisplayState.Loading + + private fun balanceUnreachable() = BalanceDisplayState.Unreachable + + fun allSingleScenarios(): List = listOf( + SingleUserAssetItemPreviewScenario( + title = "Hidden (balanceState ignored)", + item = single( + balanceState = balanceLoaded(), + isBalanceHidden = true, + ), + ), + SingleUserAssetItemPreviewScenario( + title = "Balance – Loaded", + item = single(balanceState = balanceLoaded(), isBalanceHidden = false), + ), + SingleUserAssetItemPreviewScenario( + title = "Balance – Flickering", + item = single(balanceState = balanceFlickering(), isBalanceHidden = false), + ), + SingleUserAssetItemPreviewScenario( + title = "Balance – Stale", + item = single(balanceState = balanceStale(), isBalanceHidden = false), + ), + SingleUserAssetItemPreviewScenario( + title = "Balance – Loading", + item = single(balanceState = balanceLoading(), isBalanceHidden = false), + ), + SingleUserAssetItemPreviewScenario( + title = "Balance – Unreachable", + item = single(balanceState = balanceUnreachable(), isBalanceHidden = false), + ), + ) + + fun allGroupedScenarios(): List = listOf( + GroupedUserAssetItemPreviewScenario( + title = "Hidden (balanceState ignored)", + item = grouped( + balanceState = balanceLoaded(), + isBalanceHidden = true, + ), + ), + GroupedUserAssetItemPreviewScenario( + title = "Balance – Loaded", + item = grouped(balanceState = balanceLoaded(), isBalanceHidden = false), + ), + GroupedUserAssetItemPreviewScenario( + title = "Balance – Flickering", + item = grouped(balanceState = balanceFlickering(), isBalanceHidden = false), + ), + GroupedUserAssetItemPreviewScenario( + title = "Balance – Stale", + item = grouped(balanceState = balanceStale(), isBalanceHidden = false), + ), + GroupedUserAssetItemPreviewScenario( + title = "Balance – Loading", + item = grouped(balanceState = balanceLoading(), isBalanceHidden = false), + ), + GroupedUserAssetItemPreviewScenario( + title = "Balance – Unreachable", + item = grouped(balanceState = balanceUnreachable(), isBalanceHidden = false), + ), + ) + + private fun single(balanceState: BalanceDisplayState, isBalanceHidden: Boolean): UserAssetItemUM.Single = + UserAssetItemUM.Single( + id = "single_preview", + icon = sampleIcon, + tokenName = "Ethereum", + tokenSymbol = "ETH", + fiatRate = "$98,765.43", + priceChangeState = samplePriceChange, + balanceState = balanceState, + isBalanceHidden = isBalanceHidden, + onClick = {}, + ) + + private fun grouped(balanceState: BalanceDisplayState, isBalanceHidden: Boolean): UserAssetItemUM.Grouped = + UserAssetItemUM.Grouped( + id = "grouped_preview", + icon = sampleIcon, + tokenName = "Ethereum", + tokenSymbol = "ETH", + tokensCount = 3, + balanceState = balanceState, + isBalanceHidden = isBalanceHidden, + children = persistentListOf(), + onClick = {}, + ) +} + +internal class SingleUserAssetItemPreviewParameterProvider : + PreviewParameterProvider { + override val values: Sequence + get() = UserAssetItemPreviewFixtures.allSingleScenarios().asSequence() +} + +internal class GroupedUserAssetItemPreviewParameterProvider : + PreviewParameterProvider { + override val values: Sequence + get() = UserAssetItemPreviewFixtures.allGroupedScenarios().asSequence() +} + +@Composable +private fun SingleUserAssetItemPreviewHost( + scenario: SingleUserAssetItemPreviewScenario, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(8.dp), + ) { + SingleUserAssetItem(item = scenario.item) + } +} + +@Composable +private fun GroupedUserAssetItemPreviewHost( + scenario: GroupedUserAssetItemPreviewScenario, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(8.dp), + ) { + GroupedUserAssetItem(item = scenario.item) + } +} + +@Composable +@Preview(name = "Single – all balance states (parameter)", showBackground = true, widthDp = 360) +@Preview( + name = "Single – all balance states (night)", + showBackground = true, + widthDp = 360, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun SingleUserAssetItemPreview_AllBalanceStates( + @PreviewParameter(SingleUserAssetItemPreviewParameterProvider::class) scenario: SingleUserAssetItemPreviewScenario, +) { + TangemThemePreviewRedesign { + SingleUserAssetItemPreviewHost(scenario = scenario) + } +} + +@Composable +@Preview(name = "Grouped – all balance states (parameter)", showBackground = true, widthDp = 360) +@Preview( + name = "Grouped – all balance states (night)", + showBackground = true, + widthDp = 360, + uiMode = Configuration.UI_MODE_NIGHT_YES, +) +private fun GroupedUserAssetItemPreview_AllBalanceStates( + @PreviewParameter(GroupedUserAssetItemPreviewParameterProvider::class) + scenario: GroupedUserAssetItemPreviewScenario, +) { + TangemThemePreviewRedesign { + GroupedUserAssetItemPreviewHost(scenario = scenario) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt index ede3decb1b..31277562c6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/search/state/SearchUM.kt @@ -4,8 +4,11 @@ import androidx.compose.runtime.Immutable import com.tangem.common.ui.account.AccountNameUM import com.tangem.common.ui.markets.models.MarketsListItemUM import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.components.marketprice.PriceChangeState import com.tangem.core.ui.ds.image.TangemIconUM +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.account.CryptoPortfolioIcon +import com.tangem.domain.models.currency.CryptoCurrencyStatus import kotlinx.collections.immutable.ImmutableList data class SearchUM( @@ -45,6 +48,28 @@ sealed interface MarketSearchResultUM { data class TextHintItemUM(val text: String) +@Immutable +sealed interface BalanceDisplayState { + + data class Loaded( + val cryptoBalance: TextReference, + val fiatBalance: TextReference, + ) : BalanceDisplayState + + data class Flickering( + val cryptoBalance: TextReference, + val fiatBalance: TextReference, + ) : BalanceDisplayState + + data class Stale( + val cryptoBalance: TextReference, + val fiatBalance: TextReference, + ) : BalanceDisplayState + + data object Loading : BalanceDisplayState + data object Unreachable : BalanceDisplayState +} + @Immutable sealed interface UserAssetItemUM { val id: String @@ -59,8 +84,8 @@ sealed interface UserAssetItemUM { override val tokenName: String, override val tokenSymbol: String, val fiatRate: String?, - val cryptoBalance: String, - val fiatBalance: String, + val priceChangeState: PriceChangeState, + val balanceState: BalanceDisplayState, val isBalanceHidden: Boolean, override val onClick: () -> Unit, ) : UserAssetItemUM @@ -71,8 +96,7 @@ sealed interface UserAssetItemUM { override val tokenName: String, override val tokenSymbol: String, val tokensCount: Int, - val totalCryptoBalance: String, - val totalFiatBalance: String, + val balanceState: BalanceDisplayState, val isBalanceHidden: Boolean, val children: ImmutableList, override val onClick: () -> Unit, @@ -83,7 +107,6 @@ sealed interface UserAssetItemUM { val accountName: AccountNameUM, val accountIcon: CryptoPortfolioIcon.Icon, val accountColor: CryptoPortfolioIcon.Color, - val cryptoBalance: String, - val fiatBalance: String, + val currencyStatus: CryptoCurrencyStatus, ) } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt index 2bbfa496e2..5963f2ea34 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt @@ -27,11 +27,7 @@ internal fun contentFeedEntryStackAnimation(): StackAnimation< ComposableModularBottomSheetContentComponent, > = stackAnimation { to, from, _ -> - val isSearchToTokenList = - (to.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true - val isFromSearchTokenList = - (from.configuration as? FeedEntryChildFactory.Child.TokenList)?.params?.shouldAlwaysShowSearchBar == true - if (isSearchToTokenList || isFromSearchTokenList) { + if (to.configuration.usesFadeStackTransition() || from.configuration.usesFadeStackTransition()) { fade() } else { slide() diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt index 820fa36229..79c058b390 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/token/block/impl/ui/TokenMarketBlockLegacy.kt @@ -106,6 +106,7 @@ private fun LeftSide( modifier = Modifier.alignByBaseline(), valueInPercent = percentText, type = type, + textStyle = TangemTheme.typography.body2, ) Text( modifier = Modifier.alignByBaseline(), From c72ffd2e81550464414ca9afd31868c33fdd97ca Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2026 16:55:50 +0400 Subject: [PATCH 18/32] Updated on 2026-08-14 --- app/build.gradle.kts | 2 + common/ui-markets/build.gradle.kts | 16 ++++ .../ui/markets/action/CryptoCurrencyData.kt | 11 +++ .../ui/markets/action}/QuickActionUM.kt | 6 +- .../common/ui/markets/action/QuickActions.kt | 9 ++ .../markets/action/QuickActionsConverter.kt | 61 +++++++++++++ .../action}/TokenActionsBSContentUM.kt | 6 +- .../ui/markets/action}/TokenActionsHandler.kt | 28 +++--- features/common-features/api/.gitignore | 1 + features/common-features/api/build.gradle.kts | 27 ++++++ .../AddToPortfolioComponent.kt | 2 +- .../addtoportfolio}/AddToPortfolioManager.kt | 2 +- .../AddToPortfolioPreselectedDataComponent.kt | 2 +- .../api/addtoportfolio}/AvailableToAddData.kt | 2 +- features/common-features/impl/.gitignore | 1 + .../common-features/impl/build.gradle.kts | 81 +++++++++++++++++ .../AddToPortfolioBottomSheet.kt | 6 +- .../impl/addtoportfolio}/AddTokenComponent.kt | 10 +- .../addtoportfolio}/ChooseNetworkComponent.kt | 8 +- .../DefaultAddToPortfolioComponent.kt | 8 +- ...tAddToPortfolioPreselectedDataComponent.kt | 8 +- .../addtoportfolio}/TokenActionsComponent.kt | 12 +-- .../analytics/EarnAnalyticsEvent.kt | 35 +++++++ .../analytics/PortfolioAnalyticsEvent.kt | 91 +++++++++++++++++++ .../converter/AvailableToAddDataConverter.kt | 8 +- .../converter}/BlockchainRowUMConverter.kt | 14 +-- .../di/AddToPortfolioComponentModule.kt | 28 ++++++ .../di/AddToPortfolioModelModule.kt | 8 +- .../model/AddToPortfolioModel.kt | 26 +++--- .../AddToPortfolioPreselectedDataModel.kt | 16 ++-- .../model/AddToPortfolioRoutes.kt | 4 +- .../addtoportfolio}/model/AddTokenModel.kt | 12 +-- .../model/AddTokenUiBuilder.kt | 10 +- .../model/CheckCurrencyUnsupportedDelegate.kt | 4 +- .../model/ChooseNetworkModel.kt | 8 +- .../model/TokenActionsModel.kt | 10 +- .../model/TokenActionsUiBuilder.kt | 16 ++-- .../ui/ChooseNetworkContent.kt | 6 +- .../ui/DefaultAddToPortfolioManager.kt | 6 +- .../addtoportfolio}/ui/TokenActionsContent.kt | 12 +-- .../ui/state/ChooseNetworkUM.kt | 2 +- .../addtoportfolio/ui/state/TokenActionsUM.kt | 10 ++ features/feed/impl/build.gradle.kts | 1 + .../feed/components/FeedEntryChildFactory.kt | 2 +- .../components/earn/DefaultEarnComponent.kt | 2 +- .../components/feed/DefaultFeedComponent.kt | 4 +- .../components/feed/FeedBottomSheetRoute.kt | 2 +- .../impl/di/AddToPortfolioComponentModule.kt | 28 ------ .../add/impl/ui/state/TokenActionsUM.kt | 10 -- .../impl/DefaultMarketsPortfolioComponent.kt | 2 +- .../impl/analytics/PortfolioAnalyticsEvent.kt | 76 +--------------- .../portfolio/impl/loader/PortfolioData.kt | 33 ------- .../impl/model/MarketsPortfolioDelegate.kt | 5 +- .../impl/model/MarketsPortfolioModel.kt | 7 +- .../impl/model/PortfolioTokenUMConverter.kt | 73 ++------------- .../impl/ui/PortfolioQuickActions.kt | 2 +- .../preview/PreviewMyPortfolioUMProvider.kt | 4 +- .../impl/ui/state/PortfolioTokenUM.kt | 8 +- .../features/feed/model/earn/EarnModel.kt | 2 +- .../earn/analytics/EarnAnalyticsEvent.kt | 24 ----- .../feed/model/feed/FeedComponentModel.kt | 2 +- features/onramp/impl/build.gradle.kts | 3 + .../swap/DefaultSwapSelectTokensComponent.kt | 2 +- .../AvailableSwapPairsComponent.kt | 4 +- .../DefaultAvailableSwapPairsComponent.kt | 4 +- .../model/AvailableSwapPairsModel.kt | 4 +- features/swap/impl/build.gradle.kts | 3 + .../impl/DefaultChooseTokenComponent.kt | 4 +- .../impl/model/ChooseTokenModel.kt | 2 +- .../impl/model/MarketBlockDelegate.kt | 4 +- settings.gradle.kts | 3 + 71 files changed, 556 insertions(+), 399 deletions(-) create mode 100644 common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/CryptoCurrencyData.kt rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action}/QuickActionUM.kt (91%) create mode 100644 common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActions.kt create mode 100644 common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action}/TokenActionsBSContentUM.kt (91%) rename {features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model => common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action}/TokenActionsHandler.kt (85%) create mode 100644 features/common-features/api/.gitignore create mode 100644 features/common-features/api/build.gradle.kts rename features/{feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add => common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio}/AddToPortfolioComponent.kt (90%) rename features/{feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add => common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio}/AddToPortfolioManager.kt (94%) rename features/{feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add => common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio}/AddToPortfolioPreselectedDataComponent.kt (94%) rename features/{feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add => common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio}/AvailableToAddData.kt (96%) create mode 100644 features/common-features/impl/.gitignore create mode 100644 features/common-features/impl/build.gradle.kts rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/AddToPortfolioBottomSheet.kt (95%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/AddTokenComponent.kt (79%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/ChooseNetworkComponent.kt (79%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/DefaultAddToPortfolioComponent.kt (91%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/DefaultAddToPortfolioPreselectedDataComponent.kt (89%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/TokenActionsComponent.kt (83%) create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/analytics/EarnAnalyticsEvent.kt create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/analytics/PortfolioAnalyticsEvent.kt rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/converter/AvailableToAddDataConverter.kt (92%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/converter}/BlockchainRowUMConverter.kt (79%) create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/di/AddToPortfolioModelModule.kt (66%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/AddToPortfolioModel.kt (94%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/AddToPortfolioPreselectedDataModel.kt (95%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/AddToPortfolioRoutes.kt (78%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/AddTokenModel.kt (92%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/AddTokenUiBuilder.kt (91%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/CheckCurrencyUnsupportedDelegate.kt (96%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/ChooseNetworkModel.kt (85%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/TokenActionsModel.kt (86%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/model/TokenActionsUiBuilder.kt (65%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/ui/ChooseNetworkContent.kt (94%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/ui/DefaultAddToPortfolioManager.kt (90%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/ui/TokenActionsContent.kt (94%) rename features/{feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl => common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio}/ui/state/ChooseNetworkUM.kt (73%) create mode 100644 features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/state/TokenActionsUM.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/state/TokenActionsUM.kt delete mode 100644 features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioData.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5def3358f3..6f398fb14f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -274,6 +274,8 @@ dependencies { implementation(projects.features.nft.impl) implementation(projects.features.walletconnect.api) implementation(projects.features.walletconnect.impl) + implementation(projects.features.commonFeatures.api) + implementation(projects.features.commonFeatures.impl) implementation(projects.features.usedesk.api) implementation(projects.features.usedesk.impl) implementation(projects.features.hotWallet.api) diff --git a/common/ui-markets/build.gradle.kts b/common/ui-markets/build.gradle.kts index b83e4298e2..7eb6c53bd1 100644 --- a/common/ui-markets/build.gradle.kts +++ b/common/ui-markets/build.gradle.kts @@ -1,6 +1,8 @@ plugins { alias(deps.plugins.android.library) alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.hilt.android) id("configuration") } @@ -12,13 +14,23 @@ dependencies { /** Project - Core */ implementation(projects.core.ui) implementation(projects.core.utils) + implementation(projects.core.navigation) + implementation(projects.core.analytics) /** Project - Common */ implementation(projects.common.uiCharts) implementation(projects.common.ui) + implementation(projects.common.routing) /** Project - Domain */ implementation(projects.domain.models) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.staking) + implementation(projects.domain.staking.models) + implementation(projects.domain.onramp.models) + implementation(projects.domain.offramp) + implementation(projects.domain.demo) implementation(deps.lifecycle.compose) implementation(deps.compose.foundation) @@ -26,4 +38,8 @@ dependencies { implementation(deps.compose.ui.tooling) implementation(deps.compose.ui.utils) implementation(deps.kotlin.immutable.collections) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) } \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/CryptoCurrencyData.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/CryptoCurrencyData.kt new file mode 100644 index 0000000000..34ecb8e2f3 --- /dev/null +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/CryptoCurrencyData.kt @@ -0,0 +1,11 @@ +package com.tangem.common.ui.markets.action + +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.tokens.model.TokenActionsState + +data class CryptoCurrencyData( + val userWallet: UserWallet, + val status: CryptoCurrencyStatus, + val actions: List, +) \ 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/QuickActionUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionUM.kt similarity index 91% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/QuickActionUM.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionUM.kt index c98c012df0..c2565c295b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/QuickActionUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionUM.kt @@ -1,14 +1,14 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state +package com.tangem.common.ui.markets.action import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.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.features.feed.impl.R @Immutable -internal sealed class QuickActionUM( +sealed class QuickActionUM( val title: TextReference, val description: TextReference, @DrawableRes val icon: Int, diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActions.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActions.kt new file mode 100644 index 0000000000..27d4bfac71 --- /dev/null +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActions.kt @@ -0,0 +1,9 @@ +package com.tangem.common.ui.markets.action + +import kotlinx.collections.immutable.ImmutableList + +data class QuickActions( + val actions: ImmutableList, + val onQuickActionClick: (QuickActionUM) -> Unit, + val onQuickActionLongClick: (QuickActionUM) -> Unit, +) \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt new file mode 100644 index 0000000000..f0112cab38 --- /dev/null +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt @@ -0,0 +1,61 @@ +package com.tangem.common.ui.markets.action + +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import kotlinx.collections.immutable.toImmutableList + +object QuickActionsConverter { + + fun quickActions(cryptoData: CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): QuickActions { + return 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 = { actionUM -> + if (actionUM == 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(action.shouldShowBadge) + is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive + is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake + is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(action.apy) + else -> null + }?.let(::add) + } + } + }.toImmutableList() +} \ 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/TokenActionsBSContentUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsBSContentUM.kt similarity index 91% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/TokenActionsBSContentUM.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsBSContentUM.kt index f35729eaa8..f33fee7c3d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/state/TokenActionsBSContentUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsBSContentUM.kt @@ -1,14 +1,14 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.ui.state +package com.tangem.common.ui.markets.action import androidx.annotation.DrawableRes import androidx.compose.runtime.Immutable +import com.tangem.common.ui.markets.R 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.feed.impl.R import kotlinx.collections.immutable.ImmutableList -internal data class TokenActionsBSContentUM( +data class TokenActionsBSContentUM( val title: String, val actions: ImmutableList, val onActionClick: (Action) -> Unit, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt similarity index 85% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt rename to common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt index 3f6071b417..90f8b3281b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/TokenActionsHandler.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt @@ -1,7 +1,8 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model +package com.tangem.common.ui.markets.action import com.tangem.common.routing.AppRoute import com.tangem.common.ui.bottomsheet.receive.mapToAddressModels +import com.tangem.common.ui.markets.R import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.models.AnalyticsParam import com.tangem.core.analytics.models.event.OfframpAnalyticsEvent @@ -18,9 +19,6 @@ 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.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM -import com.tangem.features.feed.impl.R import com.tangem.utils.Provider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -28,7 +26,7 @@ import dagger.assisted.AssistedInject import kotlinx.collections.immutable.toImmutableList @Suppress("LongParameterList") -internal class TokenActionsHandler @AssistedInject constructor( +class TokenActionsHandler @AssistedInject constructor( private val router: Router, private val clipboardManager: ClipboardManager, private val uiMessageSender: UiMessageSender, @@ -45,7 +43,7 @@ internal class TokenActionsHandler @AssistedInject constructor( add(TokenActionsBSContentUM.Action.Sell) } - fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: CryptoCurrencyData) { onHandleQuickAction( HandledQuickAction( action = action, @@ -79,13 +77,13 @@ internal class TokenActionsHandler @AssistedInject constructor( } private fun showDemoModeWarning() { - val message = DialogMessage( + val message = DialogMessage.Companion( message = resourceReference(R.string.alert_demo_feature_disabled), ) messageSender.send(message) } - private fun onCopyAddress(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + private fun onCopyAddress(cryptoCurrencyData: CryptoCurrencyData) { val cryptoCurrencyStatus = cryptoCurrencyData.status val networkAddress = cryptoCurrencyStatus.value.networkAddress ?: return val addresses = networkAddress.availableAddresses @@ -97,7 +95,7 @@ internal class TokenActionsHandler @AssistedInject constructor( uiMessageSender.send(SnackbarMessage(resourceReference(R.string.wallet_notification_address_copied))) } - private fun onBuyClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + private fun onBuyClick(cryptoCurrencyData: CryptoCurrencyData) { router.push( AppRoute.Onramp( userWalletId = cryptoCurrencyData.userWallet.walletId, @@ -107,7 +105,7 @@ internal class TokenActionsHandler @AssistedInject constructor( ) } - private fun onSellClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + private fun onSellClick(cryptoCurrencyData: CryptoCurrencyData) { getOfframpUrlUseCase( cryptoCurrencyStatus = cryptoCurrencyData.status, appCurrencyCode = currentAppCurrency().code, @@ -117,7 +115,7 @@ internal class TokenActionsHandler @AssistedInject constructor( } } - private fun onExchangeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + private fun onExchangeClick(cryptoCurrencyData: CryptoCurrencyData) { router.push( AppRoute.Swap( currencyFrom = cryptoCurrencyData.status.currency, @@ -128,7 +126,7 @@ internal class TokenActionsHandler @AssistedInject constructor( ) } - private fun onSendClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + private fun onSendClick(cryptoCurrencyData: CryptoCurrencyData) { val route = AppRoute.SendEntryPoint( userWalletId = cryptoCurrencyData.userWallet.walletId, currency = cryptoCurrencyData.status.currency, @@ -136,7 +134,7 @@ internal class TokenActionsHandler @AssistedInject constructor( router.push(route) } - private fun onStakeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + private fun onStakeClick(cryptoCurrencyData: CryptoCurrencyData) { val option = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake } ?.let { it as TokenActionsState.ActionState.Stake } ?.option ?: return @@ -150,7 +148,7 @@ internal class TokenActionsHandler @AssistedInject constructor( ) } - private fun onYieldModeClick(cryptoCurrencyData: PortfolioData.CryptoCurrencyData) { + private fun onYieldModeClick(cryptoCurrencyData: CryptoCurrencyData) { val yieldSupplyApy = cryptoCurrencyData.actions.filterIsInstance() .firstOrNull()?.apy ?: return @@ -173,6 +171,6 @@ internal class TokenActionsHandler @AssistedInject constructor( data class HandledQuickAction( val action: TokenActionsBSContentUM.Action, - val cryptoCurrencyData: PortfolioData.CryptoCurrencyData, + val cryptoCurrencyData: CryptoCurrencyData, ) } \ No newline at end of file diff --git a/features/common-features/api/.gitignore b/features/common-features/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/common-features/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/common-features/api/build.gradle.kts b/features/common-features/api/build.gradle.kts new file mode 100644 index 0000000000..f46795f50f --- /dev/null +++ b/features/common-features/api/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.serialization) + id("configuration") +} + +android { + namespace = "com.tangem.features.commonfeatures.api" +} + +dependencies { + /** Api */ // todo swap delete after move portfolio selector + implementation(projects.features.account.api) + + /* Project - Domain */ + implementation(projects.domain.models) + implementation(projects.domain.markets) + + /* Project - Core */ + implementation(projects.core.decompose) + implementation(projects.core.ui) + + /* Compose */ + implementation(deps.compose.runtime) + implementation(deps.kotlin.immutable.collections) +} \ No newline at end of file diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioComponent.kt similarity index 90% rename from features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt rename to features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioComponent.kt index 5a1dabaa43..7e3d52793e 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioComponent.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add +package com.tangem.features.commonfeatures.api.addtoportfolio import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt similarity index 94% rename from features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt rename to features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt index 50b323e258..594e0e18ec 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioManager.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioManager.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add +package com.tangem.features.commonfeatures.api.addtoportfolio import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioPreselectedDataComponent.kt similarity index 94% rename from features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt rename to features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioPreselectedDataComponent.kt index c7feac3a8c..59b4f03918 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AddToPortfolioPreselectedDataComponent.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AddToPortfolioPreselectedDataComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add +package com.tangem.features.commonfeatures.api.addtoportfolio import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AvailableToAddData.kt similarity index 96% rename from features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt rename to features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AvailableToAddData.kt index e97bb37f05..d5e89d19f7 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/AvailableToAddData.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addtoportfolio/AvailableToAddData.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add +package com.tangem.features.commonfeatures.api.addtoportfolio import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.account.AccountId diff --git a/features/common-features/impl/.gitignore b/features/common-features/impl/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/common-features/impl/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/common-features/impl/build.gradle.kts b/features/common-features/impl/build.gradle.kts new file mode 100644 index 0000000000..01aef9c2e7 --- /dev/null +++ b/features/common-features/impl/build.gradle.kts @@ -0,0 +1,81 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) + alias(deps.plugins.hilt.android) + id("configuration") +} + +android { + namespace = "com.tangem.features.commonfeatures.impl" +} + +dependencies { + /** Api */ + implementation(projects.features.commonFeatures.api) + // todo swap delete after move portfolio selector + implementation(projects.features.account.api) + implementation(projects.features.tokenRecieve.api) + + /** Core modules */ + implementation(projects.core.analytics) + implementation(projects.core.analytics.models) + implementation(projects.core.utils) + implementation(projects.core.ui) + implementation(projects.core.error) + implementation(projects.core.res) + implementation(projects.core.decompose) + implementation(projects.core.navigation) + implementation(projects.core.datasource) + + /** Domain */ + implementation(projects.domain.models) + implementation(projects.domain.account) + implementation(projects.domain.account.status) + implementation(projects.domain.core) + implementation(projects.domain.appCurrency) + implementation(projects.domain.appCurrency.models) + implementation(projects.domain.markets) + implementation(projects.domain.transaction) + implementation(projects.domain.tokens) + implementation(projects.domain.tokens.models) + implementation(projects.domain.manageTokens) + implementation(projects.domain.manageTokens.models) + implementation(projects.domain.balanceHiding) + implementation(projects.domain.balanceHiding.models) + implementation(projects.domain.wallets) + implementation(projects.domain.wallets.models) + + /** Tangem libraries */ + implementation(tangemDeps.card.core) + + /** Common */ + implementation(projects.common.ui) + implementation(projects.common.uiMarkets) + implementation(projects.common.routing) + + /** AndroidX libraries */ + implementation(deps.androidx.core.ktx) + implementation(deps.lifecycle.runtime.ktx) + + /** Compose libraries */ + implementation(deps.compose.material3) + implementation(deps.compose.animation) + implementation(deps.compose.foundation) + implementation(deps.compose.ui) + implementation(deps.compose.ui.tooling) + implementation(deps.compose.coil) + implementation(deps.decompose.ext.compose) + implementation(deps.androidx.activity.compose) + + /** Other libraries */ + implementation(deps.arrow.core) + implementation(deps.kotlin.immutable.collections) + implementation(deps.kotlin.serialization) + implementation(deps.firebase.crashlytics) + + /** DI */ + implementation(deps.hilt.android) + kapt(deps.hilt.kapt) +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddToPortfolioBottomSheet.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt similarity index 95% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddToPortfolioBottomSheet.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt index 8c6aba3001..c649a267f2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddToPortfolioBottomSheet.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddToPortfolioBottomSheet.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl +package com.tangem.features.commonfeatures.impl.addtoportfolio import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Column @@ -9,6 +9,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.tangem.features.commonfeatures.impl.R import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig @@ -20,8 +21,7 @@ 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.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes -import com.tangem.features.feed.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes @Composable internal fun AddToPortfolioBottomSheet( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddTokenComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddTokenComponent.kt similarity index 79% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddTokenComponent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddTokenComponent.kt index b762aa9727..9175781e37 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/AddTokenComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/AddTokenComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl +package com.tangem.features.commonfeatures.impl.addtoportfolio import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -9,10 +9,10 @@ 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.feed.components.market.details.portfolio.add.SelectedNetwork -import com.tangem.features.feed.components.market.details.portfolio.add.SelectedPortfolio -import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddTokenModel -import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenModel +import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedNetwork +import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedPortfolio +import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ChooseNetworkComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ChooseNetworkComponent.kt similarity index 79% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ChooseNetworkComponent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ChooseNetworkComponent.kt index 71c5a6dc55..95dab8e47a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ChooseNetworkComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ChooseNetworkComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl +package com.tangem.features.commonfeatures.impl.addtoportfolio import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -9,9 +9,9 @@ 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.feed.components.market.details.portfolio.add.SelectedPortfolio -import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.ChooseNetworkModel -import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.ChooseNetworkContent +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.ChooseNetworkModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.ChooseNetworkContent +import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedPortfolio import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt similarity index 91% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioComponent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt index 8f7dfb79a1..b072275b3b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl +package com.tangem.features.commonfeatures.impl.addtoportfolio import androidx.compose.runtime.Composable import com.arkivanov.decompose.ComponentContext @@ -12,9 +12,9 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.account.PortfolioSelectorComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioModel -import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioPreselectedDataComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt similarity index 89% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioPreselectedDataComponent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt index 95efde9298..f2b8949242 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/DefaultAddToPortfolioPreselectedDataComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/DefaultAddToPortfolioPreselectedDataComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl +package com.tangem.features.commonfeatures.impl.addtoportfolio import androidx.compose.runtime.Composable import com.arkivanov.decompose.extensions.compose.subscribeAsState @@ -10,9 +10,9 @@ import com.tangem.core.decompose.context.child import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.features.account.PortfolioSelectorComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioPreselectedDataModel -import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddToPortfolioRoutes +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioPreselectedDataModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/TokenActionsComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/TokenActionsComponent.kt similarity index 83% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/TokenActionsComponent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/TokenActionsComponent.kt index 9498c4041f..4f2af5aa01 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/TokenActionsComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/TokenActionsComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl +package com.tangem.features.commonfeatures.impl.addtoportfolio import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -8,6 +8,7 @@ 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.common.ui.markets.action.CryptoCurrencyData import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.factory.ComponentFactory @@ -15,10 +16,9 @@ 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.feed.components.market.details.portfolio.add.impl.model.TokenActionsModel -import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.TokenActionsContent -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.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.TokenActionsModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.TokenActionsContent import com.tangem.features.tokenreceive.TokenReceiveComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -64,7 +64,7 @@ internal class TokenActionsComponent @AssistedInject constructor( data class Params( val eventBuilder: PortfolioAnalyticsEvent.EventBuilder, - val data: Flow, + val data: Flow, val callbacks: Callbacks, ) diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/analytics/EarnAnalyticsEvent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/analytics/EarnAnalyticsEvent.kt new file mode 100644 index 0000000000..5980f4c26c --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/analytics/EarnAnalyticsEvent.kt @@ -0,0 +1,35 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.analytics.models.AnalyticsParam + +// todo swap unify with PortfolioAnalyticsEvent, AddToPortfolioFlow +internal sealed class EarnAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Earn", event = event, params = params) { + + data class AddTokenScreenOpened( + private val tokenSymbol: String, + private val blockchain: String, + private val source: String, + ) : EarnAnalyticsEvent( + event = "Add Token Screen Opened", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.BLOCKCHAIN to blockchain, + AnalyticsParam.SOURCE to source, + ), + ) + + data class TokenAdded( + private val tokenSymbol: String, + private val blockchain: String, + ) : EarnAnalyticsEvent( + event = "Token Added", + params = mapOf( + AnalyticsParam.TOKEN_PARAM to tokenSymbol, + AnalyticsParam.BLOCKCHAIN to blockchain, + ), + ) +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/analytics/PortfolioAnalyticsEvent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/analytics/PortfolioAnalyticsEvent.kt new file mode 100644 index 0000000000..6f72186a1b --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/analytics/PortfolioAnalyticsEvent.kt @@ -0,0 +1,91 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.analytics + +import com.tangem.common.ui.markets.action.TokenActionsBSContentUM +import com.tangem.core.analytics.models.AnalyticsEvent + +// todo swap unify with EarnAnalyticsEvent, AddToPortfolioFlow +internal class PortfolioAnalyticsEvent( + event: String, + params: Map = emptyMap(), +) : AnalyticsEvent(category = "Markets / Chart", event = event, params = params) { + + data class EventBuilder( + val tokenSymbol: String, + val source: String?, + ) { + + fun popupToChooseAccount() = PortfolioAnalyticsEvent( + event = "Choose Account Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun popupToConfirm() = PortfolioAnalyticsEvent( + event = "Add Token Screen Opened", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToNotMainAccount() = PortfolioAnalyticsEvent( + event = "Button - Add To Account", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addButtonClick() = PortfolioAnalyticsEvent( + event = "Button - Add Token", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( + event = "Wallet Selected", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( + event = "Token Network Selected", + params = buildMap { + put("Count", blockchainNames.size.toString()) + put("Token", tokenSymbol) + put("blockchain", blockchainNames.joinToString(separator = ", ")) + if (source != null) put("Source", source) + }, + ) + + fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( + event = "Token Added", + params = buildMap { + put("Token", tokenSymbol) + put("Blockchain", blockchainName) + if (source != null) put("Source", source) + }, + ) + + 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" + }, + params = buildMap { + if (source != null) put("Source", source) + }, + ) + + fun getTokenLater() = PortfolioAnalyticsEvent( + event = "Popup Get token - Button Later", + params = buildMap { + if (source != null) put("Source", source) + }, + ) + } +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/converter/AvailableToAddDataConverter.kt similarity index 92% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/converter/AvailableToAddDataConverter.kt index 946ab12577..32f382fdcb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/converter/AvailableToAddDataConverter.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/converter/AvailableToAddDataConverter.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.converter +package com.tangem.features.commonfeatures.impl.addtoportfolio.converter import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase @@ -13,9 +13,9 @@ 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.feed.components.market.details.portfolio.add.AvailableToAddAccount -import com.tangem.features.feed.components.market.details.portfolio.add.AvailableToAddData -import com.tangem.features.feed.components.market.details.portfolio.add.AvailableToAddWallet +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddAccount +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData +import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddWallet import javax.inject.Inject internal class AvailableToAddDataConverter @Inject constructor( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/BlockchainRowUMConverter.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/converter/BlockchainRowUMConverter.kt similarity index 79% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/BlockchainRowUMConverter.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/converter/BlockchainRowUMConverter.kt index e4be2f3924..ed70205959 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/BlockchainRowUMConverter.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/converter/BlockchainRowUMConverter.kt @@ -1,15 +1,14 @@ -package com.tangem.features.feed.components.market.details.portfolio.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.converter 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.domain.markets.TokenMarketInfo.Network import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.converter.Converter /** - * Converter from [TokenMarketInfo.Network] to [BlockchainRowUM] + * Converter from [com.tangem.domain.markets.TokenMarketInfo.Network] to [com.tangem.core.ui.components.rows.model.BlockchainRowUM] * * @property alreadyAddedNetworks set of already added networks * @@ -17,9 +16,9 @@ import com.tangem.utils.converter.Converter */ internal class BlockchainRowUMConverter( private val alreadyAddedNetworks: Set, -) : Converter, BlockchainRowUM> { +) : Converter, BlockchainRowUM> { - override fun convert(value: Pair): BlockchainRowUM { + override fun convert(value: Pair): BlockchainRowUM { val (network, isSelected) = value val blockchainInfo = BlockchainUtils.getNetworkInfo(networkId = network.networkId) @@ -48,7 +47,10 @@ internal class BlockchainRowUMConverter( ) } - private fun getNetworkType(network: Network, blockchainInfo: BlockchainUtils.BlockchainInfo): String { + 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 diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt new file mode 100644 index 0000000000..871d7f7c44 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioComponentModule.kt @@ -0,0 +1,28 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.di + +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.DefaultAddToPortfolioPreselectedDataComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.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 + + @Binds + fun bindAddToPortfolioPreselectedDataComponent( + factory: DefaultAddToPortfolioPreselectedDataComponent.Factory, + ): AddToPortfolioPreselectedDataComponent.Factory +} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioModelModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt similarity index 66% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioModelModule.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt index 60b3f51d91..d0e2845d69 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioModelModule.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/di/AddToPortfolioModelModule.kt @@ -1,8 +1,12 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.di +package com.tangem.features.commonfeatures.impl.addtoportfolio.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model -import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.* +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioPreselectedDataModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.ChooseNetworkModel +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.TokenActionsModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt similarity index 94% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt index ce12bfc05a..dec0e1f72b 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioModel.kt @@ -1,9 +1,11 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.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.common.ui.markets.action.CryptoCurrencyData +import com.tangem.common.ui.markets.action.QuickActionsConverter.toQuickActions import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -19,22 +21,20 @@ 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.feed.components.market.details.portfolio.add.* -import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.ChooseNetworkComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.TokenActionsComponent -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.model.PortfolioTokenUMConverter.Companion.toQuickActions -import com.tangem.features.feed.impl.R +import com.tangem.features.commonfeatures.api.addtoportfolio.* +import com.tangem.features.commonfeatures.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.ChooseNetworkComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger 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 com.tangem.utils.logging.TangemLogger import javax.inject.Inject private const val TOKEN_ACTIONS_DELAY = 500L @@ -62,7 +62,7 @@ internal class AddToPortfolioModel @Inject constructor( /* Flows that hold state and provide it to child models */ val selectedNetwork: MutableSharedFlow = replayMutableSharedFlow() val selectedPortfolio: MutableSharedFlow = replayMutableSharedFlow() - val tokenActionsData: MutableSharedFlow = replayMutableSharedFlow() + val tokenActionsData: MutableSharedFlow = replayMutableSharedFlow() private val addToPortfolioManager = params.addToPortfolioManager val portfolioFetcher = addToPortfolioManager.portfolioFetcher @@ -255,7 +255,7 @@ internal class AddToPortfolioModel @Inject constructor( private fun setupTokenActionsFlow( selectedPortfolio: SelectedPortfolio, addedToken: CryptoCurrencyStatus, - ): Flow { + ): Flow { val timeFlow = channelFlow { val timerJob = launch { delay(TOKEN_ACTIONS_DELAY) } getCryptoCurrencyActionsUseCase( @@ -275,7 +275,7 @@ internal class AddToPortfolioModel @Inject constructor( }.collect() } return timeFlow.map { actionsState -> - PortfolioData.CryptoCurrencyData( + CryptoCurrencyData( userWallet = selectedPortfolio.userWallet, status = actionsState.cryptoCurrencyStatus, actions = actionsState.states, diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioPreselectedDataModel.kt similarity index 95% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioPreselectedDataModel.kt index a28a4fd173..10e935ada2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioPreselectedDataModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioPreselectedDataModel.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.model import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.replaceAll @@ -22,17 +22,19 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.features.account.PortfolioFetcher import com.tangem.features.account.PortfolioSelectorController -import com.tangem.features.feed.components.market.details.portfolio.add.* -import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent -import com.tangem.features.feed.components.market.details.portfolio.impl.analytics.PortfolioAnalyticsEvent -import com.tangem.features.feed.impl.R -import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent +import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.* +import com.tangem.features.commonfeatures.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.EarnAnalyticsEvent +import com.tangem.features.commonfeatures.impl.addtoportfolio.analytics.PortfolioAnalyticsEvent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.* import java.math.BigDecimal import javax.inject.Inject +import kotlin.collections.get +import kotlin.collections.mapNotNull @Suppress("LongParameterList") internal class AddToPortfolioPreselectedDataModel @Inject constructor( @@ -163,7 +165,7 @@ internal class AddToPortfolioPreselectedDataModel @Inject constructor( } /** - * Creates [AvailableToAddData] for preselected network when token is already added in all networks. + * Creates [com.tangem.features.commonfeatures.api.AvailableToAddData] for preselected network when token is already added in all networks. * This allows user to select wallet/account and do smth after it with selected info. */ private suspend fun createAvailableToAddDataForPreselectedNetwork( diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioRoutes.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt similarity index 78% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioRoutes.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt index a8c2acb3aa..e548f5b16f 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddToPortfolioRoutes.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddToPortfolioRoutes.kt @@ -1,8 +1,8 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.model import androidx.compose.runtime.Immutable import com.tangem.core.decompose.navigation.Route -import com.tangem.features.feed.components.market.details.portfolio.add.SelectedPortfolio +import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedPortfolio import kotlinx.serialization.Serializable @Serializable diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt similarity index 92% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt index 17d27f1aad..1ebff809de 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenModel.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.model import com.tangem.common.ui.addtoken.AddTokenUM import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -13,11 +13,11 @@ 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.feed.components.market.details.portfolio.add.SelectedNetwork -import com.tangem.features.feed.components.market.details.portfolio.add.SelectedPortfolio -import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.model.AddTokenUiBuilder.Companion.toggleProgress -import com.tangem.features.feed.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddTokenUiBuilder.Companion.toggleProgress +import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedNetwork +import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedPortfolio +import com.tangem.features.commonfeatures.impl.R import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt similarity index 91% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt index 2055a204ab..3498931b1d 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/AddTokenUiBuilder.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/AddTokenUiBuilder.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.model import com.tangem.common.ui.account.AccountIconUM import com.tangem.common.ui.account.CryptoPortfolioIconConverter @@ -14,10 +14,10 @@ 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.feed.components.market.details.portfolio.add.SelectedNetwork -import com.tangem.features.feed.components.market.details.portfolio.add.SelectedPortfolio -import com.tangem.features.feed.components.market.details.portfolio.add.impl.AddTokenComponent -import com.tangem.features.feed.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.AddTokenComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedNetwork +import com.tangem.features.commonfeatures.api.addtoportfolio.SelectedPortfolio +import com.tangem.features.commonfeatures.impl.R import javax.inject.Inject @ModelScoped diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/CheckCurrencyUnsupportedDelegate.kt similarity index 96% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/CheckCurrencyUnsupportedDelegate.kt index 8b32b2497b..2bc12e52e3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/CheckCurrencyUnsupportedDelegate.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/CheckCurrencyUnsupportedDelegate.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.model import arrow.core.getOrElse import com.tangem.core.decompose.ui.UiMessageSender @@ -10,7 +10,7 @@ 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.feed.impl.R +import com.tangem.features.commonfeatures.impl.R import com.tangem.utils.logging.TangemLogger import javax.inject.Inject diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/ChooseNetworkModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/ChooseNetworkModel.kt similarity index 85% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/ChooseNetworkModel.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/ChooseNetworkModel.kt index 5dc3fc5a62..afc9170d74 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/ChooseNetworkModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/ChooseNetworkModel.kt @@ -1,13 +1,13 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.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.feed.components.market.details.portfolio.add.impl.ChooseNetworkComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state.ChooseNetworkUM -import com.tangem.features.feed.components.market.details.portfolio.impl.model.BlockchainRowUMConverter +import com.tangem.features.commonfeatures.impl.addtoportfolio.ChooseNetworkComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.converter.BlockchainRowUMConverter +import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.ChooseNetworkUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/TokenActionsModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsModel.kt similarity index 86% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/TokenActionsModel.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsModel.kt index 65e1cf28d1..2a9e691f46 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/TokenActionsModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsModel.kt @@ -1,7 +1,9 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.model import com.arkivanov.decompose.router.slot.SlotNavigation import com.arkivanov.decompose.router.slot.activate +import com.tangem.common.ui.markets.action.TokenActionsBSContentUM +import com.tangem.common.ui.markets.action.TokenActionsHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -10,10 +12,8 @@ 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.feed.components.market.details.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.feed.components.market.details.portfolio.impl.model.TokenActionsHandler -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM +import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.SharingStarted diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/TokenActionsUiBuilder.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsUiBuilder.kt similarity index 65% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/TokenActionsUiBuilder.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsUiBuilder.kt index 8828010452..5b12b3efd4 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/model/TokenActionsUiBuilder.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/model/TokenActionsUiBuilder.kt @@ -1,16 +1,16 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.model +package com.tangem.features.commonfeatures.impl.addtoportfolio.model +import com.tangem.common.ui.markets.action.CryptoCurrencyData +import com.tangem.common.ui.markets.action.QuickActionsConverter.quickActions +import com.tangem.common.ui.markets.action.TokenActionsHandler 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.feed.components.market.details.portfolio.add.impl.TokenActionsComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.feed.components.market.details.portfolio.impl.loader.PortfolioData -import com.tangem.features.feed.components.market.details.portfolio.impl.model.PortfolioTokenUMConverter -import com.tangem.features.feed.components.market.details.portfolio.impl.model.TokenActionsHandler +import com.tangem.features.commonfeatures.impl.addtoportfolio.TokenActionsComponent +import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM import javax.inject.Inject @ModelScoped @@ -20,7 +20,7 @@ internal class TokenActionsUiBuilder @Inject constructor( ) { private val params = paramsContainer.require() - fun build(data: PortfolioData.CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): TokenActionsUM { + fun build(data: CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler): TokenActionsUM { val status = data.status val tokenUM = TokenItemState.Content( id = status.currency.id.value, @@ -38,7 +38,7 @@ internal class TokenActionsUiBuilder @Inject constructor( analyticsEventHandler.send(params.eventBuilder.getTokenLater()) params.callbacks.onLaterClick() }, - quickActions = PortfolioTokenUMConverter.quickActions(data, tokenActionsHandler), + quickActions = quickActions(data, tokenActionsHandler), ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/ChooseNetworkContent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/ChooseNetworkContent.kt similarity index 94% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/ChooseNetworkContent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/ChooseNetworkContent.kt index 7b92e78e3b..a682ee6396 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/ChooseNetworkContent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/ChooseNetworkContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui +package com.tangem.features.commonfeatures.impl.addtoportfolio.ui import android.content.res.Configuration import androidx.compose.foundation.background @@ -24,8 +24,8 @@ 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.feed.components.market.details.portfolio.add.impl.ui.state.ChooseNetworkUM -import com.tangem.features.feed.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.ChooseNetworkUM +import com.tangem.features.commonfeatures.impl.R import kotlinx.collections.immutable.persistentListOf import java.util.UUID diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/DefaultAddToPortfolioManager.kt similarity index 90% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/DefaultAddToPortfolioManager.kt index 8c27a3929d..0093289a88 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/DefaultAddToPortfolioManager.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/DefaultAddToPortfolioManager.kt @@ -1,10 +1,10 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui +package com.tangem.features.commonfeatures.impl.addtoportfolio.ui import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.markets.TokenMarketParams import com.tangem.features.account.PortfolioFetcher -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager -import com.tangem.features.feed.components.market.details.portfolio.add.impl.converter.AvailableToAddDataConverter +import com.tangem.features.commonfeatures.impl.addtoportfolio.converter.AvailableToAddDataConverter +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.assisted.Assisted import dagger.assisted.AssistedFactory diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/TokenActionsContent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContent.kt similarity index 94% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/TokenActionsContent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContent.kt index d88388bb30..23d8df105c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/TokenActionsContent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/TokenActionsContent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui +package com.tangem.features.commonfeatures.impl.addtoportfolio.ui import android.content.res.Configuration import androidx.compose.foundation.ExperimentalFoundationApi @@ -22,6 +22,8 @@ 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.common.ui.markets.action.QuickActionUM +import com.tangem.common.ui.markets.action.QuickActions import com.tangem.core.ui.components.SecondaryButton import com.tangem.core.ui.components.SpacerH import com.tangem.core.ui.components.SpacerH16 @@ -37,10 +39,8 @@ 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.feed.components.market.details.portfolio.add.impl.ui.state.TokenActionsUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.QuickActionUM -import com.tangem.features.feed.impl.R +import com.tangem.features.commonfeatures.impl.R +import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM import kotlinx.collections.immutable.persistentListOf import java.util.UUID @@ -188,7 +188,7 @@ private class TokenActionsContentPreviewProvider : PreviewParameterProvider get() = sequenceOf( TokenActionsUM( - quickActions = PortfolioTokenUM.QuickActions( + quickActions = QuickActions( actions = persistentListOf( QuickActionUM.Buy, QuickActionUM.Exchange(shouldShowBadge = true), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/state/ChooseNetworkUM.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/state/ChooseNetworkUM.kt similarity index 73% rename from features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/state/ChooseNetworkUM.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/state/ChooseNetworkUM.kt index 8f54e3cb3b..89fde91bde 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/state/ChooseNetworkUM.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/state/ChooseNetworkUM.kt @@ -1,4 +1,4 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state +package com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state import com.tangem.core.ui.components.rows.model.BlockchainRowUM import kotlinx.collections.immutable.ImmutableList diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/state/TokenActionsUM.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/state/TokenActionsUM.kt new file mode 100644 index 0000000000..b0b0b9b56e --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addtoportfolio/ui/state/TokenActionsUM.kt @@ -0,0 +1,10 @@ +package com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state + +import com.tangem.common.ui.markets.action.QuickActions +import com.tangem.core.ui.components.token.state.TokenItemState + +internal data class TokenActionsUM( + val token: TokenItemState, + val quickActions: QuickActions, + val onLaterClick: () -> Unit, +) \ No newline at end of file diff --git a/features/feed/impl/build.gradle.kts b/features/feed/impl/build.gradle.kts index 41a5739506..f5d80f4224 100644 --- a/features/feed/impl/build.gradle.kts +++ b/features/feed/impl/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { api(projects.features.tokenRecieve.api) api(projects.features.wallet.api) api(projects.features.account.api) + api(projects.features.commonFeatures.api) implementation(projects.features.promoBanners.api) /* Data */ diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index fb93121de6..ef34cba3d1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -11,7 +11,7 @@ import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams import com.tangem.features.feed.components.market.details.DefaultMarketsTokenDetailsComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent import com.tangem.features.feed.components.market.list.DefaultMarketsTokenListComponent diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt index ac8f5fffdc..86bb48edfb 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/earn/DefaultEarnComponent.kt @@ -34,7 +34,7 @@ import com.tangem.core.ui.res.LocalMainBottomSheetColor import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.features.feed.components.feed.FeedBottomSheetRoute -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.earn.EarnModel import com.tangem.features.feed.ui.components.FeedSearchBar import com.tangem.features.feed.ui.earn.EarnContent diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt index 4f06480af4..24bd9e7eb6 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/DefaultFeedComponent.kt @@ -19,8 +19,8 @@ import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.core.ui.decompose.EmptyComposableBottomSheetComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent.Params +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent.Params import com.tangem.features.feed.model.feed.FeedComponentModel import com.tangem.features.feed.model.feed.FeedModelClickIntents import com.tangem.features.feed.ui.feed.FeedList diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt index fea9189d03..ecbaa6c87a 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/feed/FeedBottomSheetRoute.kt @@ -2,7 +2,7 @@ package com.tangem.features.feed.components.feed import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent import com.tangem.features.feed.components.earn.EarnTypeFilterComponent -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent internal sealed interface FeedBottomSheetRoute { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt deleted file mode 100644 index 1233c2be6a..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/di/AddToPortfolioComponentModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.di - -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.add.AddToPortfolioPreselectedDataComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioComponent -import com.tangem.features.feed.components.market.details.portfolio.add.impl.DefaultAddToPortfolioPreselectedDataComponent -import com.tangem.features.feed.components.market.details.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 - - @Binds - fun bindAddToPortfolioPreselectedDataComponent( - factory: DefaultAddToPortfolioPreselectedDataComponent.Factory, - ): AddToPortfolioPreselectedDataComponent.Factory -} \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/state/TokenActionsUM.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/state/TokenActionsUM.kt deleted file mode 100644 index d90e3ca130..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/add/impl/ui/state/TokenActionsUM.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.features.feed.components.market.details.portfolio.add.impl.ui.state - -import com.tangem.core.ui.components.token.state.TokenItemState -import com.tangem.features.feed.components.market.details.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/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 a1cf255e9b..b10ad7940d 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 @@ -14,7 +14,7 @@ import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.model.getOrCreateModel import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.markets.TokenMarketInfo -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.impl.model.MarketsPortfolioModel import com.tangem.features.feed.components.market.details.portfolio.impl.model.MarketsPortfolioRoute diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt index 87ed9607b7..80c831f780 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/analytics/PortfolioAnalyticsEvent.kt @@ -1,7 +1,7 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.analytics +import com.tangem.common.ui.markets.action.TokenActionsBSContentUM import com.tangem.core.analytics.models.AnalyticsEvent -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM internal class PortfolioAnalyticsEvent( event: String, @@ -21,60 +21,6 @@ internal class PortfolioAnalyticsEvent( }, ) - fun popupToChooseAccount() = PortfolioAnalyticsEvent( - event = "Choose Account Opened", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun popupToConfirm() = PortfolioAnalyticsEvent( - event = "Add Token Screen Opened", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun addToNotMainAccount() = PortfolioAnalyticsEvent( - event = "Button - Add To Account", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun addButtonClick() = PortfolioAnalyticsEvent( - event = "Button - Add Token", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun addToPortfolioWalletChanged() = PortfolioAnalyticsEvent( - event = "Wallet Selected", - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun addToPortfolioContinue(blockchainNames: List) = PortfolioAnalyticsEvent( - event = "Token Network Selected", - params = buildMap { - put("Count", blockchainNames.size.toString()) - put("Token", tokenSymbol) - put("blockchain", blockchainNames.joinToString(separator = ", ")) - if (source != null) put("Source", source) - }, - ) - - fun tokenAdded(blockchainName: String) = PortfolioAnalyticsEvent( - event = "Token Added", - params = buildMap { - put("Token", tokenSymbol) - put("Blockchain", blockchainName) - if (source != null) put("Source", source) - }, - ) - fun quickActionClick(actionUM: TokenActionsBSContentUM.Action, blockchainName: String) = PortfolioAnalyticsEvent( event = when (actionUM) { @@ -91,25 +37,5 @@ internal class PortfolioAnalyticsEvent( 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" - }, - params = buildMap { - if (source != null) put("Source", source) - }, - ) - - fun getTokenLater() = PortfolioAnalyticsEvent( - event = "Popup Get token - Button Later", - params = buildMap { - if (source != null) put("Source", source) - }, - ) } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioData.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioData.kt deleted file mode 100644 index 044a0ef229..0000000000 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/loader/PortfolioData.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.tangem.features.feed.components.market.details.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/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt index e1ed13a89e..0b21fb11f3 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/MarketsPortfolioDelegate.kt @@ -5,6 +5,8 @@ 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.common.ui.markets.action.CryptoCurrencyData +import com.tangem.common.ui.markets.action.TokenActionsHandler import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.account.models.AccountStatusList @@ -28,7 +30,6 @@ 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.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.PortfolioHeader import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioListItem @@ -248,7 +249,7 @@ internal class MarketsPortfolioDelegate @AssistedInject constructor( accountWithAdded.addedCurrency.forEach { currencyStatus -> val actions = allActions[currencyStatus.currency]?.states.orEmpty() - val value = PortfolioData.CryptoCurrencyData( + val value = CryptoCurrencyData( userWallet = userWallet, status = currencyStatus, actions = actions, 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 0401beb1e7..93b2b5629a 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,6 +5,8 @@ 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.markets.action.TokenActionsBSContentUM +import com.tangem.common.ui.markets.action.TokenActionsHandler import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model @@ -14,12 +16,11 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.transaction.usecase.ReceiveAddressesFactory -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.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.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.ui.state.MyPortfolioUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.utils.Provider import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioTokenUMConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioTokenUMConverter.kt index 2a078e38ed..5474899d5c 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioTokenUMConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/model/PortfolioTokenUMConverter.kt @@ -1,19 +1,16 @@ package com.tangem.features.feed.components.market.details.portfolio.impl.model +import com.tangem.common.ui.markets.action.CryptoCurrencyData +import com.tangem.common.ui.markets.action.QuickActionsConverter.quickActions +import com.tangem.common.ui.markets.action.TokenActionsHandler 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.feed.components.market.details.portfolio.impl.loader.PortfolioData import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.PortfolioTokenUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.QuickActionUM -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.TokenActionsBSContentUM import com.tangem.utils.converter.Converter -import kotlinx.collections.immutable.toImmutableList /** * Converter from [UserWallet] and [CryptoCurrencyStatus] to [PortfolioTokenUM] @@ -25,10 +22,10 @@ internal class PortfolioTokenUMConverter( private val isBalanceHidden: Boolean, private val onTokenItemClick: (CryptoCurrencyStatus) -> Unit, private val tokenActionsHandler: TokenActionsHandler, -) : Converter { +) : Converter { fun convertV2( - value: PortfolioData.CryptoCurrencyData, + value: CryptoCurrencyData, isQuickActionsShown: Boolean, onTokenItemClick: (UserWallet, CryptoCurrencyStatus) -> Unit, ): PortfolioTokenUM { @@ -45,7 +42,7 @@ internal class PortfolioTokenUMConverter( ) } - override fun convert(value: PortfolioData.CryptoCurrencyData): PortfolioTokenUM { + override fun convert(value: CryptoCurrencyData): PortfolioTokenUM { val tokenItemStateConverter = TokenItemStateConverter( appCurrency = appCurrency, titleStateProvider = { TokenItemState.TitleState.Content(text = stringReference(value.userWallet.name)) }, @@ -63,62 +60,4 @@ internal class PortfolioTokenUMConverter( 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 = { actionUM -> - if (actionUM == 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(action.shouldShowBadge) - is TokenActionsState.ActionState.Receive -> QuickActionUM.Receive - is TokenActionsState.ActionState.Stake -> QuickActionUM.Stake - is TokenActionsState.ActionState.YieldMode -> QuickActionUM.YieldMode(action.apy) - else -> null - }?.let(::add) - } - } - }.toImmutableList() - } } \ 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/PortfolioQuickActions.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioQuickActions.kt index 2260f52c5d..b1ad93e9b2 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioQuickActions.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/portfolio/impl/ui/PortfolioQuickActions.kt @@ -28,6 +28,7 @@ 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.common.ui.markets.action.QuickActionUM import com.tangem.core.ui.components.SpacerH4 import com.tangem.core.ui.components.icons.badge.drawBadge import com.tangem.core.ui.extensions.resolveReference @@ -36,7 +37,6 @@ import com.tangem.core.ui.res.LocalHapticManager import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.test.MarketTokenDetailsBottomSheetTestTags -import com.tangem.features.feed.components.market.details.portfolio.impl.ui.state.QuickActionUM import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf 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 f84d983885..ebc36fbbd5 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,6 +3,8 @@ 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.common.ui.markets.action.QuickActionUM +import com.tangem.common.ui.markets.action.QuickActions 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 @@ -125,7 +127,7 @@ internal class PreviewMyPortfolioUMProvider : PreviewParameterProvider, - val onQuickActionClick: (QuickActionUM) -> Unit, - val onQuickActionLongClick: (QuickActionUM) -> Unit, - ) } \ No newline at end of file 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 ac71423894..b69171f8f3 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 @@ -25,7 +25,7 @@ import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.earn.EarnNetworkFilterComponent import com.tangem.features.feed.components.earn.EarnTypeFilterComponent import com.tangem.features.feed.components.feed.FeedBottomSheetRoute -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkConverter import com.tangem.features.feed.model.earn.filters.state.EarnFilterNetworkUMConverter 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 3ff24b4467..5369759688 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 @@ -50,30 +50,6 @@ internal sealed class EarnAnalyticsEvent( ), ) - data class AddTokenScreenOpened( - private val tokenSymbol: String, - private val blockchain: String, - private val source: String, - ) : EarnAnalyticsEvent( - event = "Add Token Screen Opened", - params = mapOf( - AnalyticsParam.TOKEN_PARAM to tokenSymbol, - AnalyticsParam.BLOCKCHAIN to blockchain, - AnalyticsParam.SOURCE to source, - ), - ) - - data class TokenAdded( - private val tokenSymbol: String, - private val blockchain: String, - ) : EarnAnalyticsEvent( - event = "Token Added", - params = mapOf( - AnalyticsParam.TOKEN_PARAM to tokenSymbol, - AnalyticsParam.BLOCKCHAIN to blockchain, - ), - ) - data class BestOpportunitiesLoadError( private val code: Int?, private val message: String, 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 33fc8bb216..170b29bdae 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 @@ -31,7 +31,7 @@ import com.tangem.domain.news.usecase.FetchTrendingNewsUseCase import com.tangem.domain.news.usecase.ManageTrendingNewsUseCase import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.feed.FeedBottomSheetRoute -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioPreselectedDataComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioPreselectedDataComponent import com.tangem.features.feed.entry.featuretoggle.FeedFeatureToggle import com.tangem.features.feed.impl.R import com.tangem.features.feed.model.earn.analytics.EarnAnalyticsEvent diff --git a/features/onramp/impl/build.gradle.kts b/features/onramp/impl/build.gradle.kts index 4c6b3a90a3..4d66407671 100644 --- a/features/onramp/impl/build.gradle.kts +++ b/features/onramp/impl/build.gradle.kts @@ -12,6 +12,9 @@ android { } dependencies { + /** Api */ + implementation(projects.features.commonFeatures.api) + /** Project - API */ implementation(projects.features.account.api) implementation(projects.features.onramp.api) 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 ee2dc2dd5b..f09d27300f 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 @@ -15,7 +15,7 @@ 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.commonfeatures.api.addtoportfolio.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 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 bae0b5ba51..e8d885de0e 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 @@ -7,8 +7,8 @@ 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.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.features.onramp.swap.availablepairs.model.AddToPortfolioRoute import com.tangem.features.onramp.tokenlist.entity.TokenListUM import kotlinx.coroutines.flow.StateFlow 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 27e528cd04..23816a7b3a 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 @@ -6,8 +6,8 @@ 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.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.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 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 e949b69cf7..cd1e38a700 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 @@ -41,8 +41,8 @@ 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.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.features.onramp.impl.R import com.tangem.features.onramp.swap.availablepairs.AvailableSwapPairsComponent import com.tangem.features.onramp.swap.availablepairs.entity.transformers.SetErrorWarningTransformer diff --git a/features/swap/impl/build.gradle.kts b/features/swap/impl/build.gradle.kts index 6cd1167b8e..ca51e0e384 100644 --- a/features/swap/impl/build.gradle.kts +++ b/features/swap/impl/build.gradle.kts @@ -12,6 +12,9 @@ android { } dependencies { + /** Api */ + implementation(projects.features.commonFeatures.api) + /** Core modules */ implementation(projects.core.analytics) implementation(projects.core.analytics.models) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt index 62bd2f26e5..b92904c2d7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/DefaultChooseTokenComponent.kt @@ -15,15 +15,15 @@ import com.tangem.feature.swap.choosetoken.api.ChooseTokenComponent import com.tangem.feature.swap.choosetoken.impl.model.ChooseTokenModel import com.tangem.feature.swap.models.AddToPortfolioRoute import com.tangem.feature.swap.ui.SwapSelectTokenScreen -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject internal class DefaultChooseTokenComponent @AssistedInject constructor( + private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, @Assisted appComponentContext: AppComponentContext, @Assisted private val params: ChooseTokenComponent.Params, - private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, ) : AppComponentContext by appComponentContext, ChooseTokenComponent { private val model: ChooseTokenModel = getOrCreateModel(params) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt index 0766482b98..12459d97e7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/ChooseTokenModel.kt @@ -23,7 +23,7 @@ import com.tangem.feature.swap.converters.TokensDataConverter import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.market.state.SwapMarketState import com.tangem.feature.swap.presentation.R -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt index 0a0340e417..2c66851ed2 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/choosetoken/impl/model/MarketBlockDelegate.kt @@ -17,7 +17,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase import com.tangem.feature.swap.models.AddToPortfolioRoute import com.tangem.feature.swap.models.market.MarketsListBatchFlowManager import com.tangem.feature.swap.models.market.state.SwapMarketState -import com.tangem.features.feed.components.market.details.portfolio.add.AddToPortfolioManager +import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioManager import com.tangem.lib.crypto.BlockchainUtils import com.tangem.utils.Provider import com.tangem.utils.coroutines.JobHolder @@ -32,9 +32,9 @@ import kotlinx.coroutines.launch @Suppress("LongParameterList") internal class MarketBlockDelegate @AssistedInject constructor( private val marketsListBatchFlowManagerFactory: MarketsListBatchFlowManager.Factory, - private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, private val excludedBlockchains: ExcludedBlockchains, private val getUserWalletsUseCase: GetWalletsUseCase, + private val addToPortfolioManagerFactory: AddToPortfolioManager.Factory, @Assisted private val modelScope: CoroutineScope, @Assisted private val searchQueryState: StateFlow, @Assisted private val screensSourcesName: String, diff --git a/settings.gradle.kts b/settings.gradle.kts index 35213a3a98..1938b7587c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -321,6 +321,9 @@ include(":features:virtual-accounts:main:impl") include(":features:virtual-accounts:details:api") include(":features:virtual-accounts:details:impl") + +include(":features:common-features:api") +include(":features:common-features:impl") // endregion Feature modules // region Domain modules From 11d218a155249029c82e1f11f88ccb6a37711144 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2026 17:10:04 +0400 Subject: [PATCH 19/32] Updated on 2026-08-14 --- .../tangem/datasource/api/pay/TangemPayApi.kt | 6 + .../request/UpdateCardDisplayNameRequest.kt | 9 + .../pay/models/response/CustomerMeResponse.kt | 1 + .../response/UpdateCardDisplayNameResponse.kt | 14 ++ .../entity/PaymentAccountStatusValueDM.kt | 1 + .../PaymentAccountStatusValueDMConverter.kt | 6 + .../DefaultPaymentAccountStatusFetcher.kt | 2 + .../repository/DefaultOnboardingRepository.kt | 11 +- .../DefaultTangemPayCardDetailsRepository.kt | 22 +++ .../domain/models/account/CardDisplayName.kt | 36 ++++ .../account/PaymentAccountStatusValue.kt | 2 + .../domain/pay/TangemPayDetailsConfig.kt | 2 + .../tangem/domain/pay/model/CustomerInfo.kt | 2 + .../TangemPayCardDetailsRepository.kt | 6 + .../DefaultTangemPayCardPageComponent.kt | 9 +- ...faultTangemPayDetailsContainerComponent.kt | 7 + .../TangemPayAddToWalletComponent.kt | 5 +- .../TangemPayCardPageScreenComponent.kt | 5 +- .../components/TangemPayDetailsComponent.kt | 5 +- .../TangemPayEditDisplayNameComponent.kt | 51 ++++++ .../TangemPayCardDetailsBlockComponent.kt | 5 +- .../tangempay/di/TangemPayModelModule.kt | 6 + .../TangemPayCardDetailsBlockStateFactory.kt | 2 + .../tangempay/entity/TangemPayDetailsUM.kt | 19 ++ .../entity/TangemPayEditDisplayNameUM.kt | 9 + .../model/TangemPayCardDetailsBlockModel.kt | 20 ++- .../model/TangemPayEditDisplayNameModel.kt | 92 ++++++++++ .../navigation/TangemPayDetailsInnerRoute.kt | 3 + .../ui/TangemPayAddToWalletScreen.kt | 1 + .../tangempay/ui/TangemPayCardDetailsBlock.kt | 168 +++++++++++++++++- .../tangempay/ui/TangemPayCardPageScreen.kt | 52 ++++-- .../tangempay/ui/TangemPayDetailsScreen.kt | 2 + .../ui/TangemPayEditDisplayNameScreen.kt | 82 +++++++++ .../TangemPayUpdateInfoStateTransformer.kt | 1 + .../converter/TangemPayMainBlockConverter.kt | 2 + 35 files changed, 636 insertions(+), 30 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardDisplayNameRequest.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/UpdateCardDisplayNameResponse.kt create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/account/CardDisplayName.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayEditDisplayNameComponent.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEditDisplayNameUM.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt index 8f67d01dae..3c1f040b64 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/TangemPayApi.kt @@ -96,4 +96,10 @@ interface TangemPayApi { @Header("Authorization") authHeader: String, @Body body: WithdrawRequest, ): ApiResponse + + @PATCH("v1/card") + suspend fun updateCardDisplayName( + @Header("Authorization") authHeader: String, + @Body body: UpdateCardDisplayNameRequest, + ): ApiResponse } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardDisplayNameRequest.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardDisplayNameRequest.kt new file mode 100644 index 0000000000..b5c9e74e58 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/request/UpdateCardDisplayNameRequest.kt @@ -0,0 +1,9 @@ +package com.tangem.datasource.api.pay.models.request + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UpdateCardDisplayNameRequest( + @Json(name = "display_name") val displayName: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt index 38e8982664..906a69fa12 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/CustomerMeResponse.kt @@ -29,6 +29,7 @@ data class CustomerMeResponse( @Json(name = "status") val status: Status, @Json(name = "updated_at") val updatedAt: String, @Json(name = "payment_account_id") val paymentAccountId: String, + @Json(name = "display_name") val displayName: String?, ) { @JsonClass(generateAdapter = false) enum class Status { diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/UpdateCardDisplayNameResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/UpdateCardDisplayNameResponse.kt new file mode 100644 index 0000000000..feb64ea566 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/api/pay/models/response/UpdateCardDisplayNameResponse.kt @@ -0,0 +1,14 @@ +package com.tangem.datasource.api.pay.models.response + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class UpdateCardDisplayNameResponse( + @Json(name = "result") val result: Result?, +) { + @JsonClass(generateAdapter = true) + data class Result( + @Json(name = "display_name") val displayName: String?, + ) +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt index 7b1ca88f44..98bfec8c62 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/entity/PaymentAccountStatusValueDM.kt @@ -43,6 +43,7 @@ sealed interface PaymentAccountStatusValueDM { @Json(name = "is_pin_set") val isPinSet: Boolean, @Json(name = "fiat_balance") val fiatBalance: FiatBalanceDM, @Json(name = "crypto_balance") val cryptoBalance: CryptoBalanceDM, + @Json(name = "display_name") val displayName: String?, ) : PaymentAccountStatusValueDM @NameLabel("card_issue_failed") diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt index 57eb9b4402..cfa7822955 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/converter/PaymentAccountStatusValueDMConverter.kt @@ -1,8 +1,10 @@ package com.tangem.data.pay.converter import com.tangem.data.pay.entity.TangemPayCurrencyFactory +import arrow.core.getOrElse import com.tangem.datasource.local.visa.entity.PaymentAccountStatusValueDM import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.wallet.UserWalletId import javax.inject.Inject @@ -39,6 +41,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( isPinSet = value.isPinSet, fiatBalance = value.fiatBalance.toDM(), cryptoBalance = value.cryptoBalance.toDM(), + displayName = value.displayName?.value, ) is PaymentAccountStatusValue.Loaded -> PaymentAccountStatusValueDM.ActiveCard( isLocked = false, @@ -50,6 +53,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( isPinSet = value.isPinSet, fiatBalance = value.fiatBalance.toDM(), cryptoBalance = value.cryptoBalance.toDM(), + displayName = value.displayName?.value, ) is PaymentAccountStatusValue.Error.CardIssueFailed -> PaymentAccountStatusValueDM.CardIssueFailed( customerId = value.customerId, @@ -84,6 +88,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( fiatBalance = value.fiatBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(), cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), + displayName = value.displayName?.let { CardDisplayName(it).getOrElse { null } }, ) } else { PaymentAccountStatusValue.Loaded( @@ -97,6 +102,7 @@ internal class PaymentAccountStatusValueDMConverter @Inject constructor( fiatBalance = value.fiatBalance.toDomain(), cryptoBalance = value.cryptoBalance.toDomain(), cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), + displayName = value.displayName?.let { CardDisplayName(it).getOrElse { null } }, ) } is PaymentAccountStatusValueDM.UnderReview -> PaymentAccountStatusValue.UnderReview( 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 index 9e0ae38e91..0f48c011ce 100644 --- 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 @@ -234,6 +234,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( fiatBalance = cardInfo.fiatBalance, cryptoBalance = cardInfo.cryptoBalance, cryptoCurrency = cryptoCurrency, + displayName = productInstance.displayName, ) else -> PaymentAccountStatusValue.Loaded( source = StatusSource.ACTUAL, @@ -246,6 +247,7 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( fiatBalance = cardInfo.fiatBalance, cryptoBalance = cardInfo.cryptoBalance, cryptoCurrency = cryptoCurrency, + displayName = productInstance.displayName, ) } } 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 8253961561..600f2f3ebc 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 @@ -2,6 +2,7 @@ package com.tangem.data.pay.repository import arrow.core.Either import arrow.core.flatMap +import arrow.core.getOrElse import arrow.core.left import arrow.core.right import com.tangem.core.analytics.api.AnalyticsEventHandler @@ -15,6 +16,7 @@ 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.TangemPayEligibilityType +import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.models.wallet.UserWallet @@ -198,7 +200,14 @@ internal class DefaultOnboardingRepository @Inject constructor( } cardFrozenStateStore.store(key = instance.cardId, value = cardFrozenState) - ProductInstance(id = instance.id, cardId = instance.cardId, frozenState = cardFrozenState) + val displayName = instance.displayName?.ifEmpty { null } + + ProductInstance( + id = instance.id, + cardId = instance.cardId, + frozenState = cardFrozenState, + displayName = if (displayName != null) CardDisplayName(displayName).getOrElse { null } else null, + ) } return CustomerInfo( customerId = response?.id, 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 88f54d413e..814476fa34 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 @@ -15,10 +15,12 @@ import com.tangem.datasource.api.pay.TangemPayApi 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.request.UpdateCardDisplayNameRequest 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.account.CardDisplayName import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance @@ -318,6 +320,26 @@ internal class DefaultTangemPayCardDetailsRepository @Inject constructor( } } + override suspend fun updateCardDisplayName( + userWalletId: UserWalletId, + displayName: CardDisplayName, + ): Either { + return catch( + block = { + requestHelper.performRequest(userWalletId) { authHeader -> + tangemPayApi.updateCardDisplayName( + authHeader = authHeader, + body = UpdateCardDisplayNameRequest(displayName = displayName.value), + ) + }.fold( + ifLeft = { error -> error.left() }, + ifRight = { Unit.right() }, + ) + }, + catch = ::catchException, + ) + } + override fun cardFrozenState(cardId: String): Flow { return cardFrozenStateStore.get(cardId) } diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/CardDisplayName.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/CardDisplayName.kt new file mode 100644 index 0000000000..eb0bd72a9c --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/CardDisplayName.kt @@ -0,0 +1,36 @@ +package com.tangem.domain.models.account + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import kotlinx.serialization.Serializable + +@Serializable +@ConsistentCopyVisibility +data class CardDisplayName private constructor(val value: String) { + + @Serializable + sealed interface Error { + @Serializable + data object Empty : Error + + @Serializable + data object ExceedsMaxLength : Error + + @Serializable + data object InvalidCharacters : Error + } + + companion object { + const val MAX_LENGTH = 20 + private val allowedPattern = Regex("^[\\p{L}\\p{N} ]+$") + + operator fun invoke(name: String): Either = either { + val trimmed = name.trim() + ensure(trimmed.isNotEmpty()) { Error.Empty } + ensure(trimmed.length <= MAX_LENGTH) { Error.ExceedsMaxLength } + ensure(allowedPattern.matches(trimmed)) { Error.InvalidCharacters } + CardDisplayName(trimmed) + } + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt index 53beaf23be..8e74069ff6 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/account/PaymentAccountStatusValue.kt @@ -109,6 +109,7 @@ sealed class PaymentAccountStatusValue { val fiatBalance: FiatBalance, val cryptoBalance: CryptoBalance, val cryptoCurrency: CryptoCurrency.Token, + val displayName: CardDisplayName?, ) : PaymentAccountStatusValue() { val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( currency = cryptoCurrency, @@ -157,6 +158,7 @@ sealed class PaymentAccountStatusValue { val fiatBalance: FiatBalance, val cryptoBalance: CryptoBalance, val cryptoCurrency: CryptoCurrency.Token, + val displayName: CardDisplayName?, ) : PaymentAccountStatusValue() { val cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus( currency = cryptoCurrency, 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 e2424a061a..5b2be89b69 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 @@ -1,5 +1,6 @@ package com.tangem.domain.pay +import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.visa.model.TangemPayCardFrozenState import kotlinx.serialization.Serializable @@ -11,4 +12,5 @@ data class TangemPayDetailsConfig( val cardFrozenState: TangemPayCardFrozenState, val cardNumberEnd: String, val chainId: Int, + val displayName: CardDisplayName?, ) \ 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 a35e9adaea..7c93887879 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,5 +1,6 @@ package com.tangem.domain.pay.model +import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.models.account.PaymentAccountStatusValue import com.tangem.domain.models.kyc.KycStatus import com.tangem.domain.visa.model.TangemPayCardFrozenState @@ -49,6 +50,7 @@ data class CustomerInfo( val id: String, val cardId: String, val frozenState: TangemPayCardFrozenState, + val displayName: CardDisplayName?, ) data class CardInfo( diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt index 088b34d193..80aa3ea5c9 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/repository/TangemPayCardDetailsRepository.kt @@ -2,6 +2,7 @@ package com.tangem.domain.pay.repository import arrow.core.Either import com.tangem.core.error.UniversalError +import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.SetPinResult import com.tangem.domain.pay.model.TangemPayCardBalance @@ -31,4 +32,9 @@ interface TangemPayCardDetailsRepository { fun cardFrozenState(cardId: String): Flow suspend fun cardFrozenStateSync(cardId: String): TangemPayCardFrozenState? + + suspend fun updateCardDisplayName( + userWalletId: UserWalletId, + displayName: CardDisplayName, + ): Either } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt index a69dbe675f..7b9e24e79b 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayCardPageComponent.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import com.arkivanov.decompose.ComponentContext import com.arkivanov.decompose.extensions.compose.stack.Children -import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation import com.arkivanov.decompose.extensions.compose.subscribeAsState import com.arkivanov.decompose.router.stack.StackNavigation import com.arkivanov.decompose.router.stack.childStack @@ -45,7 +44,6 @@ internal class DefaultTangemPayCardPageComponent @AssistedInject constructor( val childStack by childStack.subscribeAsState() Children( stack = childStack, - animation = stackAnimation(), ) { child -> child.instance.Content(modifier = modifier) } @@ -76,6 +74,13 @@ internal class DefaultTangemPayCardPageComponent @AssistedInject constructor( config = params.config, ), ) + TangemPayDetailsInnerRoute.EditCardDisplayName -> TangemPayEditDisplayNameComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = TangemPayDetailsContainerComponent.Params( + userWalletId = params.userWalletId, + config = params.config, + ), + ) } private fun onChildBack() { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt index baa27a447b..35ea8d9089 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/DefaultTangemPayDetailsContainerComponent.kt @@ -75,6 +75,13 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), params = params, ) + TangemPayDetailsInnerRoute.EditCardDisplayName -> TangemPayEditDisplayNameComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = TangemPayDetailsContainerComponent.Params( + userWalletId = params.userWalletId, + config = params.config, + ), + ) } private fun onChildBack() { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayAddToWalletComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayAddToWalletComponent.kt index 561ef1f9cf..91d394396e 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayAddToWalletComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayAddToWalletComponent.kt @@ -23,7 +23,10 @@ internal class TangemPayAddToWalletComponent( private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent( appComponentContext = child("cardDetailsBlockComponent"), - params = TangemPayCardDetailsBlockComponent.Params(params = params), + params = TangemPayCardDetailsBlockComponent.Params( + params = params, + isDisplayCardNameEnabled = false, + ), ) @Composable diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt index 975ce82cf3..e0f2994b3c 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayCardPageScreenComponent.kt @@ -34,7 +34,10 @@ internal class TangemPayCardPageScreenComponent( private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent( appComponentContext = child("cardDetailsBlockComponent"), - params = TangemPayCardDetailsBlockComponent.Params(params = containerParams), + params = TangemPayCardDetailsBlockComponent.Params( + params = containerParams, + isDisplayCardNameEnabled = true, + ), ) private val bottomSheetSlot = childSlot( 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 58f5d9ef84..c193b44545 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 @@ -51,7 +51,10 @@ internal class TangemPayDetailsComponent( private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent( appComponentContext = child("cardDetailsBlockComponent"), - params = TangemPayCardDetailsBlockComponent.Params(params = params), + params = TangemPayCardDetailsBlockComponent.Params( + params = params, + isDisplayCardNameEnabled = false, + ), ) private val expressTransactionsComponent by lazy { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayEditDisplayNameComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayEditDisplayNameComponent.kt new file mode 100644 index 0000000000..8b4bb6214d --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/TangemPayEditDisplayNameComponent.kt @@ -0,0 +1,51 @@ +package com.tangem.features.tangempay.components + +import androidx.activity.compose.BackHandler +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child +import com.tangem.core.decompose.model.getOrCreateModel +import com.tangem.core.ui.decompose.ComposableContentComponent +import com.tangem.features.tangempay.components.cardDetails.DefaultTangemPayCardDetailsBlockComponent +import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent +import com.tangem.features.tangempay.entity.DisplayNameState +import com.tangem.features.tangempay.model.TangemPayEditDisplayNameModel +import com.tangem.features.tangempay.ui.TangemPayEditDisplayNameScreen + +internal class TangemPayEditDisplayNameComponent( + private val appComponentContext: AppComponentContext, + params: TangemPayDetailsContainerComponent.Params, +) : AppComponentContext by appComponentContext, ComposableContentComponent { + + private val model: TangemPayEditDisplayNameModel = getOrCreateModel(params) + + private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent( + appComponentContext = child("editDisplayNameCardDetails"), + params = TangemPayCardDetailsBlockComponent.Params(params = params, isDisplayCardNameEnabled = true), + ) + + @Composable + override fun Content(modifier: Modifier) { + val state by model.uiState.collectAsStateWithLifecycle() + val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle() + val editingCardDetailsState = cardDetailsState.copy( + displayNameState = DisplayNameState.Editing( + displayName = state.editingValue, + editingValue = state.editingValue, + onValueChanged = state.onValueChanged, + onSubmit = state.onDoneClick, + onDismiss = state.onDismiss, + ), + ) + BackHandler(onBack = state.onDismiss) + TangemPayEditDisplayNameScreen( + state = state, + cardDetailsBlockComponent = cardDetailsBlockComponent, + cardDetailsState = editingCardDetailsState, + modifier = modifier, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/cardDetails/TangemPayCardDetailsBlockComponent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/cardDetails/TangemPayCardDetailsBlockComponent.kt index 9fb41015c0..9b80de3eb5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/cardDetails/TangemPayCardDetailsBlockComponent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/components/cardDetails/TangemPayCardDetailsBlockComponent.kt @@ -14,5 +14,8 @@ internal interface TangemPayCardDetailsBlockComponent { @Composable fun CardDetailsBlockContent(state: TangemPayCardDetailsUM, modifier: Modifier) - data class Params(val params: TangemPayDetailsContainerComponent.Params) + data class Params( + val params: TangemPayDetailsContainerComponent.Params, + val isDisplayCardNameEnabled: Boolean, + ) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt index d533bce2b9..623eea9c2a 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/di/TangemPayModelModule.kt @@ -8,6 +8,7 @@ import com.tangem.features.tangempay.model.TangemPayCardDetailsBlockModel import com.tangem.features.tangempay.model.TangemPayCardPageModel import com.tangem.features.tangempay.model.TangemPayChangePinModel import com.tangem.features.tangempay.model.TangemPayDetailsModel +import com.tangem.features.tangempay.model.TangemPayEditDisplayNameModel import com.tangem.features.tangempay.model.TangemPayTxHistoryDetailsModel import com.tangem.features.tangempay.model.TangemPayTxHistoryModel import com.tangem.features.tangempay.model.TangemPayViewPinModel @@ -65,4 +66,9 @@ internal interface TangemPayModelModule { @IntoMap @ClassKey(TangemPayCardPageModel::class) fun bindTangemPayCardPageModel(model: TangemPayCardPageModel): Model + + @Binds + @IntoMap + @ClassKey(TangemPayEditDisplayNameModel::class) + fun bindTangemPayEditDisplayNameModel(model: TangemPayEditDisplayNameModel): Model } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardDetailsBlockStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardDetailsBlockStateFactory.kt index e79a05f253..d57030a177 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardDetailsBlockStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayCardDetailsBlockStateFactory.kt @@ -8,6 +8,7 @@ import com.tangem.utils.StringsSigns internal class TangemPayCardDetailsBlockStateFactory( private val cardNumberEnd: String, + private val displayNameState: DisplayNameState?, private val onReveal: () -> Unit, private val onCopy: (String, CardDataType) -> Unit, ) { @@ -22,5 +23,6 @@ internal class TangemPayCardDetailsBlockStateFactory( onCopy = onCopy, isHidden = true, cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = displayNameState, ) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt index f67ed6b7b0..5660713341 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsUM.kt @@ -30,8 +30,27 @@ internal data class TangemPayCardDetailsUM( val isHidden: Boolean = true, val isLoading: Boolean = false, val cardFrozenState: TangemPayCardFrozenState, + val displayNameState: DisplayNameState?, ) +internal sealed interface DisplayNameState { + + val displayName: String + + data class Display( + override val displayName: String, + val onClick: () -> Unit, + ) : DisplayNameState + + data class Editing( + override val displayName: String, + val editingValue: String, + val onValueChanged: (String) -> Unit, + val onSubmit: () -> Unit, + val onDismiss: () -> Unit, + ) : DisplayNameState +} + internal sealed class TangemPayDetailsBalanceBlockState { abstract val actionButtons: ImmutableList diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEditDisplayNameUM.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEditDisplayNameUM.kt new file mode 100644 index 0000000000..a13fdbc1d0 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayEditDisplayNameUM.kt @@ -0,0 +1,9 @@ +package com.tangem.features.tangempay.entity + +internal data class TangemPayEditDisplayNameUM( + val editingValue: String, + val isLoading: Boolean, + val onValueChanged: (String) -> Unit, + val onDoneClick: () -> Unit, + val onDismiss: () -> Unit, +) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt index fcee864f89..13025f10a1 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayCardDetailsBlockModel.kt @@ -5,6 +5,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.TextReference @@ -13,13 +14,13 @@ import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository import com.tangem.domain.tangempay.TangemPayAnalyticsEvents import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.DisplayNameState import com.tangem.features.tangempay.entity.TangemPayCardDetailsBlockStateFactory import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM import com.tangem.features.tangempay.model.listener.CardDetailsEvent import com.tangem.features.tangempay.model.listener.CardDetailsEventListener -import com.tangem.features.tangempay.model.transformers.DetailsHiddenStateTransformer -import com.tangem.features.tangempay.model.transformers.DetailsRevealProgressStateTransformer -import com.tangem.features.tangempay.model.transformers.DetailsRevealedStateTransformer +import com.tangem.features.tangempay.model.transformers.* +import com.tangem.features.tangempay.navigation.TangemPayDetailsInnerRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn @@ -42,12 +43,21 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( private val uiMessageSender: UiMessageSender, private val cardDetailsEventListener: CardDetailsEventListener, private val analytics: AnalyticsEventHandler, + private val router: Router, ) : Model() { private val params: TangemPayCardDetailsBlockComponent.Params = paramsContainer.require() private val stateFactory = TangemPayCardDetailsBlockStateFactory( cardNumberEnd = params.params.config.cardNumberEnd, + displayNameState = if (params.isDisplayCardNameEnabled && params.params.config.displayName != null) { + DisplayNameState.Display( + displayName = requireNotNull(params.params.config.displayName).value, + onClick = ::startEditingDisplayName, + ) + } else { + null + }, onReveal = ::revealCardDetails, onCopy = ::copyData, ) @@ -120,6 +130,10 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( ) } + private fun startEditingDisplayName() { + router.push(TangemPayDetailsInnerRoute.EditCardDisplayName) + } + private fun copyData(text: String, type: CardDataType) { val event = when (type) { CardDataType.Number -> TangemPayAnalyticsEvents.CopyCardNumberClicked() diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt new file mode 100644 index 0000000000..a559c7a3d9 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayEditDisplayNameModel.kt @@ -0,0 +1,92 @@ +package com.tangem.features.tangempay.model + +import androidx.compose.runtime.Stable +import com.tangem.core.decompose.di.ModelScoped +import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.model.ParamsContainer +import com.tangem.core.decompose.navigation.Router +import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.message.DialogMessage +import com.tangem.domain.models.account.CardDisplayName +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.features.tangempay.components.TangemPayDetailsContainerComponent +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM +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 javax.inject.Inject + +@Stable +@ModelScoped +internal class TangemPayEditDisplayNameModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val router: Router, + private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val uiMessageSender: UiMessageSender, +) : Model() { + + private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() + + private val originalDisplayName = params.config.displayName?.value.orEmpty() + + val uiState: StateFlow + field = MutableStateFlow( + TangemPayEditDisplayNameUM( + editingValue = originalDisplayName, + isLoading = false, + onValueChanged = ::onValueChanged, + onDoneClick = ::onDoneClick, + onDismiss = ::onDismiss, + ), + ) + + private fun onValueChanged(value: String) { + if (value.length <= CardDisplayName.MAX_LENGTH) { + uiState.update { it.copy(editingValue = value) } + } + } + + private fun onDoneClick() { + val currentValue = uiState.value.editingValue + if (currentValue.trim() == originalDisplayName.trim()) { + router.pop() + return + } + CardDisplayName(currentValue) + .onRight { cardDisplayName -> + uiState.update { it.copy(isLoading = true) } + modelScope.launch { + cardDetailsRepository.updateCardDisplayName(params.userWalletId, cardDisplayName) + .onRight { router.pop() } + .onLeft { + uiState.update { state -> state.copy(isLoading = false) } + showError( + titleRes = R.string.tangem_pay_card_details_unable_to_rename_card_title, + messageRes = R.string.tangempay_card_details_unable_to_rename_card_description, + ) + } + } + } + .onLeft { + showError( + titleRes = R.string.tangempay_card_details_rename_card_invalid_title, + messageRes = R.string.tangempay_card_details_rename_card_invalid_description, + ) + } + } + + private fun showError(titleRes: Int, messageRes: Int) { + uiMessageSender.send( + DialogMessage(title = TextReference.Res(titleRes), message = TextReference.Res(messageRes)), + ) + } + + private fun onDismiss() { + router.pop() + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayDetailsInnerRoute.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayDetailsInnerRoute.kt index 7bc24c9979..168ea88da5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayDetailsInnerRoute.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayDetailsInnerRoute.kt @@ -16,4 +16,7 @@ internal sealed class TangemPayDetailsInnerRoute : Route { @Serializable data object AddToWallet : TangemPayDetailsInnerRoute() + + @Serializable + data object EditCardDisplayName : TangemPayDetailsInnerRoute() } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayAddToWalletScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayAddToWalletScreen.kt index 7fd6c66c61..6db50884f3 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayAddToWalletScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayAddToWalletScreen.kt @@ -191,6 +191,7 @@ private fun PreviewTangemPayAddToWalletScreen() { onClick = {}, buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text), cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = null, ), ), ) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt index 33e91f7745..9cc9ea437d 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardDetailsBlock.kt @@ -1,23 +1,46 @@ package com.tangem.features.tangempay.ui +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.EaseInOut +import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border +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.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider @@ -32,9 +55,12 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.DisplayNameState import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM import com.tangem.features.tangempay.model.CardDataType +import com.tangem.domain.models.account.CardDisplayName +private const val ICON_FADE_DURATION_MS = 300 private val CustomCardBlockColor = Color(0x1F828282) @Suppress("MagicNumber") @@ -92,17 +118,20 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M isLoading = state.isLoading, shortCardNumber = state.numberShort, onShowDetails = state.onClick, + displayNameState = state.displayNameState, ) } } } +@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") @Composable private fun TangemPayCardDetailsHiddenBlock( shortCardNumber: String, cardFrozenState: TangemPayCardFrozenState, isLoading: Boolean, onShowDetails: () -> Unit, + displayNameState: DisplayNameState?, modifier: Modifier = Modifier, ) { Box(modifier = modifier.fillMaxSize()) { @@ -115,22 +144,46 @@ private fun TangemPayCardDetailsHiddenBlock( painter = painterResource(id = imageResId), contentDescription = null, ) - Row( + ConstraintLayout( modifier = Modifier .align(Alignment.BottomCenter) .padding(horizontal = 16.dp) + .padding(bottom = 8.dp) .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, ) { + val (displayNameRef, cardNumberRef, frozenIconRef, buttonRef) = createRefs() + + if (displayNameState != null) { + CardDisplayName( + state = displayNameState, + modifier = Modifier.constrainAs(displayNameRef) { + start.linkTo(parent.start) + bottom.linkTo(cardNumberRef.top) + width = Dimension.wrapContent + }, + ) + } + Text( text = shortCardNumber, style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.constantWhite, + modifier = Modifier + .constrainAs(cardNumberRef) { + start.linkTo(parent.start) + bottom.linkTo(parent.bottom) + } + .padding(bottom = 8.dp), ) when (cardFrozenState) { is TangemPayCardFrozenState.Frozen -> Icon( modifier = Modifier - .padding(start = 4.dp) + .constrainAs(frozenIconRef) { + start.linkTo(cardNumberRef.end, margin = 4.dp) + top.linkTo(cardNumberRef.top) + bottom.linkTo(cardNumberRef.bottom) + } + .padding(bottom = 8.dp) .size(16.dp), painter = painterResource(id = R.drawable.ic_snow_24), contentDescription = null, @@ -138,16 +191,24 @@ private fun TangemPayCardDetailsHiddenBlock( ) TangemPayCardFrozenState.Pending -> CircularProgressIndicator( modifier = Modifier - .padding(start = 4.dp) + .constrainAs(frozenIconRef) { + start.linkTo(cardNumberRef.end, margin = 4.dp) + top.linkTo(cardNumberRef.top) + bottom.linkTo(cardNumberRef.bottom) + } + .padding(bottom = 8.dp) .size(16.dp), color = TangemTheme.colors.text.constantWhite, strokeWidth = 1.dp, ) TangemPayCardFrozenState.Unfrozen -> Unit } - SpacerWMax() + TangemPayCardDetailsCustomButton( - modifier = Modifier.padding(bottom = 8.dp), + modifier = Modifier.constrainAs(buttonRef) { + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + }, text = stringResourceSafe(id = R.string.tangempay_card_details_show_details), onClick = onShowDetails, showProgress = isLoading, @@ -156,6 +217,97 @@ private fun TangemPayCardDetailsHiddenBlock( } } +@Composable +private fun CardDisplayName(state: DisplayNameState, modifier: Modifier = Modifier) { + val isDisplayMode = state is DisplayNameState.Display + + Row( + modifier = modifier.then( + if (state is DisplayNameState.Display) { + Modifier.clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = state.onClick, + ) + } else { + Modifier + }, + ), + verticalAlignment = Alignment.CenterVertically, + ) { + when (state) { + is DisplayNameState.Display -> DisplayOnlyCardDisplayName(state = state) + is DisplayNameState.Editing -> EditingCardDisplayName(state = state) + } + val iconVisibleState = remember { + MutableTransitionState(initialState = !isDisplayMode).apply { + targetState = isDisplayMode + } + } + AnimatedVisibility( + visibleState = iconVisibleState, + enter = fadeIn(animationSpec = tween(durationMillis = ICON_FADE_DURATION_MS)), + exit = fadeOut(animationSpec = tween(durationMillis = ICON_FADE_DURATION_MS)), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Spacer(modifier = Modifier.width(6.dp)) + Icon( + painter = painterResource(id = com.tangem.core.ui.R.drawable.ic_edit_new_12), + contentDescription = null, + modifier = Modifier.size(10.dp), + tint = TangemTheme.colors.text.constantWhite, + ) + } + } + } +} + +@Composable +private fun DisplayOnlyCardDisplayName(state: DisplayNameState.Display, modifier: Modifier = Modifier) { + Text( + text = state.displayName, + style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite), + maxLines = 1, + modifier = modifier, + ) +} + +@Composable +private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Modifier = Modifier) { + val focusRequester = remember { FocusRequester() } + var textFieldValue by remember(state.editingValue) { + mutableStateOf( + TextFieldValue(text = state.editingValue, selection = TextRange(state.editingValue.length)), + ) + } + + val textStyle = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite) + val textMeasurer = rememberTextMeasurer() + val textWidthDp = with(LocalDensity.current) { + textMeasurer.measure(textFieldValue.text, textStyle).size.width.toDp() + 2.dp + } + + BasicTextField( + value = textFieldValue, + onValueChange = { newValue -> + if (newValue.text.length in 0..CardDisplayName.MAX_LENGTH) { + textFieldValue = newValue + state.onValueChanged(newValue.text) + } + }, + modifier = modifier + .width(textWidthDp.coerceAtLeast(1.dp)) + .focusRequester(focusRequester), + textStyle = textStyle, + singleLine = true, + cursorBrush = SolidColor(TangemTheme.colors.text.constantWhite), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { state.onSubmit() }), + ) + + LaunchedEffect(Unit) { focusRequester.requestFocus() } +} + @Suppress("MagicNumber", "LongParameterList") @Composable private fun TangemPayCardDetailsShownBlock( @@ -313,6 +465,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide onCopy = { _, _ -> }, isHidden = true, cardFrozenState = TangemPayCardFrozenState.Frozen, + displayNameState = DisplayNameState.Display(displayName = "Tangem", onClick = {}), ), TangemPayCardDetailsUM( isLoading = false, @@ -325,6 +478,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide onCopy = { _, _ -> }, isHidden = true, cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), ), TangemPayCardDetailsUM( isLoading = false, @@ -337,6 +491,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide onCopy = { _, _ -> }, isHidden = true, cardFrozenState = TangemPayCardFrozenState.Pending, + displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), ), TangemPayCardDetailsUM( isLoading = false, @@ -349,6 +504,7 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide expiry = "12/34", cvv = "123", cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), ), ), ) \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt index 20a8d78463..2dc2540204 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayCardPageScreen.kt @@ -1,6 +1,11 @@ package com.tangem.features.tangempay.ui import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.tween +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.Arrangement @@ -20,6 +25,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults 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.platform.LocalDensity @@ -33,11 +39,20 @@ import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.features.tangempay.components.cardDetails.PreviewTangemPayCardDetailsBlockComponent import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.DisplayNameState import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM import com.tangem.features.tangempay.entity.TangemPayCardPageSetting import com.tangem.features.tangempay.entity.TangemPayCardPageUM import kotlinx.collections.immutable.ImmutableList +private const val CONTENT_FADE_DURATION_MS = 300 +private val TangemPayCardPageSetting.titleRes + get() = when (this) { + TangemPayCardPageSetting.ChangePIN -> R.string.tangempay_card_details_change_pin + TangemPayCardPageSetting.FreezeCard -> R.string.tangempay_card_details_freeze_card + TangemPayCardPageSetting.ReplaceCard -> R.string.common_error // TODO v_rodionov #[REDACTED_TASK_KEY] + } + @Composable internal fun TangemPayCardPageScreen( state: TangemPayCardPageUM, @@ -76,16 +91,30 @@ internal fun TangemPayCardPageScreen( } if (state.addToWalletBlockState != null) { item(key = "GooglePay") { - TangemPayAddToWalletBlock( - state = state.addToWalletBlockState, - ) + val visibleState = remember { MutableTransitionState(false).apply { targetState = true } } + AnimatedVisibility( + visibleState = visibleState, + enter = fadeIn(animationSpec = tween(CONTENT_FADE_DURATION_MS)), + exit = fadeOut(animationSpec = tween(CONTENT_FADE_DURATION_MS)), + ) { + TangemPayAddToWalletBlock( + state = state.addToWalletBlockState, + ) + } } } item(key = "Settings") { - TangemPayCardPageSettingsBlock( - settings = state.settings, - onSettingClick = state.onSettingClick, - ) + val visibleState = remember { MutableTransitionState(false).apply { targetState = true } } + AnimatedVisibility( + visibleState = visibleState, + enter = fadeIn(animationSpec = tween(CONTENT_FADE_DURATION_MS)), + exit = fadeOut(animationSpec = tween(CONTENT_FADE_DURATION_MS)), + ) { + TangemPayCardPageSettingsBlock( + settings = state.settings, + onSettingClick = state.onSettingClick, + ) + } } } } @@ -145,13 +174,6 @@ private fun TangemPayCardPageSettingRow( } } -private val TangemPayCardPageSetting.titleRes - get() = when (this) { - TangemPayCardPageSetting.ChangePIN -> R.string.tangempay_card_details_change_pin - TangemPayCardPageSetting.FreezeCard -> R.string.tangempay_card_details_freeze_card - TangemPayCardPageSetting.ReplaceCard -> R.string.common_error // TODO v_rodionov #[REDACTED_TASK_KEY] - } - @Preview @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -167,6 +189,7 @@ private fun preview() = TangemThemePreview { onCopy = { _, _ -> }, onClick = {}, cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), ), ), cardDetailsState = TangemPayCardDetailsUM( @@ -177,6 +200,7 @@ private fun preview() = TangemThemePreview { onCopy = { _, _ -> }, onClick = {}, cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), ), ) } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt index 1e3a51c9a4..86e06e7ffe 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayDetailsScreen.kt @@ -312,6 +312,7 @@ private fun TangemPayDetailsScreenPreview( onClick = {}, buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text), cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = null, ), ), expressTransactionsComponent = PreviewEmptyExpressTransactionsComponent(), @@ -391,6 +392,7 @@ private fun TangemPayDetailsTxHistoryScreenPreview( onClick = {}, buttonText = TextReference.Res(R.string.tangempay_card_details_hide_text), cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = null, ), ), expressTransactionsComponent = PreviewEmptyExpressTransactionsComponent(), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt new file mode 100644 index 0000000000..ffe3a645b9 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayEditDisplayNameScreen.kt @@ -0,0 +1,82 @@ +package com.tangem.features.tangempay.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.tangem.common.ui.navigationButtons.NavigationButton +import com.tangem.common.ui.navigationButtons.NavigationPrimaryButton +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.features.tangempay.components.cardDetails.TangemPayCardDetailsBlockComponent +import com.tangem.features.tangempay.details.impl.R +import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM +import com.tangem.features.tangempay.entity.TangemPayEditDisplayNameUM + +@Composable +internal fun TangemPayEditDisplayNameScreen( + state: TangemPayEditDisplayNameUM, + cardDetailsBlockComponent: TangemPayCardDetailsBlockComponent, + cardDetailsState: TangemPayCardDetailsUM, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.secondary) + .navigationBarsPadding(), + ) { + Box( + modifier = Modifier + .statusBarsPadding() + .height(56.dp) + .fillMaxWidth(), + ) { + IconButton( + modifier = Modifier.padding(start = 4.dp, top = 4.dp), + onClick = state.onDismiss, + ) { + Icon( + painter = painterResource(id = com.tangem.core.ui.R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.icon.primary1, + ) + } + } + + cardDetailsBlockComponent.CardDetailsBlockContent( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 8.dp), + state = cardDetailsState, + ) + + Spacer(modifier = Modifier.weight(1f)) + + NavigationPrimaryButton( + modifier = Modifier + .imePadding() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .fillMaxWidth(), + primaryButton = NavigationButton( + textReference = resourceReference(R.string.common_done), + onClick = state.onDoneClick, + shouldShowProgress = state.isLoading, + isEnabled = !state.isLoading && state.editingValue.isNotBlank(), + ), + ) + } +} \ 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 3e1615a6e2..238c8a36f1 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 @@ -80,6 +80,7 @@ internal class TangemPayUpdateInfoStateTransformer( cardFrozenState = cardFrozenState, cardNumberEnd = cardInfo.lastFourDigits, chainId = POLYGON_CHAIN_ID, + displayName = productInstance.displayName, ), ) }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt index 1ce25e26ac..a065f12000 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TangemPayMainBlockConverter.kt @@ -74,6 +74,7 @@ internal class TangemPayMainBlockConverter( cardFrozenState = TangemPayCardFrozenState.Frozen, cardNumberEnd = statusValue.lastFourDigits, chainId = POLYGON_CHAIN_ID, + displayName = statusValue.displayName, ), ) }, @@ -97,6 +98,7 @@ internal class TangemPayMainBlockConverter( cardFrozenState = TangemPayCardFrozenState.Unfrozen, cardNumberEnd = statusValue.lastFourDigits, chainId = POLYGON_CHAIN_ID, + displayName = statusValue.displayName, ), ) }, From 5f2acac13785bb0a7631effa891ae182203fee7f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 31 Mar 2026 14:31:37 +0400 Subject: [PATCH 20/32] Updated on 2026-08-14 --- .../com/tangem/tap/common/redux/AppReducer.kt | 2 - .../com/tangem/tap/common/redux/AppState.kt | 4 - .../features/details/redux/DetailsAction.kt | 47 --- .../details/redux/DetailsMiddleware.kt | 231 ------------- .../features/details/redux/DetailsReducer.kt | 89 ----- .../features/details/redux/DetailsState.kt | 27 -- .../features/details/redux/SecurityOption.kt | 3 + .../ui/appsettings/model/AppSettingsModel.kt | 304 ++++++++++++------ 8 files changed, 214 insertions(+), 493 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/redux/SecurityOption.kt diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt index bc16aae955..092abd503b 100644 --- a/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt +++ b/app/src/main/java/com/tangem/tap/common/redux/AppReducer.kt @@ -1,7 +1,6 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.globalReducer -import com.tangem.tap.features.details.redux.DetailsReducer import com.tangem.tap.proxy.redux.DaggerGraphReducer import org.rekotlin.Action @@ -10,7 +9,6 @@ fun appReducer(action: Action, state: AppState): AppState { return AppState( globalState = globalReducer(action, state), - detailsState = DetailsReducer.reduce(action, state), daggerGraphState = DaggerGraphReducer.reduce(action, state), ) } diff --git a/app/src/main/java/com/tangem/tap/common/redux/AppState.kt b/app/src/main/java/com/tangem/tap/common/redux/AppState.kt index a6275955a1..d854ba1088 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 @@ -1,8 +1,6 @@ package com.tangem.tap.common.redux import com.tangem.tap.common.redux.global.GlobalState -import com.tangem.tap.features.details.redux.DetailsMiddleware -import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.proxy.redux.DaggerGraphMiddleware import com.tangem.tap.proxy.redux.DaggerGraphState import org.rekotlin.Middleware @@ -10,7 +8,6 @@ import org.rekotlin.StateType data class AppState( val globalState: GlobalState = GlobalState(), - val detailsState: DetailsState = DetailsState(), val daggerGraphState: DaggerGraphState = DaggerGraphState(), ) : StateType { @@ -18,7 +15,6 @@ data class AppState( fun getMiddleware(): List> { return listOf( logMiddleware, - DetailsMiddleware().detailsMiddleware, LockUserWalletsTimerMiddleware().middleware, AccessCodeRequestPolicyMiddleware().middleware, DaggerGraphMiddleware.daggerGraphMiddleware, diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt deleted file mode 100644 index 072d738717..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.tangem.tap.features.details.redux - -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.apptheme.model.AppThemeMode -import kotlinx.coroutines.CoroutineScope -import org.rekotlin.Action - -@Suppress("BooleanPropertyNaming") -sealed class DetailsAction : Action { - - sealed class AppSettings : DetailsAction() { - data class SwitchPrivacySetting( - val enable: Boolean, - val setting: AppSetting, - ) : AppSettings() { - data object Success : AppSettings() - - data class Failure( - val prevState: Boolean, - val setting: AppSetting, - ) : AppSettings() - } - - data class CheckBiometricsStatus( - val coroutineScope: CoroutineScope, - ) : AppSettings() - - data object EnrollBiometrics : AppSettings() - data class BiometricsStatusChanged( - val isEnrollBiometricsNeeded: Boolean, - ) : AppSettings() - - data class ChangeAppThemeMode( - val appThemeMode: AppThemeMode, - ) : AppSettings() - - data class ChangeBalanceHiding( - val shouldHideBalance: Boolean, - ) : AppSettings() - - data class ChangeAppCurrency( - val currency: AppCurrency, - ) : AppSettings() - - data class Prepare(val state: AppSettingsState) : AppSettings() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt deleted file mode 100644 index fa438bf186..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt +++ /dev/null @@ -1,231 +0,0 @@ -package com.tangem.tap.features.details.redux - -import com.tangem.common.CompletionResult -import com.tangem.common.doOnFailure -import com.tangem.common.doOnSuccess -import com.tangem.core.analytics.Analytics -import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.domain.common.wallets.UserWalletsListRepository.LockMethod -import com.tangem.domain.models.wallet.UserWallet -import com.tangem.tap.common.analytics.events.AnalyticsParam -import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.common.extensions.inject -import com.tangem.tap.common.redux.AppState -import com.tangem.tap.features.demo.DemoHelper -import com.tangem.tap.proxy.redux.DaggerGraphState -import com.tangem.tap.scope -import com.tangem.tap.store -import com.tangem.tap.tangemSdkManager -import com.tangem.utils.coroutines.JobHolder -import com.tangem.utils.coroutines.saveIn -import com.tangem.utils.logging.TangemLogger -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import org.rekotlin.Action -import org.rekotlin.Middleware - -@Suppress("MemberNameEqualsClassName") -class DetailsMiddleware { - private val appSettingsMiddleware = AppSettingsMiddleware() - val detailsMiddleware: Middleware = { _, stateProvider -> - { next -> - { action -> - if (!DemoHelper.tryHandle(stateProvider)) { - val detailsState = stateProvider()?.detailsState - if (detailsState != null) { - handleAction(action) - } - } - next(action) - } - } - } - - private fun handleAction(action: Action) { - when (action) { - is DetailsAction.AppSettings -> appSettingsMiddleware.handle(action) - } - } - - class AppSettingsMiddleware { - - private val checkBiometricsStatusJobHolder = JobHolder() - - fun handle(action: DetailsAction.AppSettings) { - when (action) { - is DetailsAction.AppSettings.SwitchPrivacySetting -> { - when (action.setting) { - AppSetting.RequireAccessCode -> toggleRequireAccessCode(enable = action.enable) - AppSetting.BiometricAuthentication -> toggleBiometricsAuthentication(enable = action.enable) - } - } - is DetailsAction.AppSettings.CheckBiometricsStatus -> { - observeBiometricsStatusChanges(action.coroutineScope) - } - is DetailsAction.AppSettings.EnrollBiometrics -> { - enrollBiometrics() - } - is DetailsAction.AppSettings.ChangeAppThemeMode -> { - changeAppThemeMode(action.appThemeMode) - } - is DetailsAction.AppSettings.ChangeBalanceHiding -> { - changeBalanceHiding(action.shouldHideBalance) - } - is DetailsAction.AppSettings.ChangeAppCurrency, - is DetailsAction.AppSettings.SwitchPrivacySetting.Success, - is DetailsAction.AppSettings.SwitchPrivacySetting.Failure, - is DetailsAction.AppSettings.BiometricsStatusChanged, - is DetailsAction.AppSettings.Prepare, - -> Unit - } - } - - private fun toggleBiometricsAuthentication(enable: Boolean) { - scope.launch { - val walletsRepository = store.inject(DaggerGraphState::walletsRepository) - - // Nothing to change - if (walletsRepository.useBiometricAuthentication() == enable) { - store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) - return@launch - } - - if (enable) { - setBiometricLockForAllWallets() - } else { - // Remove all biometric-related data - removeAllBiometricData() - walletsRepository.setRequireAccessCode(value = true) - } - - walletsRepository.setUseBiometricAuthentication(value = enable) - store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) - } - } - - private fun toggleRequireAccessCode(enable: Boolean) { - scope.launch { - val walletsRepository = store.inject(DaggerGraphState::walletsRepository) - - // Nothing to change - if (walletsRepository.requireAccessCode() == enable) { - store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) - return@launch - } - - if (enable) { - // Remove all saved access codes - removeAllBiometricSingData() - } - - walletsRepository.setRequireAccessCode(value = enable) - store.dispatchWithMain(DetailsAction.AppSettings.SwitchPrivacySetting.Success) - } - } - - private suspend fun setBiometricLockForAllWallets() { - val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) - val userWallets = userWalletsListRepository.userWalletsSync() - userWallets.forEach { wallet -> - userWalletsListRepository.setLock( - userWalletId = wallet.walletId, - lockMethod = LockMethod.Biometric, - changeUnsecured = false, - ) - } - } - - private suspend fun removeAllBiometricData() { - val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) - userWalletsListRepository.userWalletsSync().forEach { - userWalletsListRepository.removeBiometricLock(it.walletId) - } - removeAllBiometricSingData() - } - - private suspend fun removeAllBiometricSingData() { - deleteSavedAccessCodes() - val userWalletsListRepository = store.inject(DaggerGraphState::userWalletsListRepository) - val tangemHotSdk = store.inject(DaggerGraphState::tangemHotSdk) - userWalletsListRepository.userWalletsSync().forEach { wallet -> - if (wallet is UserWallet.Hot) { - userWalletsListRepository.saveWithoutLock( - userWallet = wallet.copy( - hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(wallet.hotWalletId), - ), - ) - } - } - } - - private fun observeBiometricsStatusChanges(scope: CoroutineScope) { - val needEnrollBiometricsFlow = flow { - do { - val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() - - if (isEnrollBiometricsNeeded != null) { - emit(isEnrollBiometricsNeeded) - } - - delay(timeMillis = 200) - } while (true) - } - - needEnrollBiometricsFlow - .distinctUntilChanged() - .onEach { needEnrollBiometrics -> - store.dispatchWithMain(DetailsAction.AppSettings.BiometricsStatusChanged(needEnrollBiometrics)) - } - .launchIn(scope) - .saveIn(checkBiometricsStatusJobHolder) - } - - private fun enrollBiometrics() { - Analytics.send(Settings.AppSettings.ButtonEnableBiometricAuthentication()) - store.inject(DaggerGraphState::settingsManager).openBiometricSettings() - } - - private fun changeAppThemeMode(appThemeMode: AppThemeMode) { - val repository = store.inject(DaggerGraphState::appThemeModeRepository) - - scope.launch { - repository.changeAppThemeMode(appThemeMode) - } - } - - private fun changeBalanceHiding(hideBalance: Boolean) { - val repository = store.inject(DaggerGraphState::balanceHidingRepository) - - scope.launch { - val newState = repository.getBalanceHidingSettings().copy( - isHidingEnabledInSettings = hideBalance, - isBalanceHidden = false, - ) - - repository.storeBalanceHidingSettings(newState) - } - } - - private suspend fun deleteSavedAccessCodes(): CompletionResult { - return tangemSdkManager.clearSavedUserCodes() - .doOnSuccess { - Analytics.send(Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off)) - - store.inject(DaggerGraphState::settingsRepository).setShouldSaveAccessCodes(value = false) - - store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy( - isBiometricsRequestPolicy = false, - ) - } - .doOnFailure { error -> - TangemLogger.e("Unable to delete saved access codes", error) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt deleted file mode 100644 index 40549d69f4..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.tangem.tap.features.details.redux - -import com.tangem.tap.common.redux.AppState -import org.rekotlin.Action - -object DetailsReducer { - fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state) -} - -@Suppress("CyclomaticComplexMethod") -private fun internalReduce(action: Action, state: AppState): DetailsState { - if (action !is DetailsAction) return state.detailsState - val detailsState = state.detailsState - return when (action) { - is DetailsAction.AppSettings -> { - handlePrivacyAction(action, detailsState) - } - } -} - -@Suppress("LongMethod", "CyclomaticComplexMethod") -private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: DetailsState): DetailsState { - return when (action) { - is DetailsAction.AppSettings.SwitchPrivacySetting -> state.copy( - appSettingsState = when (action.setting) { - AppSetting.RequireAccessCode -> state.appSettingsState.copy( - isInProgress = true, - requireAccessCode = action.enable, - ) - AppSetting.BiometricAuthentication -> state.appSettingsState.copy( - isInProgress = true, - useBiometricAuthentication = action.enable, - ) - }, - ) - is DetailsAction.AppSettings.SwitchPrivacySetting.Success -> state.copy( - appSettingsState = state.appSettingsState.copy( - isInProgress = false, - ), - ) - is DetailsAction.AppSettings.SwitchPrivacySetting.Failure -> state.copy( - appSettingsState = when (action.setting) { - AppSetting.RequireAccessCode -> state.appSettingsState.copy( - isInProgress = false, - requireAccessCode = action.prevState, - ) - AppSetting.BiometricAuthentication -> state.appSettingsState.copy( - isInProgress = false, - needEnrollBiometrics = action.prevState, - ) - }, - ) - is DetailsAction.AppSettings.BiometricsStatusChanged -> state.copy( - appSettingsState = state.appSettingsState.copy( - needEnrollBiometrics = action.isEnrollBiometricsNeeded, - ), - ) - is DetailsAction.AppSettings.ChangeAppThemeMode -> state.copy( - appSettingsState = state.appSettingsState.copy( - selectedThemeMode = action.appThemeMode, - ), - ) - is DetailsAction.AppSettings.ChangeAppCurrency -> state.copy( - appSettingsState = state.appSettingsState.copy( - selectedAppCurrency = action.currency, - ), - ) - is DetailsAction.AppSettings.ChangeBalanceHiding -> state.copy( - appSettingsState = state.appSettingsState.copy( - isHidingEnabled = action.shouldHideBalance, - ), - ) - // state should be copied to avoid concurrent modifications from different sources - is DetailsAction.AppSettings.Prepare -> state.copy( - appSettingsState = state.appSettingsState.copy( - isHidingEnabled = action.state.isHidingEnabled, - selectedAppCurrency = action.state.selectedAppCurrency, - selectedThemeMode = action.state.selectedThemeMode, - useBiometricAuthentication = action.state.useBiometricAuthentication, - requireAccessCode = action.state.requireAccessCode, - hasSecuredWallets = action.state.hasSecuredWallets, - needEnrollBiometrics = action.state.needEnrollBiometrics, - ), - ) - is DetailsAction.AppSettings.EnrollBiometrics, - is DetailsAction.AppSettings.CheckBiometricsStatus, - -> state - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt deleted file mode 100644 index e209c707e4..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.tap.features.details.redux - -import com.tangem.domain.appcurrency.model.AppCurrency -import com.tangem.domain.apptheme.model.AppThemeMode -import org.rekotlin.StateType - -data class DetailsState( - val appSettingsState: AppSettingsState = AppSettingsState(), -) : StateType - -@Suppress("BooleanPropertyNaming") -data class AppSettingsState( - val requireAccessCode: Boolean = false, - val useBiometricAuthentication: Boolean = false, - val needEnrollBiometrics: Boolean = false, - val hasSecuredWallets: Boolean = false, - val isHidingEnabled: Boolean = false, - val isInProgress: Boolean = false, - val selectedAppCurrency: AppCurrency = AppCurrency.Default, - val selectedThemeMode: AppThemeMode = AppThemeMode.DEFAULT, -) - -enum class SecurityOption { LongTap, PassCode, AccessCode } - -enum class AppSetting { - RequireAccessCode, BiometricAuthentication, -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/SecurityOption.kt b/app/src/main/java/com/tangem/tap/features/details/redux/SecurityOption.kt new file mode 100644 index 0000000000..e55259df3b --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/redux/SecurityOption.kt @@ -0,0 +1,3 @@ +package com.tangem.tap.features.details.redux + +enum class SecurityOption { LongTap, PassCode, AccessCode } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index e65c6560da..c9edf25848 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -1,11 +1,15 @@ package com.tangem.tap.features.details.ui.appsettings.model import androidx.compose.runtime.Stable +import com.tangem.common.doOnFailure +import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model +import com.tangem.core.decompose.navigation.Router import com.tangem.core.decompose.ui.UiMessageSender +import com.tangem.core.navigation.settings.SettingsManager import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.appcurrency.model.AppCurrency @@ -13,42 +17,38 @@ import com.tangem.domain.appcurrency.repository.AppCurrencyRepository import com.tangem.domain.apptheme.model.AppThemeMode import com.tangem.domain.apptheme.repository.AppThemeModeRepository import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository +import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.wallets.UserWalletsListRepository -import com.tangem.sdk.api.TangemSdkManager +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.sdk.api.TangemSdkManager import com.tangem.tap.common.analytics.events.AnalyticsParam import com.tangem.tap.common.analytics.events.Settings -import com.tangem.tap.common.extensions.dispatchNavigationAction -import com.tangem.tap.common.extensions.dispatchOnMain -import com.tangem.tap.common.extensions.dispatchWithMain -import com.tangem.tap.features.details.redux.AppSetting -import com.tangem.tap.features.details.redux.AppSettingsState -import com.tangem.tap.features.details.redux.DetailsAction -import com.tangem.tap.features.details.redux.DetailsState import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState import com.tangem.tap.features.details.ui.appsettings.analytics.AppSettingsItemsAnalyticsSender -import com.tangem.tap.scope -import com.tangem.tap.store import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn import com.tangem.utils.extensions.addIf +import com.tangem.utils.logging.TangemLogger import com.tangem.wallet.R import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import org.rekotlin.StoreSubscriber import javax.inject.Inject -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") @Stable @ModelScoped internal class AppSettingsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, - private val appCurrencyRepository: AppCurrencyRepository, + appCurrencyRepository: AppCurrencyRepository, private val walletsRepository: WalletsRepository, private val userWalletsListRepository: UserWalletsListRepository, private val balanceHidingRepository: BalanceHidingRepository, @@ -56,72 +56,100 @@ internal class AppSettingsModel @Inject constructor( private val appThemeModeRepository: AppThemeModeRepository, private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender, private val tangemSdkManager: TangemSdkManager, + private val settingsManager: SettingsManager, + private val settingsRepository: SettingsRepository, + private val cardSdkConfigRepository: CardSdkConfigRepository, + private val tangemHotSdk: TangemHotSdk, + private val router: Router, private val uiMessageSender: UiMessageSender, -) : Model(), StoreSubscriber { +) : Model() { private val itemsFactory = AppSettingsItemsFactory() private val dialogsFactory = AppSettingsDialogsFactory() - private val appCurrencyUpdatesJobHolder = JobHolder() + private val localState = MutableStateFlow(LocalState()) + private val biometricsStatusJobHolder = JobHolder() - private val _uiState: MutableStateFlow = MutableStateFlow( - value = AppSettingsScreenState.Loading, - ) - val uiState: StateFlow = _uiState + val uiState: StateFlow + field = MutableStateFlow(value = AppSettingsScreenState.Loading) init { - bootstrapAppCurrencyUpdates() - bootstrapBiometricsUpdates() + bootstrapLocalState() + + combine( + flow = appCurrencyRepository.getSelectedAppCurrency().distinctUntilChanged(), + flow2 = appThemeModeRepository.getAppThemeMode(), + flow3 = balanceHidingRepository.getBalanceHidingSettingsFlow(), + flow4 = localState, + ) { currency, themeMode, hidingSettings, local -> + AppSettingsState( + appCurrency = currency, + themeMode = themeMode, + isHidingEnabled = hidingSettings.isHidingEnabledInSettings, + local = local, + ) + } + .onEach { state -> + val items = buildItems(state) + uiState.update { prevState -> + when (prevState) { + is AppSettingsScreenState.Content -> prevState.copy(items = items) + is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content( + items = items, + dialog = null, + ) + } + } + } + .launchIn(modelScope) - subscribeToStoreChanges() sendItemsAnalytics() } - override fun newState(state: DetailsState) { - val items = buildItems(state.appSettingsState) - - _uiState.update { prevState -> - when (prevState) { - is AppSettingsScreenState.Content -> prevState.copy( - items = items, - ) - is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content( - items = items, - dialog = null, - ) - } - } - } - fun onResume() { - store.dispatch(DetailsAction.AppSettings.CheckBiometricsStatus(modelScope)) + observeBiometricsStatusChanges() } - override fun onDestroy() { - super.onDestroy() - store.unsubscribe(subscriber = this) + private fun observeBiometricsStatusChanges() { + flow { + do { + val isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() + if (isEnrollBiometricsNeeded != null) { + emit(isEnrollBiometricsNeeded) + } + delay(timeMillis = 200) + } while (true) + } + .flowOn(dispatchers.default) + .distinctUntilChanged() + .onEach { isEnrollBiometricsNeeded -> + localState.update { it.copy(isEnrollBiometricsNeeded = isEnrollBiometricsNeeded) } + } + .launchIn(modelScope) + .saveIn(biometricsStatusJobHolder) } private fun buildItems(state: AppSettingsState): ImmutableList { val items = buildList { addIf( - condition = state.needEnrollBiometrics, + condition = state.local.isEnrollBiometricsNeeded, element = itemsFactory.createEnrollBiometricsCard(onClick = ::enrollBiometrics), ) add( itemsFactory.createSelectAppCurrencyButton( - currentAppCurrencyName = state.selectedAppCurrency.name, + currentAppCurrencyName = state.appCurrency.name, onClick = ::showAppCurrencySelector, ), ) - val canUseBiometrics = - !state.needEnrollBiometrics && !state.isInProgress && state.hasSecuredWallets + val canUseBiometrics = with(state.local) { + !isEnrollBiometricsNeeded && !isInProgress && hasSecuredWallets + } add( itemsFactory.createUseBiometricsSwitch( - isChecked = state.useBiometricAuthentication, + isChecked = state.local.isBiometricAuthenticationUsed, isEnabled = canUseBiometrics, onCheckedChange = ::onBiometricAuthenticationToggled, onDisabledClick = ::onBiometricAuthenticationDisabledClicked, @@ -130,8 +158,8 @@ internal class AppSettingsModel @Inject constructor( add( itemsFactory.createRequireAccessCodeSwitch( - isChecked = state.requireAccessCode || !state.useBiometricAuthentication, - isEnabled = canUseBiometrics && state.useBiometricAuthentication, + isChecked = state.local.isAccessCodeRequired || !state.local.isBiometricAuthenticationUsed, + isEnabled = canUseBiometrics && state.local.isBiometricAuthenticationUsed, onCheckedChange = ::onRequireAccessCodeToggled, ), ) @@ -146,8 +174,8 @@ internal class AppSettingsModel @Inject constructor( add( itemsFactory.createSelectThemeModeButton( - currentThemeMode = state.selectedThemeMode, - onClick = { showThemeModeSelector(state.selectedThemeMode) }, + currentThemeMode = state.themeMode, + onClick = { showThemeModeSelector(state.themeMode) }, ), ) } @@ -156,11 +184,12 @@ internal class AppSettingsModel @Inject constructor( } private fun enrollBiometrics() { - store.dispatchOnMain(DetailsAction.AppSettings.EnrollBiometrics) + analyticsEventHandler.send(Settings.AppSettings.ButtonEnableBiometricAuthentication()) + settingsManager.openBiometricSettings() } private fun showAppCurrencySelector() { - store.dispatchNavigationAction { push(AppRoute.AppCurrencySelector) } + router.push(AppRoute.AppCurrencySelector) } private fun showThemeModeSelector(selectedMode: AppThemeMode) { @@ -174,7 +203,7 @@ internal class AppSettingsModel @Inject constructor( theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode), ), ) - store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode)) + changeAppThemeMode(mode) dismissDialog() }, onDismiss = ::dismissDialog, @@ -188,13 +217,13 @@ internal class AppSettingsModel @Inject constructor( // val param = AnalyticsParam.OnOffState(isChecked) // analyticsEventHandler.send(Settings.AppSettings.BiometricAuthenticationChanged(param)) if (isChecked) { - onSettingsToggled(AppSetting.BiometricAuthentication, enable = true) + toggleBiometricsAuthentication(enable = true) } else { updateContentState { copy( dialog = dialogsFactory.createDisableBiometricAuthenticationAlert( onDisable = { - onSettingsToggled(AppSetting.BiometricAuthentication, enable = false) + toggleBiometricsAuthentication(enable = false) dismissDialog() }, onDismiss = ::dismissDialog, @@ -217,7 +246,7 @@ internal class AppSettingsModel @Inject constructor( dialog = if (isChecked) { dialogsFactory.createEnableRequireAccessCodeAlert( onEnable = { - onSettingsToggled(AppSetting.RequireAccessCode, enable = true) + toggleRequireAccessCode(enable = true) dismissDialog() }, onDismiss = ::dismissDialog, @@ -225,7 +254,7 @@ internal class AppSettingsModel @Inject constructor( } else { dialogsFactory.createDisableRequireAccessCodeAlert( onDisable = { - onSettingsToggled(AppSetting.RequireAccessCode, enable = false) + toggleRequireAccessCode(enable = false) dismissDialog() }, onDismiss = ::dismissDialog, @@ -235,51 +264,125 @@ internal class AppSettingsModel @Inject constructor( } } - private fun onSettingsToggled(setting: AppSetting, enable: Boolean) { - store.dispatch(DetailsAction.AppSettings.SwitchPrivacySetting(enable = enable, setting = setting)) + private fun toggleBiometricsAuthentication(enable: Boolean) { + localState.update { it.copy(isBiometricAuthenticationUsed = enable, isInProgress = true) } + + modelScope.launch { + // Nothing to change + if (walletsRepository.useBiometricAuthentication() == enable) { + localState.update { it.copy(isInProgress = false) } + return@launch + } + + if (enable) { + setBiometricLockForAllWallets() + } else { + removeAllBiometricData() + walletsRepository.setRequireAccessCode(value = true) + localState.update { it.copy(isAccessCodeRequired = true) } + } + + walletsRepository.setUseBiometricAuthentication(value = enable) + localState.update { it.copy(isInProgress = false) } + } + } + + private fun toggleRequireAccessCode(enable: Boolean) { + localState.update { it.copy(isAccessCodeRequired = enable, isInProgress = true) } + + modelScope.launch { + // Nothing to change + if (walletsRepository.requireAccessCode() == enable) { + localState.update { it.copy(isInProgress = false) } + return@launch + } + + if (enable) { + removeAllBiometricSingData(userWalletsListRepository.userWalletsSync()) + } + + walletsRepository.setRequireAccessCode(value = enable) + localState.update { it.copy(isInProgress = false) } + } + } + + private suspend fun setBiometricLockForAllWallets() { + val userWallets = userWalletsListRepository.userWalletsSync() + userWallets.forEach { wallet -> + userWalletsListRepository.setLock( + userWalletId = wallet.walletId, + lockMethod = UserWalletsListRepository.LockMethod.Biometric, + changeUnsecured = false, + ) + } + } + + private suspend fun removeAllBiometricData() { + val userWallets = userWalletsListRepository.userWalletsSync() + userWallets.forEach { + userWalletsListRepository.removeBiometricLock(it.walletId) + } + removeAllBiometricSingData(userWallets) + } + + private suspend fun removeAllBiometricSingData(userWallets: List) { + deleteSavedAccessCodes() + userWallets.forEach { wallet -> + if (wallet is UserWallet.Hot) { + userWalletsListRepository.saveWithoutLock( + userWallet = wallet.copy( + hotWalletId = tangemHotSdk.removeBiometryAuthIfPresented(wallet.hotWalletId), + ), + ) + } + } + } + + private suspend fun deleteSavedAccessCodes() { + tangemSdkManager.clearSavedUserCodes() + .doOnSuccess { + analyticsEventHandler.send( + Settings.AppSettings.SaveAccessCodeSwitcherChanged(AnalyticsParam.OnOffState.Off), + ) + settingsRepository.setShouldSaveAccessCodes(value = false) + cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = false) + } + .doOnFailure { error -> + TangemLogger.e("Unable to delete saved access codes", error) + } } private fun onFlipToHideBalanceToggled(enable: Boolean) { val param = AnalyticsParam.OnOffState(enable) analyticsEventHandler.send(Settings.AppSettings.HideBalanceChanged(param)) - store.dispatch(DetailsAction.AppSettings.ChangeBalanceHiding(shouldHideBalance = enable)) + modelScope.launch { + val settings = balanceHidingRepository.getBalanceHidingSettings().copy( + isHidingEnabledInSettings = enable, + isBalanceHidden = false, + ) + balanceHidingRepository.storeBalanceHidingSettings(settings) + } + } + + private fun changeAppThemeMode(mode: AppThemeMode) { + modelScope.launch { + appThemeModeRepository.changeAppThemeMode(mode) + } } private fun dismissDialog() { updateContentState { copy(dialog = null) } } - private fun bootstrapAppCurrencyUpdates() { - appCurrencyRepository - .getSelectedAppCurrency() - .distinctUntilChanged() - .onEach { appCurrency -> - store.dispatchWithMain(DetailsAction.AppSettings.ChangeAppCurrency(appCurrency)) - } - .launchIn(scope) - .saveIn(appCurrencyUpdatesJobHolder) - } - - private fun bootstrapBiometricsUpdates() = modelScope.launch { - val state = AppSettingsState( - useBiometricAuthentication = walletsRepository.useBiometricAuthentication(), - requireAccessCode = walletsRepository.requireAccessCode(), - isHidingEnabled = balanceHidingRepository.getBalanceHidingSettings().isHidingEnabledInSettings, - selectedAppCurrency = appCurrencyRepository.getSelectedAppCurrency().firstOrNull() ?: AppCurrency.Default, - selectedThemeMode = appThemeModeRepository.getAppThemeMode().firstOrNull() ?: AppThemeMode.DEFAULT, - needEnrollBiometrics = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, - hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(), - ) - - store.dispatchWithMain(DetailsAction.AppSettings.Prepare(state)) - } - - private fun subscribeToStoreChanges() { - store.subscribe(subscriber = this) { state -> - state.skipRepeats { oldState, newState -> - oldState.detailsState == newState.detailsState - }.select { it.detailsState } + private fun bootstrapLocalState() = modelScope.launch { + localState.update { state -> + state.copy( + hasSecuredWallets = userWalletsListRepository.hasSecuredWallets(), + isEnrollBiometricsNeeded = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() == true, + isBiometricAuthenticationUsed = walletsRepository.useBiometricAuthentication(), + isAccessCodeRequired = walletsRepository.requireAccessCode(), + ) } } @@ -288,15 +391,30 @@ internal class AppSettingsModel @Inject constructor( .filterIsInstance() .distinctUntilChangedBy(AppSettingsScreenState.Content::items) .onEach { appSettingsItemsAnalyticsSender.send(it.items) } - .launchIn(scope) + .launchIn(modelScope) } private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) { - _uiState.update { prevState -> + uiState.update { prevState -> when (prevState) { is AppSettingsScreenState.Content -> block(prevState) is AppSettingsScreenState.Loading -> prevState } } } + + private data class LocalState( + val hasSecuredWallets: Boolean = false, + val isEnrollBiometricsNeeded: Boolean = false, + val isBiometricAuthenticationUsed: Boolean = false, + val isAccessCodeRequired: Boolean = false, + val isInProgress: Boolean = false, + ) + + private data class AppSettingsState( + val themeMode: AppThemeMode = AppThemeMode.DEFAULT, + val isHidingEnabled: Boolean = false, + val appCurrency: AppCurrency = AppCurrency.Default, + val local: LocalState = LocalState(), + ) } \ No newline at end of file From 547d562d6de7c7fc013ebe6423e19a34ecaad0d8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2026 17:26:05 +0400 Subject: [PATCH 21/32] Updated on 2026-08-14 --- .../SwapAmountSelectQuoteTransformer.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt index 19a5e22946..7588c150a5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/transformers/SwapAmountSelectQuoteTransformer.kt @@ -62,7 +62,8 @@ internal class SwapAmountSelectQuoteTransformer( ) return prevState.copy( - isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content, + isPrimaryButtonEnabled = quoteUM is SwapQuoteUM.Content && + !hasInsufficientBalance(prevState, quoteContent, newPrimaryAmount), selectedQuote = quoteUM, isShowFCAWarning = isNeedApplyFCARestrictions && quoteUM.provider?.isRestrictedByFCA() == true, primaryAmount = newPrimaryAmount, @@ -202,4 +203,22 @@ internal class SwapAmountSelectQuoteTransformer( prevState.secondaryAmount } } + + private fun hasInsufficientBalance( + prevState: SwapAmountUM.Content, + quoteContent: SwapQuoteUM.Content?, + newPrimaryAmount: SwapAmountFieldUM, + ): Boolean { + if (quoteContent == null) return false + + val primaryBalance = prevState.primaryCryptoCurrencyStatus.value.amount ?: return false + val fromAmount = if (prevState.selectedAmountType == SwapAmountType.To) { + quoteContent.fromAmount + } else { + val amountData = (newPrimaryAmount as? SwapAmountFieldUM.Content)?.amountField as? AmountState.Data + amountData?.amountTextField?.cryptoAmount?.value + } + + return fromAmount != null && fromAmount > primaryBalance + } } \ No newline at end of file From d0ef5c70cd7cae67012243daf6c6d3fe6a150310 Mon Sep 17 00:00:00 2001 From: Tangem Date: Sat, 11 Apr 2026 09:38:37 +0300 Subject: [PATCH 22/32] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 9 ++ core/res/src/main/res/values/strings.xml | 1 + .../ic_dynamic_addresses_badge_16.xml | 9 ++ .../DefaultWalletManagersFacade.kt | 6 +- .../domain/models/TokenReceiveConfig.kt | 6 +- domain/transaction/build.gradle.kts | 2 + .../usecase/ReceiveAddressesFactory.kt | 90 ++++++++++++++----- .../tangempay/model/TangemPayAddFundsModel.kt | 4 +- .../tokenreceive/entity/ReceiveAddress.kt | 2 + .../entity/TokenReceiveStateFactory.kt | 68 ++++++++------ .../ui/TokenReceiveAssetsContent.kt | 36 ++++++++ gradle/tangem_dependencies.toml | 2 +- 12 files changed, 175 insertions(+), 60 deletions(-) create mode 100644 core/ui/src/main/res/drawable/ic_dynamic_addresses_badge_16.xml diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 9b3b5503e7..d88224fe30 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -5,6 +5,9 @@ import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.demo.models.DemoConfig +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.networks.single.SingleNetworkStatusFetcher import com.tangem.domain.networks.single.SingleNetworkStatusSupplier import com.tangem.domain.notifications.repository.PushNotificationsRepository @@ -257,10 +260,16 @@ internal object TransactionDomainModule { fun provideReceiveAddressesFactory( getEnsNameUseCase: GetEnsNameUseCase, getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + getDynamicReceiveAddressUseCase: GetDynamicReceiveAddressUseCase, + dynamicAddressesRepository: DynamicAddressesRepository, + dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, ): ReceiveAddressesFactory { return ReceiveAddressesFactory( getEnsNameUseCase = getEnsNameUseCase, getViewedTokenReceiveWarningUseCase = getViewedTokenReceiveWarningUseCase, + getDynamicReceiveAddressUseCase = getDynamicReceiveAddressUseCase, + dynamicAddressesRepository = dynamicAddressesRepository, + dynamicAddressesFeatureToggles = dynamicAddressesFeatureToggles, ) } diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1ec9514666..586600272c 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -487,6 +487,7 @@ Disable dynamic addresses Dynamic addresses disabled Dynamic addresses enabled + Dynamic address Use a new address for each transaction to reduce traceability and improve on-chain privacy. Enhanced Privacy Easily receive funds in UTXO-based networks with automatic address generation — no manual address management required. diff --git a/core/ui/src/main/res/drawable/ic_dynamic_addresses_badge_16.xml b/core/ui/src/main/res/drawable/ic_dynamic_addresses_badge_16.xml new file mode 100644 index 0000000000..5ab98c661f --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_dynamic_addresses_badge_16.xml @@ -0,0 +1,9 @@ + + + diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index 1055b56685..d86119e490 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -476,12 +476,12 @@ internal class DefaultWalletManagersFacade @Inject constructor( val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return null return dynamicAddressesManager.usedAddresses .filter { usedAddress -> - val nodes = runCatching { DerivationPath(usedAddress.path).nodes }.getOrNull() + val nodes = runCatching { DerivationPath(usedAddress.derivationPath).nodes }.getOrNull() ?: return@filter false nodes.size >= XPUB_PATH_MIN_NODES && nodes[nodes.size - 2].index == RECEIVE_CHAIN_INDEX } .maxByOrNull { usedAddress -> - runCatching { DerivationPath(usedAddress.path).nodes.last().index }.getOrDefault(0L) + runCatching { DerivationPath(usedAddress.derivationPath).nodes.last().index }.getOrDefault(0L) } ?.address } @@ -490,7 +490,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( val walletManager = getOrCreateWalletManager(userWalletId = userWalletId, network = network) val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return false return dynamicAddressesManager.usedAddresses.any { usedAddress -> - val nodes = runCatching { DerivationPath(usedAddress.path).nodes }.getOrNull() + val nodes = runCatching { DerivationPath(usedAddress.derivationPath).nodes }.getOrNull() ?: return@any false val isBaseAddress = nodes.size >= XPUB_PATH_MIN_NODES && nodes[nodes.size - 2].index == RECEIVE_CHAIN_INDEX && diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt index 467675af70..f0504c15ca 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/TokenReceiveConfig.kt @@ -18,11 +18,11 @@ data class TokenReceiveConfig( @Serializable data class ReceiveAddressModel( - val nameService: NameService, + val displayType: DisplayType, val value: String, ) { - enum class NameService { - Default, Legacy, Ens + enum class DisplayType { + Default, Legacy, Ens, Dynamic, } } diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index aae9c5368c..cc13ce06eb 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -27,6 +27,8 @@ dependencies { implementation(projects.libs.crypto) implementation(projects.domain.account.status) + implementation(projects.domain.dynamicAddresses) + implementation(projects.domain.dynamicAddresses.models) implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.domain.walletManager) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ReceiveAddressesFactory.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ReceiveAddressesFactory.kt index e0b596f81e..93a25ab440 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ReceiveAddressesFactory.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/ReceiveAddressesFactory.kt @@ -1,5 +1,9 @@ package com.tangem.domain.transaction.usecase +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase +import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.models.Asset import com.tangem.domain.models.ReceiveAddressModel import com.tangem.domain.models.TokenReceiveConfig @@ -12,10 +16,15 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.tokens.GetViewedTokenReceiveWarningUseCase import com.tangem.domain.transaction.R import com.tangem.lib.crypto.BlockchainUtils +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.flow.firstOrNull class ReceiveAddressesFactory( private val getEnsNameUseCase: GetEnsNameUseCase, private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase, + private val getDynamicReceiveAddressUseCase: GetDynamicReceiveAddressUseCase, + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, ) { suspend fun create( @@ -32,27 +41,14 @@ class ReceiveAddressesFactory( address = addresses.defaultAddress.value, ) - val receiveAddresses = buildList { - ensName?.let { ens -> - add( - ReceiveAddressModel( - nameService = ReceiveAddressModel.NameService.Ens, - value = ens, - ), - ) - } - addresses.availableAddresses.map { address -> - add( - ReceiveAddressModel( - nameService = when (address.type) { - NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default - NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy - }, - value = address.value, - ), - ) - } + val dynamicAddress = getDynamicAddressIfEnabled(userWalletId, cryptoCurrency) + + val receiveAddresses = if (dynamicAddress != null) { + buildDynamicAddressList(ensName, dynamicAddress) + } else { + buildStandardAddressList(ensName, addresses) } + return TokenReceiveConfig( shouldShowWarning = cryptoCurrency.name !in getViewedTokenReceiveWarningUseCase(), cryptoCurrency = cryptoCurrency, @@ -64,6 +60,52 @@ class ReceiveAddressesFactory( ) } + private suspend fun getDynamicAddressIfEnabled( + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): String? { + if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return null + if (cryptoCurrency !is CryptoCurrency.Coin) return null + + val status = dynamicAddressesRepository.getStatus(userWalletId, cryptoCurrency.network).firstOrNull() + if (status != DynamicAddressesStatus.ENABLED) return null + + return getDynamicReceiveAddressUseCase(userWalletId, cryptoCurrency.network) + .onLeft { TangemLogger.e("Failed to get dynamic receive address: ${it.message}") } + .getOrNull() + } + + private fun buildDynamicAddressList(ensName: String?, dynamicAddress: String): List = + buildList { + ensName?.let { ens -> + add(ReceiveAddressModel(displayType = ReceiveAddressModel.DisplayType.Ens, value = ens)) + } + add( + ReceiveAddressModel( + displayType = ReceiveAddressModel.DisplayType.Dynamic, + value = dynamicAddress, + ), + ) + } + + private fun buildStandardAddressList(ensName: String?, addresses: NetworkAddress): List = + buildList { + ensName?.let { ens -> + add(ReceiveAddressModel(displayType = ReceiveAddressModel.DisplayType.Ens, value = ens)) + } + addresses.availableAddresses.map { address -> + add( + ReceiveAddressModel( + displayType = when (address.type) { + NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.DisplayType.Default + NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.DisplayType.Legacy + }, + value = address.value, + ), + ) + } + } + suspend fun createForNft( userWalletId: UserWalletId, addresses: NetworkAddress, @@ -82,7 +124,7 @@ class ReceiveAddressesFactory( ensName?.let { ens -> add( ReceiveAddressModel( - nameService = ReceiveAddressModel.NameService.Ens, + displayType = ReceiveAddressModel.DisplayType.Ens, value = ens, ), ) @@ -90,9 +132,9 @@ class ReceiveAddressesFactory( addresses.availableAddresses.map { address -> add( ReceiveAddressModel( - nameService = when (address.type) { - NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.NameService.Default - NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.NameService.Legacy + displayType = when (address.type) { + NetworkAddress.Address.Type.Primary -> ReceiveAddressModel.DisplayType.Default + NetworkAddress.Address.Type.Secondary -> ReceiveAddressModel.DisplayType.Legacy }, value = address.value, ), diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt index b1a7a7133a..40b49906f5 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayAddFundsModel.kt @@ -5,7 +5,7 @@ import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.models.ReceiveAddressModel -import com.tangem.domain.models.ReceiveAddressModel.NameService +import com.tangem.domain.models.ReceiveAddressModel.DisplayType import com.tangem.domain.pay.TangemPayCryptoCurrencyFactory import com.tangem.domain.pay.model.TangemPayTopUpData import com.tangem.domain.wallets.usecase.GetUserWalletUseCase @@ -42,7 +42,7 @@ internal class TangemPayAddFundsModel @Inject constructor( depositAddress = params.depositAddress, receiveAddress = listOf( ReceiveAddressModel( - nameService = NameService.Default, + displayType = DisplayType.Default, value = params.depositAddress, ), ), diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt index b5264d8402..1a65fcfde1 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/ReceiveAddress.kt @@ -19,6 +19,8 @@ internal data class ReceiveAddress( data class Default(override val displayName: TextReference) : Primary data class Legacy(override val displayName: TextReference) : Primary + + data class Dynamic(override val displayName: TextReference) : Primary } } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt index bcd955a4af..8ca7790141 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/entity/TokenReceiveStateFactory.kt @@ -20,6 +20,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList +@Suppress("MagicNumber") internal class TokenReceiveStateFactory( private val currentStateProvider: Provider, private val cryptoCurrency: CryptoCurrency, @@ -66,8 +67,9 @@ internal class TokenReceiveStateFactory( compareBy { address -> when (address.type) { is Ens -> 0 - is Primary.Default -> 1 - is Primary.Legacy -> 2 + is Primary.Dynamic -> 1 + is Primary.Default -> 2 + is Primary.Legacy -> 3 } }, ) @@ -82,32 +84,18 @@ internal class TokenReceiveStateFactory( addresses: List, cryptoCurrency: CryptoCurrency, ): ImmutableList { - val needUseToLegacyAndDefaultName = addresses.any { it.nameService == ReceiveAddressModel.NameService.Legacy } + val shouldUseToLegacyAndDefaultName = addresses.any { it.displayType == ReceiveAddressModel.DisplayType.Legacy } val receiveAddresses = addresses.map { model -> - val type = when (model.nameService) { - ReceiveAddressModel.NameService.Default -> { - val displayName = when (cryptoCurrency) { - is CryptoCurrency.Coin -> cryptoCurrency.name - is CryptoCurrency.Token -> cryptoCurrency.symbol - } - - Primary.Default( - displayName = if (needUseToLegacyAndDefaultName) { - TextReference.Res(R.string.domain_receive_assets_default_address) - } else { - TextReference.Combined( - wrappedList( - TextReference.Str(displayName), - TextReference.Str(" "), - TextReference.Res(R.string.common_address), - ), - ) - }, - ) + val type = when (model.displayType) { + ReceiveAddressModel.DisplayType.Default -> { + Primary.Default(displayName = defaultDisplayName(cryptoCurrency, shouldUseToLegacyAndDefaultName)) } - ReceiveAddressModel.NameService.Ens -> Ens - ReceiveAddressModel.NameService.Legacy -> Primary.Legacy( + ReceiveAddressModel.DisplayType.Dynamic -> { + Primary.Dynamic(displayName = coinAddressDisplayName(cryptoCurrency)) + } + ReceiveAddressModel.DisplayType.Ens -> Ens + ReceiveAddressModel.DisplayType.Legacy -> Primary.Legacy( displayName = resourceReference( R.string.domain_receive_assets_legacy_address, WrappedList(listOf(cryptoCurrency.name)), @@ -124,8 +112,9 @@ internal class TokenReceiveStateFactory( compareBy { address -> when (address.type) { is Ens -> 0 - is Primary.Default -> 1 - is Primary.Legacy -> 2 + is Primary.Dynamic -> 1 + is Primary.Default -> 2 + is Primary.Legacy -> 3 } }, ) @@ -170,4 +159,29 @@ internal class TokenReceiveStateFactory( isGrayscale = false, shouldShowCustomBadge = false, ) + + private fun defaultDisplayName( + cryptoCurrency: CryptoCurrency, + needUseToLegacyAndDefaultName: Boolean, + ): TextReference { + return if (needUseToLegacyAndDefaultName) { + TextReference.Res(R.string.domain_receive_assets_default_address) + } else { + coinAddressDisplayName(cryptoCurrency) + } + } + + private fun coinAddressDisplayName(cryptoCurrency: CryptoCurrency): TextReference { + val displayName = when (cryptoCurrency) { + is CryptoCurrency.Coin -> cryptoCurrency.name + is CryptoCurrency.Token -> cryptoCurrency.symbol + } + return TextReference.Combined( + wrappedList( + TextReference.Str(displayName), + TextReference.Str(" "), + TextReference.Res(R.string.common_address), + ), + ) + } } \ No newline at end of file diff --git a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt index 63202e22b0..61602b31b4 100644 --- a/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt +++ b/features/token-recieve/impl/src/main/java/com/tangem/features/tokenreceive/ui/TokenReceiveAssetsContent.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -300,6 +301,7 @@ private fun PrimaryAddressesItems( onShareClick = { onShareClick(selectedAddress.value) }, primaryType = selectedAddress.type as ReceiveAddress.Type.Primary, address = selectedAddress.value, + isDynamicAddress = selectedAddress.type is ReceiveAddress.Type.Primary.Dynamic, snackbarHostState = snackbarHostState, ) } @@ -345,6 +347,7 @@ private fun AddressItem( onShareClick: () -> Unit, primaryType: ReceiveAddress.Type.Primary, address: String, + isDynamicAddress: Boolean, snackbarHostState: SnackbarHostState, modifier: Modifier = Modifier, ) { @@ -369,6 +372,11 @@ private fun AddressItem( SpacerH(12.dp) + if (isDynamicAddress) { + DynamicAddressBadge() + SpacerH(8.dp) + } + Text( text = primaryType.displayName.resolveReference(), color = TangemTheme.colors.text.primary1, @@ -564,6 +572,34 @@ private fun ActionButtonWithResizableText(config: ActionButtonConfig, modifier: ) } +@Composable +private fun DynamicAddressBadge(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .background( + color = TangemTheme.colors.text.accent.copy(alpha = 0.1f), + shape = RoundedCornerShape(percent = 50), + ) + .padding(horizontal = 12.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + Icon( + painter = painterResource( + id = R.drawable.ic_dynamic_addresses_badge_16, + ), + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = TangemTheme.colors.icon.accent, + ) + Text( + text = stringResourceSafe(R.string.dynamic_addresses_receive_badge), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.text.accent, + ) + } +} + @Composable @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d9b87d6b95..7dce45b0c8 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -5,7 +5,7 @@ # https://github.com/tangem/tangem-sdk-android/ # https://github.com/tangem/vico -tangemBlockchainSdk = "develop-1486" +tangemBlockchainSdk = "develop-1487" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "develop-602" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 948a63d6b6f764f2580615c978e4c73706f95fcc Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 10 Apr 2026 14:20:59 +0400 Subject: [PATCH 23/32] Updated on 2026-08-14 --- .../local/network/entity/NetworkStatusDM.kt | 16 ++++-- data/networks/detekt-baseline-debug.xml | 15 ------ .../converters/NetworkAmountsConverter.kt | 18 +++---- ...erter.kt => NetworkCurrencyIdConverter.kt} | 22 ++++---- .../NetworkStatusDataModelConverter.kt | 4 +- .../NetworkYieldSupplyStatusConverter.kt | 16 +++--- .../SimpleNetworkStatusConverter.kt | 10 ++-- .../di/NetworkStatusSupplierModule.kt | 19 ++++--- .../multi/DefaultMultiNetworkStatusFetcher.kt | 4 +- .../repository/DefaultNetworksRepository.kt | 54 +++++++++---------- .../converters/NetworkAmountsConverterTest.kt | 16 +++--- ...t.kt => NetworkCurrencyIdConverterTest.kt} | 26 ++++----- .../NetworkStatusDataModelConverterTest.kt | 2 +- .../NetworkYieldSupplyStatusConverterTest.kt | 12 ++--- .../tangem/domain/models/network/Network.kt | 2 +- domain/networks/detekt-baseline-main.xml | 8 --- .../multi/MultiNetworkStatusSupplier.kt | 2 +- .../single/SingleNetworkStatusSupplier.kt | 2 +- .../com/tangem/lib/crypto/BlockchainUtils.kt | 2 +- 19 files changed, 117 insertions(+), 133 deletions(-) delete mode 100644 data/networks/detekt-baseline-debug.xml rename data/networks/src/main/java/com/tangem/data/networks/converters/{CurrencyIdConverter.kt => NetworkCurrencyIdConverter.kt} (86%) rename data/networks/src/test/java/com/tangem/data/networks/converters/{CurrencyIdConverterTest.kt => NetworkCurrencyIdConverterTest.kt} (84%) delete mode 100644 domain/networks/detekt-baseline-main.xml diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt index 8d7cd539c0..f05d2b8240 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt @@ -29,11 +29,12 @@ sealed interface NetworkStatusDM { /** * Verified * - * @property networkId network id - * @property derivationPath derivation path - * @property selectedAddress selected address - * @property availableAddresses available addresses - * @property amounts amounts + * @property networkId network id + * @property derivationPath derivation path + * @property selectedAddress selected address + * @property availableAddresses available addresses + * @property amounts amounts + * @property yieldSupplyStatuses yield supply statuses */ @NameLabel("amounts") data class Verified( @@ -65,6 +66,11 @@ sealed interface NetworkStatusDM { @Json(name = "error_message") val errorMessage: String, ) : NetworkStatusDM + /** + * Id + * + * @property value blockchain id [com.tangem.blockchain.common.Blockchain.id] + */ @JsonClass(generateAdapter = true) data class ID( @Json(name = "value") val value: String, diff --git a/data/networks/detekt-baseline-debug.xml b/data/networks/detekt-baseline-debug.xml deleted file mode 100644 index 981dd137d6..0000000000 --- a/data/networks/detekt-baseline-debug.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - MultilineLambdaItParameter:CommonNetworkStatusFetcher.kt$CommonNetworkStatusFetcher${ Timber.e("Failed to fetch network status for $userWalletId [${network.rawId}]: $it") networksStatusesStore.setSourceAsOnlyCache(userWalletId = userWalletId, network = network) } - MultilineLambdaItParameter:DefaultMultiNetworkStatusFetcher.kt$DefaultMultiNetworkStatusFetcher${ networksStatusesStore.setSourceAsOnlyCache( userWalletId = params.userWalletId, networks = params.networks, ) raise(it) } - MultilineLambdaItParameter:DefaultNetworksRepository.kt$DefaultNetworksRepository${ Timber.e(it, "Unable to create wallet currencies") return emptyList() } - MultilineLambdaItParameter:DefaultNetworksRepository.kt$DefaultNetworksRepository${ Timber.e(it, "Unable to create wallet currencies") return@withContext } - MultilineLambdaItParameter:NetworkAmountsConverter.kt$NetworkAmountsConverter${ val amount = it.value as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null CurrencyAmount( id = currencyIdConverter.convertBack(value = it.key), amount = amount.value, ) } - MultilineLambdaItParameter:NetworkAmountsConverter.kt$NetworkAmountsConverter${ val currencyId = currencyIdConverter.convert(value = it.id) val amount = NetworkStatus.Amount.Loaded(value = it.amount) currencyId to amount } - MultilineLambdaItParameter:NetworkStatusSupplierModule.kt$NetworkStatusSupplierModule.<no name provided>${ "single_network_status_${it.userWalletId.stringValue}_${it.network.rawId}_" + it.network.derivationPath.value } - MultilineLambdaItParameter:NetworkYieldSupplyStatusConverter.kt$NetworkYieldSupplyStatusConverter${ val id = currencyIdConverter.convert(value = it.id) val status = YieldSupplyStatus( isActive = it.isActive, isInitialized = it.isInitialized, isAllowedToSpend = it.isAllowedToSpend, effectiveProtocolBalance = it.effectiveProtocolBalance, ) id to status } - SuspendFunSwallowedCancellation:DefaultNetworksRepository.kt$DefaultNetworksRepository$runCatching - - diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAmountsConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAmountsConverter.kt index a2876c0071..dc74b16cd8 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAmountsConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkAmountsConverter.kt @@ -12,33 +12,33 @@ private typealias AmountsDomainModel = Map { - private val currencyIdConverter = CurrencyIdConverter(rawNetworkId, derivationPath) + private val currencyIdConverter = NetworkCurrencyIdConverter(blockchainId, derivationPath) override fun convert(value: AmountsDataModel): AmountsDomainModel { - return value.associate { - val currencyId = currencyIdConverter.convert(value = it.id) - val amount = NetworkStatus.Amount.Loaded(value = it.amount) + return value.associate { currencyAmount -> + val currencyId = currencyIdConverter.convert(value = currencyAmount.id) + val amount = NetworkStatus.Amount.Loaded(value = currencyAmount.amount) currencyId to amount } } override fun convertBack(value: AmountsDomainModel): AmountsDataModel { - return value.mapNotNull { - val amount = it.value as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null + return value.mapNotNull { (currencyId, networkAmount) -> + val amount = networkAmount as? NetworkStatus.Amount.Loaded ?: return@mapNotNull null CurrencyAmount( - id = currencyIdConverter.convertBack(value = it.key), + id = currencyIdConverter.convertBack(value = currencyId), amount = amount.value, ) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/CurrencyIdConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkCurrencyIdConverter.kt similarity index 86% rename from data/networks/src/main/java/com/tangem/data/networks/converters/CurrencyIdConverter.kt rename to data/networks/src/main/java/com/tangem/data/networks/converters/NetworkCurrencyIdConverter.kt index 728b1db741..8bef0658ac 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/CurrencyIdConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkCurrencyIdConverter.kt @@ -1,6 +1,6 @@ package com.tangem.data.networks.converters -import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.blockchainsdk.utils.toCoinId import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId import com.tangem.datasource.local.network.entity.NetworkStatusDM.CurrencyId.Companion.CONTRACT_ADDRESS_DELIMITER @@ -12,13 +12,13 @@ import com.tangem.domain.models.currency.CryptoCurrency.ID.Suffix as CurrencyIdS /** * Converts between [CurrencyId] and [CryptoCurrency.ID]. * - * @property rawNetworkId the raw network ID associated with the currency + * @property blockchainId the blockchain ID associated with the currency * @property derivationPath the derivation path used for the network * [REDACTED_AUTHOR] */ -internal class CurrencyIdConverter( - private val rawNetworkId: String, +internal class NetworkCurrencyIdConverter( + private val blockchainId: String, private val derivationPath: Network.DerivationPath, ) : TwoWayConverter { @@ -31,7 +31,7 @@ internal class CurrencyIdConverter( return if (contractAddress.isNullOrBlank()) { getCoinId( coinId = rawId.takeUnless { it.isNullOrBlank() } - ?: error("Coin id is null for $rawNetworkId with $derivationPath"), + ?: error("Coin id is null for $blockchainId with $derivationPath"), ) } else { getTokenId( @@ -43,14 +43,12 @@ internal class CurrencyIdConverter( override fun convertBack(value: CryptoCurrency.ID): CurrencyId { return if (value.isCoin) { - CurrencyId.createCoinId( - coinId = Blockchain.fromId(value.rawNetworkId).toCoinId(), - ) + CurrencyId.createCoinId(coinId = value.toBlockchain().toCoinId()) } else { CurrencyId.createTokenId( rawTokenId = value.rawCurrencyId?.value, contractAddress = requireNotNull(value.contractAddress) { - "Token contractAddress is null for token id: $this" + "Token contractAddress is null for token id: $value" }, ) } @@ -82,17 +80,17 @@ internal class CurrencyIdConverter( return when (derivationPath) { is Network.DerivationPath.Card -> { CryptoCurrency.ID.Body.NetworkIdWithDerivationPath( - rawId = rawNetworkId, + rawId = blockchainId, derivationPath = derivationPath.value, ) } is Network.DerivationPath.Custom -> { CryptoCurrency.ID.Body.NetworkIdWithDerivationPath( - rawId = rawNetworkId, + rawId = blockchainId, derivationPath = derivationPath.value, ) } - is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(rawNetworkId) + is Network.DerivationPath.None -> CryptoCurrency.ID.Body.NetworkId(blockchainId) } } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt index d876b01234..7f0d1b6fd3 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverter.kt @@ -18,11 +18,11 @@ internal object NetworkStatusDataModelConverter : Converter internal class NetworkYieldSupplyStatusConverter( - rawNetworkId: String, + blockchainId: String, derivationPath: Network.DerivationPath, ) : TwoWayConverter { - private val currencyIdConverter = CurrencyIdConverter(rawNetworkId, derivationPath) + private val currencyIdConverter = NetworkCurrencyIdConverter(blockchainId, derivationPath) override fun convert(value: YieldSupplyStatusDataModel): YieldSupplyStatusDomainModel { - return value.associate { - val id = currencyIdConverter.convert(value = it.id) + return value.associate { yieldSupplyStatus -> + val id = currencyIdConverter.convert(value = yieldSupplyStatus.id) val status = YieldSupplyStatus( - isActive = it.isActive, - isInitialized = it.isInitialized, - isAllowedToSpend = it.isAllowedToSpend, - effectiveProtocolBalance = it.effectiveProtocolBalance, + isActive = yieldSupplyStatus.isActive, + isInitialized = yieldSupplyStatus.isInitialized, + isAllowedToSpend = yieldSupplyStatus.isAllowedToSpend, + effectiveProtocolBalance = yieldSupplyStatus.effectiveProtocolBalance, ) id to status diff --git a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt index b82bfb9455..940cfdea59 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/converters/SimpleNetworkStatusConverter.kt @@ -25,15 +25,15 @@ internal object SimpleNetworkStatusConverter : Converter + listOf( + "single_network_status", + params.userWalletId.stringValue, + params.network.rawId, + params.network.derivationPath.value, + ) + .joinToString(separator = "_") }, - ) {} + ) } @Provides @Singleton fun provideMultiNetworkStatusSupplier(factory: MultiNetworkStatusProducer.Factory): MultiNetworkStatusSupplier { - return object : MultiNetworkStatusSupplier( + return MultiNetworkStatusSupplier( factory = factory, keyCreator = { "multi_networks_statuses_${it.userWalletId.stringValue}" }, - ) {} + ) } } \ No newline at end of file diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index b039edc5ba..e429c134da 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -40,13 +40,13 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( val networksCurrencies = catch( block = { createNetworksCurrenciesMap(params) }, - catch = { + catch = { error -> networksStatusesStore.setSourceAsOnlyCache( userWalletId = params.userWalletId, networks = params.networks, ) - raise(it) + raise(error) }, ) diff --git a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt index b5a2fb854c..defc120690 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt @@ -11,6 +11,7 @@ import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.withContext @@ -33,13 +34,12 @@ internal class DefaultNetworksRepository( override suspend fun fetchPendingTransactions(userWalletId: UserWalletId, network: Network) { withContext(dispatchers.default) { - val currencies = runCatching { + val currencies = runSuspendCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + }.getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) + return@withContext } - .getOrElse { error -> - TangemLogger.e("Unable to create wallet currencies", error) - return@withContext - } fetchPendingTransactions(userWalletId = userWalletId, network = network, currencies = currencies) } @@ -49,34 +49,34 @@ internal class DefaultNetworksRepository( userWalletId: UserWalletId, network: Network, ): List { - return runCatching { cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) } - .getOrElse { error -> - TangemLogger.e("Unable to create wallet currencies", error) - return emptyList() - } - .map { currency -> - CryptoCurrencyAddress( - cryptoCurrency = currency, - address = getDefaultAddress(userWalletId, network).orEmpty(), - ) - } + return runSuspendCatching { + cardCryptoCurrencyFactory.create(userWalletId = userWalletId, network = network) + }.getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) + return emptyList() + }.map { currency -> + CryptoCurrencyAddress( + cryptoCurrency = currency, + address = getDefaultAddress(userWalletId, network).orEmpty(), + ) + } } override suspend fun getNetworkAddresses( userWalletId: UserWalletId, network: Network.RawID, ): List { - return runCatching { cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network) } - .getOrElse { error -> - TangemLogger.e("Unable to create wallet currencies", error) - return emptyList() - } - .map { currency -> - CryptoCurrencyAddress( - cryptoCurrency = currency, - address = getDefaultAddress(userWalletId, currency.network).orEmpty(), - ) - } + return runSuspendCatching { + cardCryptoCurrencyFactory.createByRawId(userWalletId = userWalletId, network = network) + }.getOrElse { error -> + TangemLogger.e("Unable to create wallet currencies", error) + return emptyList() + }.map { currency -> + CryptoCurrencyAddress( + cryptoCurrency = currency, + address = getDefaultAddress(userWalletId, currency.network).orEmpty(), + ) + } } override suspend fun getDefaultAddress(userWalletId: UserWalletId, network: Network): String? { diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAmountsConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAmountsConverterTest.kt index 5bbe4daf2d..4ca75519b9 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAmountsConverterTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkAmountsConverterTest.kt @@ -16,10 +16,10 @@ import java.math.BigDecimal @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class NetworkAmountsConverterTest { - private val rawNetworkId = "ETH" + private val rawNetworkId = "ethereum" private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0") private val derivationPathHashCode = "-1843072795" - private val converter = NetworkAmountsConverter(rawNetworkId = rawNetworkId, derivationPath = derivationPath) + private val converter = NetworkAmountsConverter(blockchainId = rawNetworkId, derivationPath = derivationPath) @Test fun convert() { @@ -47,12 +47,12 @@ internal class NetworkAmountsConverterTest { // Assert val expected = mapOf( - ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE), + ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE), ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7", ) to Loaded(value = BigDecimal.ZERO), ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", ) to Loaded(value = BigDecimal.TEN), ) @@ -63,12 +63,12 @@ internal class NetworkAmountsConverterTest { fun convertBack() { // Arrange val value = mapOf( - ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE), + ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum") to Loaded(value = BigDecimal.ONE), ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7", ) to Loaded(value = BigDecimal.ZERO), ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", ) to Loaded(value = BigDecimal.TEN), ) diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/CurrencyIdConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkCurrencyIdConverterTest.kt similarity index 84% rename from data/networks/src/test/java/com/tangem/data/networks/converters/CurrencyIdConverterTest.kt rename to data/networks/src/test/java/com/tangem/data/networks/converters/NetworkCurrencyIdConverterTest.kt index 9cdcc4810e..9e129e6cd4 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/converters/CurrencyIdConverterTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkCurrencyIdConverterTest.kt @@ -13,12 +13,12 @@ import org.junit.jupiter.params.ParameterizedTest [REDACTED_AUTHOR] */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) -class CurrencyIdConverterTest { +class NetworkCurrencyIdConverterTest { - private val rawNetworkId = "ETH" + private val rawNetworkId = "ethereum" private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0") private val derivationPathHashCode = "-1843072795" - private val converter = CurrencyIdConverter(rawNetworkId = rawNetworkId, derivationPath = derivationPath) + private val converter = NetworkCurrencyIdConverter(blockchainId = rawNetworkId, derivationPath = derivationPath) @Nested @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -48,7 +48,7 @@ class CurrencyIdConverterTest { ConvertModel( value = CurrencyId.createCoinId("ethereum"), expected = Result.success( - CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩ethereum"), + CryptoCurrency.ID.fromValue(value = "coin⟨ethereum→$derivationPathHashCode⟩ethereum"), ), ), ConvertModel( @@ -71,7 +71,7 @@ class CurrencyIdConverterTest { ), expected = Result.success( CryptoCurrency.ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7", ), ), ), @@ -82,7 +82,7 @@ class CurrencyIdConverterTest { ), expected = Result.success( CryptoCurrency.ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", ), ), ), @@ -93,7 +93,7 @@ class CurrencyIdConverterTest { ), expected = Result.success( CryptoCurrency.ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", ), ), ), @@ -104,7 +104,7 @@ class CurrencyIdConverterTest { ), expected = Result.success( CryptoCurrency.ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", ), ), ), @@ -114,7 +114,7 @@ class CurrencyIdConverterTest { contractAddress = "", ), expected = Result.success( - CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩usdt"), + CryptoCurrency.ID.fromValue(value = "coin⟨ethereum→$derivationPathHashCode⟩usdt"), ), ), ConvertModel( @@ -123,7 +123,7 @@ class CurrencyIdConverterTest { contractAddress = " ", ), expected = Result.success( - CryptoCurrency.ID.fromValue(value = "coin⟨ETH→$derivationPathHashCode⟩usdt"), + CryptoCurrency.ID.fromValue(value = "coin⟨ethereum→$derivationPathHashCode⟩usdt"), ), ), ) @@ -154,14 +154,14 @@ class CurrencyIdConverterTest { private fun provideTestModels(): Collection = listOf( ConvertBackModel( - value = CryptoCurrency.ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum"), + value = CryptoCurrency.ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum"), expected = Result.success( CurrencyId.createCoinId("ethereum"), ), ), ConvertBackModel( value = CryptoCurrency.ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩usdt⚓0xdAC17F958D2ee523a2206206994597C13D831ec7", ), expected = Result.success( CurrencyId.createTokenId( @@ -172,7 +172,7 @@ class CurrencyIdConverterTest { ), ConvertBackModel( value = CryptoCurrency.ID.fromValue( - value = "token⟨ETH→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", + value = "token⟨ethereum→$derivationPathHashCode⟩0xdAC17F958D2ee523a2206206994597C13D831ec7", ), expected = Result.success( CurrencyId.createTokenId( diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt index b995196d80..a667da490a 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkStatusDataModelConverterTest.kt @@ -50,7 +50,7 @@ internal class NetworkStatusDataModelConverterTest { ), ), amounts = mapOf( - ID.fromValue(value = "coin⟨ETH→0⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO), + ID.fromValue(value = "coin⟨ethereum→0⟩ethereum") to Amount.Loaded(value = BigDecimal.ZERO), ID( prefix = Prefix.COIN_PREFIX, body = Body.NetworkId(rawId = "BTC"), diff --git a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkYieldSupplyStatusConverterTest.kt b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkYieldSupplyStatusConverterTest.kt index af36491079..b18f2e14d2 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkYieldSupplyStatusConverterTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/converters/NetworkYieldSupplyStatusConverterTest.kt @@ -13,7 +13,7 @@ import java.math.BigDecimal @TestInstance(TestInstance.Lifecycle.PER_CLASS) internal class NetworkYieldSupplyStatusConverterTest { - private val rawNetworkId = "ETH" + private val rawNetworkId = "ethereum" private val derivationPath = Network.DerivationPath.Card(value = "m/44'/60'/0'/0/0") private val derivationPathHashCode = "-1843072795" private val converter = NetworkYieldSupplyStatusConverter(rawNetworkId, derivationPath) @@ -38,8 +38,8 @@ internal class NetworkYieldSupplyStatusConverterTest { // Assert val expected = mapOf( - ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to domainStatus, - ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus, + ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum") to domainStatus, + ID.fromValue("token⟨ethereum→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus, ) Truth.assertThat(actual).containsExactlyEntriesIn(expected) @@ -49,9 +49,9 @@ internal class NetworkYieldSupplyStatusConverterTest { fun convertBack() { // Arrange val value = mapOf( - ID.fromValue("coin⟨ETH→$derivationPathHashCode⟩ethereum") to domainStatus, - ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus, - ID.fromValue("token⟨ETH→$derivationPathHashCode⟩usdc⚓0x1") to null, + ID.fromValue("coin⟨ethereum→$derivationPathHashCode⟩ethereum") to domainStatus, + ID.fromValue("token⟨ethereum→$derivationPathHashCode⟩usdt⚓0x1") to domainStatus, + ID.fromValue("token⟨ethereum→$derivationPathHashCode⟩usdc⚓0x1") to null, ) // Act diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt index 489ede3a4d..028a16d2a2 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/network/Network.kt @@ -47,7 +47,7 @@ data class Network( /** * Represents a unique identifier for a blockchain network * - * @property rawId raw network ID + * @property rawId raw network ID (backend id) * @property derivationPath derivation path */ @Serializable diff --git a/domain/networks/detekt-baseline-main.xml b/domain/networks/detekt-baseline-main.xml deleted file mode 100644 index eaf966bb38..0000000000 --- a/domain/networks/detekt-baseline-main.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - UnnecessaryAbstractClass:MultiNetworkStatusSupplier.kt$MultiNetworkStatusSupplier$MultiNetworkStatusSupplier - UnnecessaryAbstractClass:SingleNetworkStatusSupplier.kt$SingleNetworkStatusSupplier$SingleNetworkStatusSupplier - - diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt index 65d7623cdc..ea96369684 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/multi/MultiNetworkStatusSupplier.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkStatus * [REDACTED_AUTHOR] */ -abstract class MultiNetworkStatusSupplier( +open class MultiNetworkStatusSupplier( override val factory: MultiNetworkStatusProducer.Factory, override val keyCreator: (MultiNetworkStatusProducer.Params) -> String, ) : FlowCachingSupplier>() \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusSupplier.kt b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusSupplier.kt index 8fc7770c0f..117216c91f 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusSupplier.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/single/SingleNetworkStatusSupplier.kt @@ -11,7 +11,7 @@ import com.tangem.domain.models.network.NetworkStatus * [REDACTED_AUTHOR] */ -abstract class SingleNetworkStatusSupplier( +open class SingleNetworkStatusSupplier( override val factory: SingleNetworkStatusProducer.Factory, override val keyCreator: (SingleNetworkStatusProducer.Params) -> String, ) : FlowCachingSupplier() \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/BlockchainUtils.kt index fe00a42eb6..f3e5669802 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 @@ -42,7 +42,7 @@ object BlockchainUtils { return blockchain == Blockchain.Bitcoin || blockchain == Blockchain.BitcoinTestnet } - /** Checks if the current [blockchainId] uses a custom fee converter */ + /** Checks if the current [networkId] uses a custom fee converter */ fun isUseBitcoinFeeConverter(networkId: String): Boolean { val blockchain = networkId.toBlockchain() return isBitcoin(networkId) || blockchain == Blockchain.Fact0rn From 2e431b9261b8158312ff644efb51322a34cfda5d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Apr 2026 12:03:16 +0300 Subject: [PATCH 24/32] Updated on 2026-08-14 --- .../configs/excluded_blockchains_config.json | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json index 59809c4e40..3d5c1150dc 100644 --- a/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json +++ b/core/config-toggles/src/main/assets/configs/excluded_blockchains_config.json @@ -15,24 +15,8 @@ "name": "vanar-chain", "version": "undefined" }, - { - "name": "sonic", - "version": "5.21.0" - }, - { - "name": "apechain", - "version": "5.21.0" - }, - { - "name": "alephium", - "version": "5.21.0" - }, { "name": "zklink", "version": "undefined" - }, - { - "name": "plasma", - "version": "5.31" } ] \ No newline at end of file From 9ef791c432ef32a00be0143538febcd42bc067c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 9 Apr 2026 16:33:29 +0400 Subject: [PATCH 25/32] Updated on 2026-08-14 --- .../appsettings/AppSettingsDialogsFactory.kt | 86 ++++++-------- .../ui/appsettings/AppSettingsScreen.kt | 16 +-- .../ui/appsettings/AppSettingsScreenState.kt | 27 +---- .../DefaultAppSettingsComponent.kt | 32 ++++-- .../ui/appsettings/SettingsSelectorDialog.kt | 38 +++++++ .../components/SettingsAlertDialog.kt | 54 --------- .../components/SettingsSelectorDialog.kt | 52 --------- .../model/AppSettingsDialogConfig.kt | 10 ++ .../ui/appsettings/model/AppSettingsModel.kt | 106 +++++++----------- 9 files changed, 152 insertions(+), 269 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/SettingsSelectorDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt delete mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt create mode 100644 app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsDialogConfig.kt diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt index 106fad8b50..20d7536526 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsDialogsFactory.kt @@ -2,71 +2,59 @@ package com.tangem.tap.features.details.ui.appsettings import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.wrappedList -import com.tangem.domain.apptheme.model.AppThemeMode -import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog +import com.tangem.core.ui.message.DialogMessage +import com.tangem.core.ui.message.EventMessageAction import com.tangem.wallet.R -import kotlinx.collections.immutable.toImmutableList internal class AppSettingsDialogsFactory { - fun createThemeModeSelectorDialog( - selectedModeIndex: Int, - onSelect: (AppThemeMode) -> Unit, - onDismiss: () -> Unit, - ): Dialog.Selector { - val modes = AppThemeMode.available - - return Dialog.Selector( - title = resourceReference(R.string.app_settings_theme_selector_title), - selectedItemIndex = selectedModeIndex, - items = modes.map { mode -> - resourceReference( - id = when (mode) { - AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark - AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light - AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system - }, - ) - }.toImmutableList(), - onSelect = { index -> - val mode = AppThemeMode.available[index] - - onSelect(mode) - }, - onDismiss = onDismiss, - ) - } - - fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { - return Dialog.Alert( + fun createDisableBiometricAuthenticationAlert(onDisable: () -> Unit): DialogMessage { + return DialogMessage( title = resourceReference(R.string.common_attention), - description = resourceReference( + message = resourceReference( R.string.app_settings_off_biometrics_alert_message, wrappedList(resourceReference(R.string.common_biometrics)), ), - confirmText = resourceReference(R.string.common_disable), - onConfirm = onDisable, - onDismiss = onDismiss, + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_disable), + isWarning = true, + onClick = onDisable, + ) + }, + secondActionBuilder = { cancelAction() }, ) } - fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { - return Dialog.Alert( + fun createEnableRequireAccessCodeAlert(onEnable: () -> Unit): DialogMessage { + return DialogMessage( title = resourceReference(R.string.common_attention), - description = resourceReference(R.string.app_settings_on_require_access_code_alert_message), - confirmText = resourceReference(R.string.common_enable), - onConfirm = { onEnable() }, - onDismiss = onDismiss, + message = resourceReference(R.string.app_settings_on_require_access_code_alert_message), + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_enable), + onClick = onEnable, + ) + }, + secondActionBuilder = { cancelAction() }, ) } - fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit, onDismiss: () -> Unit): Dialog.Alert { - return Dialog.Alert( + fun createDisableRequireAccessCodeAlert(onDisable: () -> Unit): DialogMessage { + return DialogMessage( title = resourceReference(R.string.common_attention), - description = resourceReference(R.string.app_settings_off_require_access_code_alert_message), - confirmText = resourceReference(R.string.common_disable), - onConfirm = { onDisable() }, - onDismiss = onDismiss, + message = resourceReference(R.string.app_settings_off_require_access_code_alert_message), + isDismissable = false, + firstActionBuilder = { + EventMessageAction( + title = resourceReference(R.string.common_disable), + isWarning = true, + onClick = onDisable, + ) + }, + secondActionBuilder = { cancelAction() }, ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt index d17e6b23b6..8ffb314316 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreen.kt @@ -8,8 +8,6 @@ import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.tooling.preview.Preview @@ -42,13 +40,6 @@ internal fun AppSettingsScreen(state: AppSettingsScreenState, onBackClick: () -> @Composable private fun AppSettings(state: AppSettingsScreenState.Content) { - val dialog by rememberUpdatedState(newValue = state.dialog) - when (val safeDialog = dialog) { - is AppSettingsScreenState.Dialog.Alert -> SettingsAlertDialog(dialog = safeDialog) - is AppSettingsScreenState.Dialog.Selector -> SettingsSelectorDialog(dialog = safeDialog) - null -> Unit - } - val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() } LazyColumn( @@ -102,12 +93,7 @@ private class AppSettingsScreenStateProvider : CollectionPreviewParameterProvide itemsFactory.createSelectThemeModeButton(AppThemeMode.DEFAULT, {}), ) - add( - AppSettingsScreenState.Content( - items = items, - dialog = null, - ), - ) + add(AppSettingsScreenState.Content(items = items)) }, ) // endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt index f73524e227..615d6248ce 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/AppSettingsScreenState.kt @@ -10,10 +10,7 @@ internal sealed class AppSettingsScreenState { object Loading : AppSettingsScreenState() - data class Content( - val items: ImmutableList, - val dialog: Dialog?, - ) : AppSettingsScreenState() + data class Content(val items: ImmutableList) : AppSettingsScreenState() @Immutable sealed class Item { @@ -46,26 +43,4 @@ internal sealed class AppSettingsScreenState { val onClick: () -> Unit, ) : Item() } - - @Immutable - sealed class Dialog { - - abstract val onDismiss: () -> Unit - - data class Alert( - val title: TextReference, - val description: TextReference, - val confirmText: TextReference, - val onConfirm: () -> Unit, - override val onDismiss: () -> Unit, - ) : Dialog() - - data class Selector( - val title: TextReference, - val selectedItemIndex: Int, - val items: ImmutableList, - val onSelect: (Int) -> Unit, - override val onDismiss: () -> Unit, - ) : Dialog() - } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/DefaultAppSettingsComponent.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/DefaultAppSettingsComponent.kt index a92b632003..116aebd3d5 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/DefaultAppSettingsComponent.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/DefaultAppSettingsComponent.kt @@ -4,42 +4,56 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.essenty.lifecycle.doOnResume -import com.tangem.common.routing.AppRouter import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.model.getOrCreateModel -import com.tangem.tap.common.extensions.dispatchNavigationAction import com.tangem.tap.features.details.ui.appsettings.api.AppSettingsComponent +import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsDialogConfig import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsModel -import com.tangem.tap.store import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -@Suppress("UnusedPrivateMember") internal class DefaultAppSettingsComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted params: Unit, + @Suppress("UnusedPrivateMember") @Assisted params: Unit, ) : AppSettingsComponent, AppComponentContext by appComponentContext { private val model: AppSettingsModel = getOrCreateModel() - init { + private val dialogSlot = childSlot( + source = model.dialogNavigation, + serializer = AppSettingsDialogConfig.serializer(), + handleBackButton = true, + childFactory = { config, _ -> config }, + ) + init { doOnResume { model.onResume() } } @Composable override fun Content(modifier: Modifier) { val state by model.uiState.collectAsStateWithLifecycle() + val dialog by dialogSlot.subscribeAsState() AppSettingsScreen( modifier = modifier, state = state, - onBackClick = { - store.dispatchNavigationAction(AppRouter::pop) - }, + onBackClick = model::onBackClick, ) + + dialog.child?.instance?.let { config -> + when (config) { + is AppSettingsDialogConfig.ThemeModeSelector -> SettingsSelectorDialog( + config = config, + onSelect = model::onThemeModeSelected, + onDismiss = model::dismissDialog, + ) + } + } } @AssistedFactory diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/SettingsSelectorDialog.kt new file mode 100644 index 0000000000..cf9f888c0e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/SettingsSelectorDialog.kt @@ -0,0 +1,38 @@ +package com.tangem.tap.features.details.ui.appsettings + +import androidx.compose.runtime.Composable +import com.tangem.core.ui.components.DialogButtonUM +import com.tangem.core.ui.components.SelectorDialog +import com.tangem.core.ui.extensions.stringResourceSafe +import com.tangem.domain.apptheme.model.AppThemeMode +import com.tangem.tap.features.details.ui.appsettings.model.AppSettingsDialogConfig +import com.tangem.wallet.R +import kotlinx.collections.immutable.toImmutableList + +@Composable +internal fun SettingsSelectorDialog( + config: AppSettingsDialogConfig.ThemeModeSelector, + onSelect: (Int) -> Unit, + onDismiss: () -> Unit, +) { + val modes = AppThemeMode.available + SelectorDialog( + title = stringResourceSafe(R.string.app_settings_theme_selector_title), + selectedItemIndex = config.selectedModeIndex, + items = modes.map { mode -> + stringResourceSafe( + id = when (mode) { + AppThemeMode.FORCE_DARK -> R.string.app_settings_theme_mode_dark + AppThemeMode.FORCE_LIGHT -> R.string.app_settings_theme_mode_light + AppThemeMode.FOLLOW_SYSTEM -> R.string.app_settings_theme_mode_system + }, + ) + }.toImmutableList(), + confirmButton = DialogButtonUM( + title = stringResourceSafe(R.string.common_cancel), + onClick = onDismiss, + ), + onSelect = onSelect, + onDismissDialog = onDismiss, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt deleted file mode 100644 index ff7916fd68..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsAlertDialog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.tangem.tap.features.details.ui.appsettings.components - -import android.content.res.Configuration -import androidx.compose.runtime.Composable -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.BasicDialog -import com.tangem.core.ui.components.DialogButtonUM -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory -import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog -import com.tangem.wallet.R - -@Composable -internal fun SettingsAlertDialog(dialog: Dialog.Alert) { - BasicDialog( - title = dialog.title.resolveReference(), - message = dialog.description.resolveReference(), - isDismissable = false, - confirmButton = DialogButtonUM( - title = dialog.confirmText.resolveReference(), - isWarning = true, - onClick = dialog.onConfirm, - ), - dismissButton = DialogButtonUM( - title = stringResourceSafe(id = R.string.common_cancel), - onClick = dialog.onDismiss, - ), - onDismissDialog = dialog.onDismiss, - ) -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun AlertDialogPreview(@PreviewParameter(AlertDialogProvider::class) dialog: Dialog.Alert) { - TangemThemePreview { - SettingsAlertDialog(dialog = dialog) - } -} - -private class AlertDialogProvider : CollectionPreviewParameterProvider( - collection = buildList { - val dialogsFactory = AppSettingsDialogsFactory() - - add(dialogsFactory.createThemeModeSelectorDialog(selectedModeIndex = 0, onSelect = {}, onDismiss = {})) - add(dialogsFactory.createDisableBiometricAuthenticationAlert(onDisable = {}, onDismiss = {})) - }, -) -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt deleted file mode 100644 index 3dba72ed5d..0000000000 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/components/SettingsSelectorDialog.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.tangem.tap.features.details.ui.appsettings.components - -import android.content.res.Configuration -import androidx.compose.runtime.Composable -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.DialogButtonUM -import com.tangem.core.ui.components.SelectorDialog -import com.tangem.core.ui.extensions.resolveReference -import com.tangem.core.ui.extensions.stringResourceSafe -import com.tangem.core.ui.res.TangemThemePreview -import com.tangem.tap.features.details.ui.appsettings.AppSettingsDialogsFactory -import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Dialog -import com.tangem.wallet.R -import kotlinx.collections.immutable.toImmutableList - -@Composable -internal fun SettingsSelectorDialog(dialog: Dialog.Selector) { - SelectorDialog( - title = dialog.title.resolveReference(), - selectedItemIndex = dialog.selectedItemIndex, - items = dialog.items.map { it.resolveReference() }.toImmutableList(), - confirmButton = DialogButtonUM( - title = stringResourceSafe(R.string.common_cancel), - onClick = dialog.onDismiss, - ), - onSelect = dialog.onSelect, - onDismissDialog = dialog.onDismiss, - ) -} - -// region Preview -@Preview(showBackground = true, widthDp = 360) -@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) -@Composable -private fun SettingsSelectorDialogPreview(@PreviewParameter(DialogProvider::class) param: Dialog.Selector) { - TangemThemePreview { - SettingsSelectorDialog(param) - } -} - -private class DialogProvider : CollectionPreviewParameterProvider( - collection = listOf( - AppSettingsDialogsFactory().createThemeModeSelectorDialog( - selectedModeIndex = 0, - onSelect = {}, - onDismiss = {}, - ), - ), -) -// endregion Preview \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsDialogConfig.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsDialogConfig.kt new file mode 100644 index 0000000000..4a53c9dc3e --- /dev/null +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsDialogConfig.kt @@ -0,0 +1,10 @@ +package com.tangem.tap.features.details.ui.appsettings.model + +import kotlinx.serialization.Serializable + +@Serializable +internal sealed interface AppSettingsDialogConfig { + + @Serializable + data class ThemeModeSelector(val selectedModeIndex: Int) : AppSettingsDialogConfig +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt index c9edf25848..181ae202e1 100644 --- a/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt +++ b/app/src/main/java/com/tangem/tap/features/details/ui/appsettings/model/AppSettingsModel.kt @@ -1,6 +1,9 @@ package com.tangem.tap.features.details.ui.appsettings.model import androidx.compose.runtime.Stable +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.doOnFailure import com.tangem.common.doOnSuccess import com.tangem.common.routing.AppRoute @@ -67,6 +70,8 @@ internal class AppSettingsModel @Inject constructor( private val itemsFactory = AppSettingsItemsFactory() private val dialogsFactory = AppSettingsDialogsFactory() + val dialogNavigation: SlotNavigation = SlotNavigation() + private val localState = MutableStateFlow(LocalState()) private val biometricsStatusJobHolder = JobHolder() @@ -94,10 +99,7 @@ internal class AppSettingsModel @Inject constructor( uiState.update { prevState -> when (prevState) { is AppSettingsScreenState.Content -> prevState.copy(items = items) - is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content( - items = items, - dialog = null, - ) + is AppSettingsScreenState.Loading -> AppSettingsScreenState.Content(items = items) } } } @@ -188,28 +190,31 @@ internal class AppSettingsModel @Inject constructor( settingsManager.openBiometricSettings() } + fun onBackClick() { + router.pop() + } + private fun showAppCurrencySelector() { router.push(AppRoute.AppCurrencySelector) } private fun showThemeModeSelector(selectedMode: AppThemeMode) { - updateContentState { - copy( - dialog = dialogsFactory.createThemeModeSelectorDialog( - selectedModeIndex = selectedMode.ordinal, - onSelect = { mode -> - analyticsEventHandler.send( - event = Settings.AppSettings.ThemeSwitched( - theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode), - ), - ) - changeAppThemeMode(mode) - dismissDialog() - }, - onDismiss = ::dismissDialog, - ), - ) - } + dialogNavigation.activate(AppSettingsDialogConfig.ThemeModeSelector(selectedMode.ordinal)) + } + + fun onThemeModeSelected(index: Int) { + val mode = AppThemeMode.available[index] + analyticsEventHandler.send( + event = Settings.AppSettings.ThemeSwitched( + theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode), + ), + ) + changeAppThemeMode(mode) + dialogNavigation.dismiss() + } + + fun dismissDialog() { + dialogNavigation.dismiss() } private fun onBiometricAuthenticationToggled(isChecked: Boolean) { @@ -219,17 +224,11 @@ internal class AppSettingsModel @Inject constructor( if (isChecked) { toggleBiometricsAuthentication(enable = true) } else { - updateContentState { - copy( - dialog = dialogsFactory.createDisableBiometricAuthenticationAlert( - onDisable = { - toggleBiometricsAuthentication(enable = false) - dismissDialog() - }, - onDismiss = ::dismissDialog, - ), - ) - } + uiMessageSender.send( + dialogsFactory.createDisableBiometricAuthenticationAlert( + onDisable = { toggleBiometricsAuthentication(enable = false) }, + ), + ) } } @@ -241,25 +240,17 @@ internal class AppSettingsModel @Inject constructor( // TODO : Uncomment and implement analytics event when ready // val param = AnalyticsParam.OnOffState(isChecked) // analyticsEventHandler.send(Settings.AppSettings.RequireAccessCodeChanged(param)) - updateContentState { - copy( - dialog = if (isChecked) { - dialogsFactory.createEnableRequireAccessCodeAlert( - onEnable = { - toggleRequireAccessCode(enable = true) - dismissDialog() - }, - onDismiss = ::dismissDialog, - ) - } else { - dialogsFactory.createDisableRequireAccessCodeAlert( - onDisable = { - toggleRequireAccessCode(enable = false) - dismissDialog() - }, - onDismiss = ::dismissDialog, - ) - }, + if (isChecked) { + uiMessageSender.send( + dialogsFactory.createEnableRequireAccessCodeAlert( + onEnable = { toggleRequireAccessCode(enable = true) }, + ), + ) + } else { + uiMessageSender.send( + dialogsFactory.createDisableRequireAccessCodeAlert( + onDisable = { toggleRequireAccessCode(enable = false) }, + ), ) } } @@ -371,10 +362,6 @@ internal class AppSettingsModel @Inject constructor( } } - private fun dismissDialog() { - updateContentState { copy(dialog = null) } - } - private fun bootstrapLocalState() = modelScope.launch { localState.update { state -> state.copy( @@ -394,15 +381,6 @@ internal class AppSettingsModel @Inject constructor( .launchIn(modelScope) } - private fun updateContentState(block: AppSettingsScreenState.Content.() -> AppSettingsScreenState.Content) { - uiState.update { prevState -> - when (prevState) { - is AppSettingsScreenState.Content -> block(prevState) - is AppSettingsScreenState.Loading -> prevState - } - } - } - private data class LocalState( val hasSecuredWallets: Boolean = false, val isEnrollBiometricsNeeded: Boolean = false, From 1afa4a5d0e84fd563135ace691e7acbabd839664 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Apr 2026 16:38:04 +0500 Subject: [PATCH 26/32] Updated on 2026-08-14 --- .../tangem/data/wallets/DefaultWalletsRepository.kt | 10 ++++++++-- .../domain/wallets/models/WalletSyncResult.kt | 6 ++++++ .../domain/wallets/repository/WalletsRepository.kt | 3 ++- .../wallets/usecase/SyncWalletWithRemoteUseCase.kt | 6 ++++-- .../im/port/model/AddExistingWalletImportModel.kt | 13 +++++++++++-- 5 files changed, 31 insertions(+), 7 deletions(-) create mode 100644 domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/WalletSyncResult.kt diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index 5a5278632e..f748ce863f 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -24,6 +24,7 @@ import com.tangem.domain.common.wallets.getSyncStrict import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.domain.wallets.models.WalletSyncResult import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -118,8 +119,13 @@ internal class DefaultWalletsRepository( } } - override suspend fun createWallet(userWalletId: UserWalletId) { - walletServerBinder.bind(userWalletId) + override suspend fun createWallet(userWalletId: UserWalletId): WalletSyncResult { + val response = walletServerBinder.bind(userWalletId) ?: return WalletSyncResult.AlreadyExists + return if (response is ApiResponse.Success && response.code == HttpException.Code.CREATED) { + WalletSyncResult.Created + } else { + WalletSyncResult.AlreadyExists + } } override fun nftEnabledStatus(userWalletId: UserWalletId): Flow = appPreferencesStore diff --git a/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/WalletSyncResult.kt b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/WalletSyncResult.kt new file mode 100644 index 0000000000..48c7caae72 --- /dev/null +++ b/domain/wallets/models/src/main/java/com/tangem/domain/wallets/models/WalletSyncResult.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.wallets.models + +enum class WalletSyncResult { + AlreadyExists, + Created, +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 2b25388b1e..33686eea83 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -4,6 +4,7 @@ import arrow.core.Either import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.wallets.models.UserWalletRemoteInfo +import com.tangem.domain.wallets.models.WalletSyncResult import com.tangem.domain.wallets.models.errors.ActivatePromoCodeError import kotlinx.coroutines.flow.Flow @@ -22,7 +23,7 @@ interface WalletsRepository { suspend fun setHasWalletsWithRing(userWalletId: UserWalletId) - suspend fun createWallet(userWalletId: UserWalletId) + suspend fun createWallet(userWalletId: UserWalletId): WalletSyncResult fun nftEnabledStatus(userWalletId: UserWalletId): Flow diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt index 063dd3380d..82ebc8e397 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SyncWalletWithRemoteUseCase.kt @@ -1,6 +1,7 @@ package com.tangem.domain.wallets.usecase import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.wallets.models.WalletSyncResult import com.tangem.domain.wallets.repository.WalletsRepository import com.tangem.utils.coroutines.runSuspendCatching import com.tangem.utils.logging.TangemLogger @@ -14,8 +15,9 @@ class SyncWalletWithRemoteUseCase( private val walletsRepository: WalletsRepository, ) { - suspend operator fun invoke(userWalletId: UserWalletId) { - runSuspendCatching { walletsRepository.createWallet(userWalletId) } + suspend operator fun invoke(userWalletId: UserWalletId): WalletSyncResult { + return runSuspendCatching { walletsRepository.createWallet(userWalletId) } .onFailure { TangemLogger.e("Error", it) } + .getOrDefault(WalletSyncResult.AlreadyExists) } } \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt index ce2c9cb8f9..6c6efa8ece 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/model/AddExistingWalletImportModel.kt @@ -19,7 +19,9 @@ import com.tangem.datasource.local.appsflyer.AppsFlyerStore import com.tangem.domain.common.wallets.error.SaveWalletError import com.tangem.domain.assetsdiscovery.usecase.StartAssetsDiscoveryUseCase import com.tangem.domain.wallets.builder.HotUserWalletBuilder +import com.tangem.domain.wallets.models.WalletSyncResult import com.tangem.domain.wallets.usecase.SaveWalletUseCase +import com.tangem.domain.wallets.usecase.SyncWalletWithRemoteUseCase import com.tangem.features.hotwallet.HotWalletFeatureToggles import com.tangem.features.hotwallet.MnemonicRepository import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent @@ -27,6 +29,7 @@ import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistin import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.model.HotAuth import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update @@ -42,6 +45,7 @@ internal class AddExistingWalletImportModel @Inject constructor( private val tangemHotSdk: TangemHotSdk, private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory, private val saveUserWalletUseCase: SaveWalletUseCase, + private val syncWalletWithRemoteUseCase: SyncWalletWithRemoteUseCase, private val startAssetsDiscoveryUseCase: StartAssetsDiscoveryUseCase, private val hotWalletFeatureToggles: HotWalletFeatureToggles, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, @@ -114,8 +118,13 @@ internal class AddExistingWalletImportModel @Inject constructor( .onRight { setImportProgress(false) - if (hotWalletFeatureToggles.isAssetsDiscoveryEnabled) { - startAssetsDiscoveryUseCase(userWallet.walletId) + launch(dispatchers.main + NonCancellable) { + val syncResult = syncWalletWithRemoteUseCase(userWallet.walletId) + if (hotWalletFeatureToggles.isAssetsDiscoveryEnabled && + syncResult == WalletSyncResult.Created + ) { + startAssetsDiscoveryUseCase(userWallet.walletId) + } } analyticsEventHandler.send( From 28ed702a51d129bd18c1400739f5e2c58e4583c5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Apr 2026 15:39:21 +0400 Subject: [PATCH 27/32] Updated on 2026-08-14 --- .../tangem/tap/routing/utils/ChildFactory.kt | 1 + .../com/tangem/common/routing/AppRoute.kt | 1 + .../configs/feature_toggles_config.json | 4 + .../component/ManageTokensSource.kt | 1 + .../featuretoggles/WalletFeatureToggles.kt | 2 + features/wallet/impl/build.gradle.kts | 1 + .../AddAndManageBottomSheetComponent.kt | 67 ++++++++ .../managetokens/di/AddAndManageModule.kt | 20 +++ .../managetokens/model/AddAndManageModel.kt | 83 +++++++++ .../ui/AddAndManageBottomSheetContent.kt | 159 ++++++++++++++++++ .../wallet/child/wallet/WalletComponent.kt | 22 +++ .../intents/WalletContentClickIntents.kt | 9 +- .../DefaultWalletFeatureToggles.kt | 3 + .../preview/WalletScreenPreviewDataLegacy.kt | 6 + .../router/DefaultWalletRouter.kt | 11 +- .../presentation/router/InnerWalletRouter.kt | 9 +- .../wallet/state/model/WalletDialogConfig.kt | 3 + .../state/model/WalletTokensListState.kt | 7 +- .../transformers/SetTokenListTransformer.kt | 3 + .../converter/TokenListStateConverter.kt | 17 ++ .../converter/WalletTokensListUMConverter.kt | 15 +- .../subscribers/AccountListSubscriber.kt | 5 + .../subscribers/BasicAccountListSubscriber.kt | 3 + .../subscribers/SingleWalletSubscriber.kt | 5 + .../SingleWalletWithTokenSubscriberLegacy.kt | 5 + .../presentation/wallet/ui/WalletScreen.kt | 18 +- .../MultiCurrencyOrganizeButton.kt | 13 +- 27 files changed, 468 insertions(+), 25 deletions(-) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/di/AddAndManageModule.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModel.kt create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index a200fea92f..afff976cd4 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 @@ -138,6 +138,7 @@ internal class ChildFactory @Inject constructor( AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT + AppRoute.ManageTokens.Source.WALLET -> ManageTokensSource.WALLET } val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None 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 ea5ec5fdcc..6fba5a36d5 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 @@ -146,6 +146,7 @@ sealed class AppRoute(val path: String) : Route { STORIES, SETTINGS, ACCOUNT, + WALLET, } } diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 0154a6d8a0..5efc71652f 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 @@ -79,5 +79,9 @@ { "name": "SOLANA_TX_HISTORY_ENABLED", "version": "undefined" + }, + { + "name": "ADD_AND_MANAGE_TOKENS_ENABLED", + "version": "undefined" } ] diff --git a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt index fcce6daea7..dc51c35411 100644 --- a/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt +++ b/features/manage-tokens/api/src/main/kotlin/com/tangem/features/managetokens/component/ManageTokensSource.kt @@ -9,6 +9,7 @@ enum class ManageTokensSource(val analyticsName: String) { ONBOARDING(analyticsName = "Onboarding"), SETTINGS(analyticsName = "Wallet Settings"), ACCOUNT(analyticsName = "Account"), + WALLET(analyticsName = "Wallet"), SEND_VIA_SWAP(analyticsName = "SendViaSwap"), } diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt index 1bb167f64c..1b13fbb3ad 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -10,4 +10,6 @@ interface WalletFeatureToggles { val isWalletReorderFeatureEnabled: Boolean val isMainScreenQrScanningEnabled: Boolean + + val isAddAndManageTokensEnabled: Boolean } \ No newline at end of file diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 70e66ddf64..c91c083d7d 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -128,6 +128,7 @@ dependencies { implementation(projects.domain.assetsdiscovery) /** Feature Apis */ + implementation(projects.features.account.api) implementation(projects.features.details.api) implementation(projects.features.hotWallet.api) implementation(projects.features.manageTokens.api) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt new file mode 100644 index 0000000000..3cd335e5c5 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/AddAndManageBottomSheetComponent.kt @@ -0,0 +1,67 @@ +package com.tangem.feature.wallet.child.managetokens + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import com.arkivanov.decompose.ComponentContext +import com.arkivanov.decompose.extensions.compose.subscribeAsState +import com.arkivanov.decompose.router.slot.childSlot +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.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.managetokens.model.AddAndManageModel +import com.tangem.feature.wallet.child.managetokens.ui.AddAndManageBottomSheetContent +import com.tangem.features.account.PortfolioSelectorComponent +import kotlinx.serialization.builtins.serializer + +internal class AddAndManageBottomSheetComponent( + appComponentContext: AppComponentContext, + private val params: Params, + private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, +) : ComposableBottomSheetComponent, AppComponentContext by appComponentContext { + + private val model: AddAndManageModel = getOrCreateModel(params) + + private val portfolioSelectorSlot = childSlot( + source = model.portfolioSelectorNavigation, + serializer = Unit.serializer(), + handleBackButton = false, + childFactory = { _, context -> portfolioSelectorChild(context) }, + ) + + private fun portfolioSelectorChild(componentContext: ComponentContext): ComposableBottomSheetComponent = + portfolioSelectorComponentFactory.create( + context = childByContext(componentContext), + params = PortfolioSelectorComponent.Params( + portfolioFetcher = model.portfolioFetcher, + controller = model.portfolioSelectorController, + bsCallback = model.portfolioSelectorCallback, + ), + ) + + override fun dismiss() { + params.onDismiss() + } + + @Composable + override fun BottomSheet() { + val portfolioSelectorSlot by portfolioSelectorSlot.subscribeAsState() + + AddAndManageBottomSheetContent( + onAddTokensClick = model::onAddTokensClick, + onOrganizeTokensClick = model::onOrganizeTokensClick, + onDismiss = ::dismiss, + ) + + portfolioSelectorSlot.child?.instance?.BottomSheet() + } + + data class Params( + val userWalletId: UserWalletId, + val onDismiss: () -> Unit, + val onOrganizeTokensClick: () -> Unit, + val onManageTokensClick: (AccountId) -> Unit, + ) +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/di/AddAndManageModule.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/di/AddAndManageModule.kt new file mode 100644 index 0000000000..5ce1fdfab7 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/di/AddAndManageModule.kt @@ -0,0 +1,20 @@ +package com.tangem.feature.wallet.child.managetokens.di + +import com.tangem.core.decompose.di.ModelComponent +import com.tangem.core.decompose.model.Model +import com.tangem.feature.wallet.child.managetokens.model.AddAndManageModel +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.multibindings.ClassKey +import dagger.multibindings.IntoMap + +@Module +@InstallIn(ModelComponent::class) +internal interface AddAndManageModule { + + @Binds + @IntoMap + @ClassKey(AddAndManageModel::class) + fun bindAddAndManageModel(model: AddAndManageModel): Model +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModel.kt new file mode 100644 index 0000000000..13df43dd76 --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModel.kt @@ -0,0 +1,83 @@ +package com.tangem.feature.wallet.child.managetokens.model + +import com.arkivanov.decompose.router.slot.SlotNavigation +import com.arkivanov.decompose.router.slot.activate +import com.arkivanov.decompose.router.slot.dismiss +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.models.account.AccountId +import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent +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.flow.first +import kotlinx.coroutines.launch +import javax.inject.Inject + +@ModelScoped +internal class AddAndManageModel @Inject constructor( + paramsContainer: ParamsContainer, + override val dispatchers: CoroutineDispatcherProvider, + private val portfolioFetcherFactory: PortfolioFetcher.Factory, + val portfolioSelectorController: PortfolioSelectorController, +) : Model() { + + private val params = paramsContainer.require() + + val portfolioSelectorNavigation: SlotNavigation = SlotNavigation() + + val portfolioFetcher: PortfolioFetcher by lazy { + portfolioFetcherFactory.create( + mode = PortfolioFetcher.Mode.Wallet(params.userWalletId), + scope = modelScope, + ) + } + + val portfolioSelectorCallback = object : PortfolioSelectorComponent.BottomSheetCallback { + override val onDismiss: () -> Unit = { portfolioSelectorNavigation.dismiss() } + override val onBack: () -> Unit = { portfolioSelectorNavigation.dismiss() } + } + + init { + observeAccountSelection() + } + + fun onAddTokensClick() { + modelScope.launch { + val data = portfolioFetcher.data.first() + val isSingleAccount = data.isSingleChoice(params.userWalletId) + + if (isSingleAccount) { + val mainAccountId = data.balances[params.userWalletId] + ?.accountsBalance + ?.mainAccount + ?.accountId + ?: AccountId.forMainCryptoPortfolio(params.userWalletId) + + params.onDismiss() + params.onManageTokensClick(mainAccountId) + } else { + portfolioSelectorNavigation.activate(Unit) + } + } + } + + fun onOrganizeTokensClick() { + params.onDismiss() + params.onOrganizeTokensClick() + } + + private fun observeAccountSelection() { + modelScope.launch { + portfolioSelectorController.selectedAccount.collect { accountId -> + if (accountId != null) { + portfolioSelectorNavigation.dismiss() + params.onDismiss() + params.onManageTokensClick(accountId) + } + } + } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt new file mode 100644 index 0000000000..5534c0cb4f --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/ui/AddAndManageBottomSheetContent.kt @@ -0,0 +1,159 @@ +package com.tangem.feature.wallet.child.managetokens.ui + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +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.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.res.R as ResR +import com.tangem.core.ui.R +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.decorations.roundedShapeItemDecoration +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 + +@Composable +internal fun AddAndManageBottomSheetContent( + onAddTokensClick: () -> Unit, + onOrganizeTokensClick: () -> Unit, + onDismiss: () -> Unit, +) { + val config = TangemBottomSheetConfig( + isShown = true, + onDismissRequest = onDismiss, + content = AddAndManageBottomSheetConfigContent, + ) + + TangemModalBottomSheet( + config = config, + containerColor = TangemTheme.colors.background.primary, + title = { + TangemModalBottomSheetTitle( + title = resourceReference(ResR.string.main_add_and_manage_tokens), + endIconRes = R.drawable.ic_close_24, + onEndClick = onDismiss, + ) + }, + content = { + AddAndManageContent( + onAddTokensClick = onAddTokensClick, + onOrganizeTokensClick = onOrganizeTokensClick, + ) + }, + ) +} + +@Composable +private fun AddAndManageContent(onAddTokensClick: () -> Unit, onOrganizeTokensClick: () -> Unit) { + Column( + modifier = Modifier.padding( + start = 16.dp, + end = 16.dp, + bottom = 16.dp, + ), + ) { + AddAndManageRow( + iconRes = R.drawable.ic_plus_24, + title = ResR.string.add_and_manage_sheet_manage_title, + subtitle = ResR.string.add_and_manage_sheet_manage_subtitle, + onClick = onAddTokensClick, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = 0, + lastIndex = 1, + addDefaultPadding = false, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + AddAndManageRow( + iconRes = R.drawable.ic_filter_default_24, + title = ResR.string.add_and_manage_sheet_organize_title, + subtitle = ResR.string.add_and_manage_sheet_organize_subtitle, + onClick = onOrganizeTokensClick, + modifier = Modifier.roundedShapeItemDecoration( + currentIndex = 1, + lastIndex = 1, + addDefaultPadding = false, + backgroundColor = TangemTheme.colors.background.action, + ), + ) + } +} + +@Composable +private fun AddAndManageRow( + iconRes: Int, + title: Int, + subtitle: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 15.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f)), + ) { + Icon( + modifier = Modifier.size(18.dp), + painter = rememberVectorPainter(ImageVector.vectorResource(id = iconRes)), + tint = TangemTheme.colors.icon.accent, + contentDescription = null, + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = stringResourceSafe(id = title), + style = TangemTheme.typography.subtitle2, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = stringResourceSafe(id = subtitle), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + ) + } + } +} + +private object AddAndManageBottomSheetConfigContent : TangemBottomSheetConfigContent + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun AddAndManageBottomSheetContent_Preview() { + TangemThemePreview { + AddAndManageContent( + onAddTokensClick = {}, + onOrganizeTokensClick = {}, + ) + } +} +// endregion \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/WalletComponent.kt index f15fbdfb75..72e1976879 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 @@ -23,7 +23,9 @@ import com.tangem.core.ui.decompose.ComposableContentComponent import com.tangem.core.ui.decompose.ComposableDialogComponent import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.details.TokenAction +import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent +import com.tangem.features.account.PortfolioSelectorComponent import com.tangem.feature.wallet.child.tokenActions.DefaultTokenActionsComponent import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent import com.tangem.feature.wallet.child.wallet.model.WalletModel @@ -64,6 +66,7 @@ internal class WalletComponent @AssistedInject constructor( private val newPromoBannersFeatureToggles: NewPromoBannersFeatureToggles, private val networkSelectionComponentFactory: NetworkSelectionComponent.Factory, private val tokenActionsComponentFactory: TokenActionsComponent.Factory, + private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, private val designFeatureToggles: DesignFeatureToggles, ) : ComposableContentComponent, AppComponentContext by appComponentContext { @@ -178,6 +181,25 @@ internal class WalletComponent @AssistedInject constructor( ), ) } + is WalletDialogConfig.AddAndManage -> { + AddAndManageBottomSheetComponent( + appComponentContext = childByContext(componentContext), + params = AddAndManageBottomSheetComponent.Params( + userWalletId = dialogConfig.userWalletId, + onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + onOrganizeTokensClick = { + model.innerWalletRouter.openOrganizeTokensScreen(dialogConfig.userWalletId) + }, + onManageTokensClick = { accountId -> + model.innerWalletRouter.openManageTokensScreen( + accountId = accountId, + source = AppRoute.ManageTokens.Source.WALLET, + ) + }, + ), + portfolioSelectorComponentFactory = portfolioSelectorComponentFactory, + ) + } is WalletDialogConfig.OrganizeTokens -> { OrganizeTokensComponent( appComponentContext = childByContext(componentContext), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt index 8f880da28c..71220bf8a9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntents.kt @@ -36,6 +36,7 @@ import com.tangem.feature.wallet.presentation.wallet.state.transformers.CloseBot import com.tangem.feature.wallet.presentation.wallet.state.transformers.OpenBottomSheetTransformer import com.tangem.feature.wallet.presentation.wallet.state.transformers.converter.MultiWalletCurrencyActionsConverter import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.collectLatest @@ -112,6 +113,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( private val yieldSupplySetShouldShowMainPromoUseCase: YieldSupplySetShouldShowMainPromoUseCase, private val tokenListAnalyticsSender: TokenListAnalyticsSender, private val uiMessageSender: UiMessageSender, + private val walletFeatureToggles: WalletFeatureToggles, ) : BaseWalletClickIntents(), WalletContentClickIntents { override fun onDetailsClick() { @@ -119,7 +121,12 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( } override fun onOrganizeTokensClick() { - router.openOrganizeTokensScreen(userWalletId = stateHolder.getSelectedWalletId()) + val userWalletId = stateHolder.getSelectedWalletId() + if (walletFeatureToggles.isAddAndManageTokensEnabled) { + router.openAddAndManageBottomSheet(userWalletId = userWalletId) + } else { + router.openOrganizeTokensScreen(userWalletId = userWalletId) + } } override fun onDismissMarketsTooltip() { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt index d1ee8ec418..0120f51987 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -14,4 +14,7 @@ internal class DefaultWalletFeatureToggles @Inject constructor( override val isMainScreenQrScanningEnabled: Boolean get() = featureToggles.isFeatureEnabled(FeatureToggles.MAIN_SCREEN_QR_SCANNING_ENABLED) + + override val isAddAndManageTokensEnabled: Boolean + get() = featureToggles.isFeatureEnabled(FeatureToggles.ADD_AND_MANAGE_TOKENS_ENABLED) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt index c52c371251..bbdd063032 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/preview/WalletScreenPreviewDataLegacy.kt @@ -91,6 +91,8 @@ internal object WalletScreenPreviewDataLegacy { ), ), organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + textRes = R.string.organize_tokens_title, + iconRes = R.drawable.ic_filter_24, isEnabled = true, onClick = {}, ), @@ -119,6 +121,8 @@ internal object WalletScreenPreviewDataLegacy { ), ), organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + textRes = R.string.organize_tokens_title, + iconRes = R.drawable.ic_filter_24, isEnabled = true, onClick = {}, ), @@ -149,6 +153,8 @@ internal object WalletScreenPreviewDataLegacy { ), ), organizeTokensButtonConfig = WalletTokensListState.OrganizeTokensButtonConfig( + textRes = R.string.organize_tokens_title, + iconRes = R.drawable.ic_filter_24, isEnabled = true, onClick = {}, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 1eadd2b07b..962bd6549c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -32,6 +32,7 @@ import javax.inject.Inject /** Default implementation of wallet feature router */ @ModelScoped +@Suppress("TooManyFunctions") internal class DefaultWalletRouter @Inject constructor( private val router: AppRouter, private val urlOpener: UrlOpener, @@ -66,14 +67,20 @@ internal class DefaultWalletRouter @Inject constructor( ) } - override fun openManageTokensScreen(accountId: AccountId) { + override fun openManageTokensScreen(accountId: AccountId, source: AppRoute.ManageTokens.Source) { val route = AppRoute.ManageTokens( - source = AppRoute.ManageTokens.Source.ACCOUNT, + source = source, accountId = accountId, ) router.push(route) } + override fun openAddAndManageBottomSheet(userWalletId: UserWalletId) { + dialogNavigation.activate( + configuration = WalletDialogConfig.AddAndManage(userWalletId = userWalletId), + ) + } + override fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean) { router.push( AppRoute.Onboarding( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 7d772e539c..597093c071 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.SharedFlow [REDACTED_AUTHOR] */ @Stable +@Suppress("TooManyFunctions") internal interface InnerWalletRouter { val dialogNavigation: SlotNavigation @@ -47,7 +48,13 @@ internal interface InnerWalletRouter { fun openDetailsScreen(selectedWalletId: UserWalletId) /** Open manage tokens screen */ - fun openManageTokensScreen(accountId: AccountId) + fun openManageTokensScreen( + accountId: AccountId, + source: AppRoute.ManageTokens.Source = AppRoute.ManageTokens.Source.ACCOUNT, + ) + + /** Open add and manage tokens bottom sheet */ + fun openAddAndManageBottomSheet(userWalletId: UserWalletId) /** Open onboarding screen */ fun openOnboardingScreen(scanResponse: ScanResponse, continueBackup: Boolean = false) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index 8322d1c8df..52cb3834c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -50,6 +50,9 @@ internal sealed interface WalletDialogConfig { @Serializable data class KycRejected(val walletId: UserWalletId, val customerId: String) : WalletDialogConfig + @Serializable + data class AddAndManage(val userWalletId: UserWalletId) : WalletDialogConfig + @Serializable data class OrganizeTokens(val userWalletId: UserWalletId) : WalletDialogConfig diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt index 82050bd5fb..69cb21f25d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletTokensListState.kt @@ -49,5 +49,10 @@ internal sealed class WalletTokensListState { } } - data class OrganizeTokensButtonConfig(val isEnabled: Boolean, val onClick: () -> Unit) + data class OrganizeTokensButtonConfig( + val textRes: Int, + val iconRes: Int, + val isEnabled: Boolean, + val onClick: () -> Unit, + ) } \ 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 f3d9013ff0..ef84cc13c6 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 @@ -25,6 +25,7 @@ internal class SetTokenListTransformer( private val shouldShowMainPromo: Boolean, private val isAccountsModeEnabled: Boolean, private val isRedesignEnabled: Boolean, + private val isAddAndManageTokensEnabled: Boolean, ) : WalletStateTransformer(userWallet.walletId) { private val tangemPayConverter by lazy { @@ -105,6 +106,7 @@ internal class SetTokenListTransformer( yieldModuleApyMap = yieldSupplyApyMap, stakingAvailabilityMap = stakingAvailabilityMap, shouldShowMainPromo = shouldShowMainPromo, + isAddAndManageTokensEnabled = isAddAndManageTokensEnabled, ).convert(value = this) } @@ -137,6 +139,7 @@ internal class SetTokenListTransformer( shouldShowMainPromo = shouldShowMainPromo, isAccountsModeEnabled = isAccountsModeEnabled, expandedAccounts = params.expandedAccounts, + isAddAndManageTokensEnabled = isAddAndManageTokensEnabled, ).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/TokenListStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt index 926fdead7a..8d23a44a9c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/TokenListStateConverter.kt @@ -41,6 +41,7 @@ internal class TokenListStateConverter( private val yieldModuleApyMap: Map, private val stakingAvailabilityMap: Map, private val shouldShowMainPromo: Boolean, + private val isAddAndManageTokensEnabled: Boolean, ) : Converter { private val yieldSupplyPromoBannerConverter = YieldSupplyPromoBannerConverter( @@ -157,6 +158,8 @@ internal class TokenListStateConverter( } return if (currenciesSize > 1 && !isSingleCurrencyWalletWithToken()) { WalletOrganizeTokensButtonConfig( + textRes = organizeButtonTextRes(), + iconRes = organizeButtonIconRes(), isEnabled = tokenList.totalFiatBalance !is TotalFiatBalance.Loading, onClick = clickIntents::onOrganizeTokensClick, ) @@ -168,6 +171,8 @@ internal class TokenListStateConverter( private fun getOrganizeTokensButtonStateV2(accountList: AccountStatusList): WalletOrganizeTokensButtonConfig? { return if (accountList.flattenCurrencies().size > 1 && !isSingleCurrencyWalletWithToken()) { WalletOrganizeTokensButtonConfig( + textRes = organizeButtonTextRes(), + iconRes = organizeButtonIconRes(), isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, onClick = clickIntents::onOrganizeTokensClick, ) @@ -176,6 +181,18 @@ internal class TokenListStateConverter( } } + private fun organizeButtonTextRes(): Int = if (isAddAndManageTokensEnabled) { + R.string.main_add_and_manage_tokens + } else { + R.string.organize_tokens_title + } + + private fun organizeButtonIconRes(): Int = if (isAddAndManageTokensEnabled) { + R.drawable.ic_filter_default_24 + } else { + R.drawable.ic_filter_24 + } + private fun isSingleCurrencyWalletWithToken(): Boolean { return selectedWallet is UserWallet.Cold && selectedWallet.scanResponse.cardTypesResolver.isSingleWalletWithToken() diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt index c252326e14..a5535584a4 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/WalletTokensListUMConverter.kt @@ -38,6 +38,7 @@ internal class WalletTokensListUMConverter( private val isAccountsModeEnabled: Boolean, private val expandedAccounts: Set, private val stakingAvailabilityMap: Map, + private val isAddAndManageTokensEnabled: Boolean, shouldShowMainPromo: Boolean, ) : Converter { @@ -164,15 +165,25 @@ internal class WalletTokensListUMConverter( } private fun getOrganizeButtonUM(accountList: AccountStatusList): TangemButtonUM? { + val textRes = if (isAddAndManageTokensEnabled) { + R.string.main_add_and_manage_tokens + } else { + R.string.organize_tokens_title + } + val iconRes = if (isAddAndManageTokensEnabled) { + R.drawable.ic_filter_default_24 + } else { + R.drawable.ic_filter_24 + } return if (accountList.flattenCurrencies().size > 1 && !selectedWallet.isSingleWalletWithToken()) { TangemButtonUM( - text = resourceReference(R.string.organize_tokens_title), + text = resourceReference(textRes), isEnabled = accountList.totalFiatBalance !is TotalFiatBalance.Loading, size = TangemButtonSize.X9, shape = TangemButtonShape.Rounded, type = TangemButtonType.PrimaryInverse, tangemIconUM = TangemIconUM.Icon( - iconRes = R.drawable.ic_filter_default_24, + iconRes = iconRes, tintReference = { if (accountList.totalFiatBalance !is TotalFiatBalance.Loading) { TangemTheme.colors2.graphic.neutral.primary 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 2d7ba38899..a440c34a7f 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 @@ -12,6 +12,7 @@ import com.tangem.domain.yield.supply.usecase.YieldSupplyGetShouldShowMainPromoU 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 com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.utils.coroutines.combine7 import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -39,8 +40,12 @@ internal class AccountListSubscriber @AssistedInject constructor( private val stakingAvailabilityListUseCase: StakingAvailabilityListUseCase, private val yieldSupplyGetShouldShowMainPromoUseCase: YieldSupplyGetShouldShowMainPromoUseCase, private val designFeatureToggles: DesignFeatureToggles, + private val walletFeatureToggles: WalletFeatureToggles, ) : BasicAccountListSubscriber() { + override val isAddAndManageTokensEnabled: Boolean + get() = walletFeatureToggles.isAddAndManageTokensEnabled + override fun create(coroutineScope: CoroutineScope): Flow<*> = combine7( flow1 = getAccountStatusListFlow(), flow2 = getAppCurrencyFlow(), 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 6192849dc4..047b5d765d 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 @@ -34,6 +34,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { abstract val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase abstract val stateController: WalletStateController abstract val clickIntents: WalletClickIntents + abstract val isAddAndManageTokensEnabled: Boolean override val singleAccountStatusListSupplier: SingleAccountStatusListSupplier get() = accountDependencies.singleAccountStatusListSupplier @@ -105,6 +106,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { shouldShowMainPromo = shouldShowMainPromo, isAccountsModeEnabled = isAccountMode, isRedesignEnabled = true, + isAddAndManageTokensEnabled = isAddAndManageTokensEnabled, ), ) } @@ -168,6 +170,7 @@ internal abstract class BasicAccountListSubscriber : BasicWalletSubscriber() { shouldShowMainPromo = shouldShowMainPromo, isAccountsModeEnabled = false, isRedesignEnabled = false, + isAddAndManageTokensEnabled = isAddAndManageTokensEnabled, ), ) } 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 index 88e0697c7e..ca409e79c6 100644 --- 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 @@ -5,6 +5,7 @@ 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 com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -18,8 +19,12 @@ internal class SingleWalletSubscriber @AssistedInject constructor( override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, override val stateController: WalletStateController, override val clickIntents: WalletClickIntents, + private val walletFeatureToggles: WalletFeatureToggles, ) : BasicAccountListSubscriber() { + override val isAddAndManageTokensEnabled: Boolean + get() = walletFeatureToggles.isAddAndManageTokensEnabled + override fun create(coroutineScope: CoroutineScope): Flow = combine( flow = getAccountStatusListFlow(), flow2 = getAppCurrencyFlow(), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt index 1a31f1f1b4..0ad3c442ba 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/SingleWalletWithTokenSubscriberLegacy.kt @@ -5,6 +5,7 @@ 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 com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -19,8 +20,12 @@ internal class SingleWalletWithTokenSubscriberLegacy @AssistedInject constructor override val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, override val stateController: WalletStateController, override val clickIntents: WalletClickIntents, + private val walletFeatureToggles: WalletFeatureToggles, ) : BasicAccountListSubscriber() { + override val isAddAndManageTokensEnabled: Boolean + get() = walletFeatureToggles.isAddAndManageTokensEnabled + override fun create(coroutineScope: CoroutineScope): Flow = combine( flow = getAccountStatusListFlow(), flow2 = getAppCurrencyFlow(), 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 8f842b82cf..1dcf79012d 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 @@ -726,17 +726,13 @@ private fun WalletSnackbarHost( } internal fun LazyListScope.organizeTokens(state: WalletState, itemModifier: Modifier) { - (state as? WalletState.MultiCurrency)?.let { - (state.tokensListState as? WalletTokensListState.ContentState)?.let { - it.organizeTokensButtonConfig?.let { config -> - organizeTokensButton( - modifier = itemModifier, - isEnabled = config.isEnabled, - onClick = config.onClick, - ) - } - } - } + val multiCurrencyState = state as? WalletState.MultiCurrency ?: return + val contentState = multiCurrencyState.tokensListState as? WalletTokensListState.ContentState ?: return + val config = contentState.organizeTokensButtonConfig ?: return + organizeTokensButton( + modifier = itemModifier, + config = config, + ) } internal fun LazyListScope.nftCollections(state: WalletState, itemModifier: Modifier) { diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt index 94592fc246..6e5177817c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/multicurrency/MultiCurrencyOrganizeButton.kt @@ -7,7 +7,7 @@ import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.buttons.actions.RoundedActionButton import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.test.MainScreenTestTags -import com.tangem.feature.wallet.impl.R +import com.tangem.feature.wallet.presentation.wallet.state.model.WalletTokensListState private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton" @@ -20,18 +20,17 @@ private const val ORGANIZE_BUTTON_CONTENT_TYPE = "OrganizeTokensButton" [REDACTED_AUTHOR] */ internal fun LazyListScope.organizeTokensButton( - isEnabled: Boolean, - onClick: () -> Unit, + config: WalletTokensListState.OrganizeTokensButtonConfig, modifier: Modifier = Modifier, ) { item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) { RoundedActionButton( modifier = modifier.testTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON), config = ActionButtonConfig( - text = resourceReference(id = R.string.organize_tokens_title), - iconResId = R.drawable.ic_filter_24, - onClick = onClick, - isEnabled = isEnabled, + text = resourceReference(id = config.textRes), + iconResId = config.iconRes, + onClick = config.onClick, + isEnabled = config.isEnabled, ), ) } From 27e9400b2aa2a85e21cb32148181fd127a622916 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Apr 2026 15:39:33 +0400 Subject: [PATCH 28/32] Updated on 2026-08-14 --- .../feedback/DefaultFeedbackRepository.kt | 29 +++++++------------ .../tangem/data/feedback/di/FeedbackModule.kt | 3 -- .../data/staking/DefaultStakeKitRepository.kt | 12 +++++--- .../DefaultTransactionRepository.kt | 10 +++---- .../feedback/models/BlockchainErrorInfo.kt | 8 ++--- .../feedback/repository/FeedbackRepository.kt | 7 ++--- .../utils/EmailMessageBodyResolver.kt | 20 +++++-------- ...GetConstructedStakingTransactionUseCase.kt | 5 ++-- .../repositories/StakeKitRepository.kt | 4 +-- .../v2/send/confirm/model/SendConfirmModel.kt | 5 ++-- .../features/send/v2/send/model/SendModel.kt | 7 ++--- .../confirm/model/NFTSendConfirmModel.kt | 9 +++--- .../send/v2/sendnft/model/NFTSendModel.kt | 5 ++-- .../impl/presentation/model/StakingModel.kt | 18 +++++------- .../helpers/StakeKitTransactionSender.kt | 4 +-- .../swap/v2/impl/common/SwapAlertFactory.kt | 3 +- .../tangem/feature/swap/model/SwapModel.kt | 3 +- .../impl/common/YieldSupplyAlertFactory.kt | 3 +- 18 files changed, 67 insertions(+), 88 deletions(-) diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt index 5f39f9f870..76ef2e52f0 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/DefaultFeedbackRepository.kt @@ -1,7 +1,7 @@ package com.tangem.data.feedback import android.os.Build -import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.core.navigation.email.EmailSender import com.tangem.data.feedback.converters.BlockchainInfoConverter import com.tangem.data.feedback.converters.WalletMetaInfoConverter @@ -10,10 +10,10 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.models.* import com.tangem.domain.feedback.repository.FeedbackRepository +import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.utils.logging.TangemLogger import com.tangem.utils.version.AppVersionProvider import kotlinx.coroutines.flow.MutableStateFlow @@ -23,23 +23,20 @@ import java.io.File /** * Implementation of [FeedbackRepository] * - * @property appLogsStore app logs store - * @property userWalletsListManager user wallets list manager - * @property userWalletsListRepository user wallets repository - * @property walletManagersStore wallet managers store - * @property emailSender email sender - * @property appVersionProvider app version provider + * @property appLogsStore app logs store + * @property userWalletsListRepository repository for getting user wallets + * @property walletManagersStore wallet managers store + * @property emailSender email sender + * @property appVersionProvider app version provider * [REDACTED_AUTHOR] */ -@Suppress("LongParameterList") internal class DefaultFeedbackRepository( private val appLogsStore: AppLogsStore, private val userWalletsListRepository: UserWalletsListRepository, private val walletManagersStore: WalletManagersStore, private val emailSender: EmailSender, private val appVersionProvider: AppVersionProvider, - private val getSelectedWalletUseCase: GetSelectedWalletUseCase, ) : FeedbackRepository { private val blockchainsErrors = MutableStateFlow>(emptyMap()) @@ -68,16 +65,12 @@ internal class DefaultFeedbackRepository( .map(BlockchainInfoConverter::convert) } - override suspend fun getBlockchainInfo( - userWalletId: UserWalletId, - blockchainId: String, - derivationPath: String?, - ): BlockchainInfo? { + override suspend fun getBlockchainInfo(userWalletId: UserWalletId, networkId: Network.ID): BlockchainInfo? { return walletManagersStore .getSyncOrNull( userWalletId = userWalletId, - blockchain = Blockchain.fromId(blockchainId), - derivationPath = derivationPath, + blockchain = networkId.toBlockchain(), + derivationPath = networkId.derivationPath.value, ) ?.let(BlockchainInfoConverter::convert) } @@ -91,7 +84,7 @@ internal class DefaultFeedbackRepository( } override fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) { - val userWallet = getSelectedWalletUseCase.sync().getOrNull() ?: error("UserWallet is not selected") + val userWallet = userWalletsListRepository.selectedUserWallet.value ?: error("UserWallet is not selected") blockchainsErrors.update { map -> map.toMutableMap().apply { diff --git a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt index db803a21d2..6d4d9176c1 100644 --- a/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt +++ b/data/feedback/src/main/java/com/tangem/data/feedback/di/FeedbackModule.kt @@ -9,7 +9,6 @@ import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.domain.feedback.repository.FeedbackFeatureToggles import com.tangem.domain.feedback.repository.FeedbackRepository -import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.utils.version.AppVersionProvider import dagger.Module import dagger.Provides @@ -29,7 +28,6 @@ internal object FeedbackModule { walletManagersStore: WalletManagersStore, emailSender: EmailSender, appVersionProvider: AppVersionProvider, - getSelectedWalletUseCase: GetSelectedWalletUseCase, ): FeedbackRepository { return DefaultFeedbackRepository( appLogsStore = appLogsStore, @@ -37,7 +35,6 @@ internal object FeedbackModule { emailSender = emailSender, appVersionProvider = appVersionProvider, userWalletsListRepository = userWalletsListRepository, - getSelectedWalletUseCase = getSelectedWalletUseCase, ) } 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 8f0fd77aec..335d1a5dac 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 @@ -8,6 +8,7 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.TransactionStatus import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.blockchainsdk.utils.toBlockchain import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.data.staking.converters.YieldConverter @@ -244,7 +245,7 @@ internal class DefaultStakeKitRepository( } override suspend fun constructTransaction( - networkId: String, + networkId: Network.RawID, fee: Fee, amount: Amount, transactionId: String, @@ -320,8 +321,11 @@ internal class DefaultStakeKitRepository( } } - private fun getTransactionDataType(networkId: String, unsignedTransaction: String): TransactionData.Compiled.Data { - return when (Blockchain.fromId(networkId)) { + private fun getTransactionDataType( + networkId: Network.RawID, + unsignedTransaction: String, + ): TransactionData.Compiled.Data { + return when (val blockchain = networkId.toBlockchain()) { Blockchain.Solana, Blockchain.Cosmos, -> TransactionData.Compiled.Data.Bytes(unsignedTransaction.hexToBytes()) @@ -335,7 +339,7 @@ internal class DefaultStakeKitRepository( ?: error("Failed to parse Tron StakeKit transaction") TransactionData.Compiled.Data.RawString(tronStakeKitTransaction.rawDataHex) } - else -> error("Unsupported blockchain") + else -> error("Unsupported blockchain: $blockchain") } } diff --git a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt index 17c0b17828..ae96776efa 100644 --- a/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt +++ b/data/transaction/src/main/java/com/tangem/data/transaction/DefaultTransactionRepository.kt @@ -61,7 +61,7 @@ internal class DefaultTransactionRepository( derivationPath = network.derivationPath.value, ) ?: error("Wallet manager not found") - val extras = txExtras ?: getMemoExtras(networkId = network.rawId, memo) + val extras = txExtras ?: getMemoExtras(networkId = network.id.rawId, memo) val patchedDestination = if (amount.type is AmountType.TokenYieldSupply) { walletManager.getYieldModuleAddress() @@ -128,7 +128,7 @@ internal class DefaultTransactionRepository( destination = destination, userWalletId = userWalletId, network = network, - txExtras = getMemoExtras(networkId = network.rawId, memo = memo) ?: extras, + txExtras = getMemoExtras(networkId = network.id.rawId, memo = memo) ?: extras, ) } @@ -262,7 +262,7 @@ internal class DefaultTransactionRepository( fee = fee ?: Fee.Common(amount = amount), destination = destination, ).copy( - extras = getMemoExtras(networkId = network.rawId, memo = memo), + extras = getMemoExtras(networkId = network.id.rawId, memo = memo), ) validator.validate(transactionData = transactionData) @@ -332,8 +332,8 @@ internal class DefaultTransactionRepository( } @Suppress("CyclomaticComplexMethod") - private fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { - val blockchain = Blockchain.fromId(networkId) + private fun getMemoExtras(networkId: Network.RawID, memo: String?): TransactionExtras? { + val blockchain = networkId.toBlockchain() if (memo == null) return null return when (blockchain) { Blockchain.Stellar -> { diff --git a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt index fc76ba451b..bede31c929 100644 --- a/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt +++ b/domain/feedback/models/src/main/kotlin/com/tangem/domain/feedback/models/BlockchainErrorInfo.kt @@ -1,11 +1,12 @@ package com.tangem.domain.feedback.models +import com.tangem.domain.models.network.Network + /** * Information about blockchain's operation error * * @property errorMessage message about error - * @property blockchainId blockchain id - * @property derivationPath derivation path + * @property networkId network ID * @property destinationAddress destination address * @property tokenSymbol token symbol or null, if it isn't operation with token * @property amount amount @@ -13,8 +14,7 @@ package com.tangem.domain.feedback.models */ data class BlockchainErrorInfo( val errorMessage: String, - val blockchainId: String, - val derivationPath: String?, + val networkId: Network.ID?, val destinationAddress: String, val tokenSymbol: String?, val amount: String, diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt index 3661f00b48..0f3bb871b5 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/repository/FeedbackRepository.kt @@ -1,6 +1,7 @@ package com.tangem.domain.feedback.repository import com.tangem.domain.feedback.models.* +import com.tangem.domain.models.network.Network import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.models.wallet.UserWalletId import java.io.File @@ -17,11 +18,7 @@ interface FeedbackRepository { fun getPhoneInfo(): PhoneInfo - suspend fun getBlockchainInfo( - userWalletId: UserWalletId, - blockchainId: String, - derivationPath: String?, - ): BlockchainInfo? + suspend fun getBlockchainInfo(userWalletId: UserWalletId, networkId: Network.ID): BlockchainInfo? fun saveBlockchainErrorInfo(error: BlockchainErrorInfo) diff --git a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt index 0d16f8c9c2..112112773f 100644 --- a/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt +++ b/domain/feedback/src/main/java/com/tangem/domain/feedback/utils/EmailMessageBodyResolver.kt @@ -94,11 +94,10 @@ class EmailMessageBodyResolver( val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) - val blockchainInfo = blockchainError?.let { + val blockchainInfo = blockchainError?.networkId?.let { networkId -> feedbackRepository.getBlockchainInfo( userWalletId = userWalletId, - blockchainId = blockchainError.blockchainId, - derivationPath = blockchainError.derivationPath, + networkId = networkId, ) } @@ -159,11 +158,10 @@ class EmailMessageBodyResolver( val userWalletId = requireNotNull(walletMetaInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) - val blockchainInfo = blockchainError?.let { + val blockchainInfo = blockchainError?.networkId?.let { networkId -> feedbackRepository.getBlockchainInfo( userWalletId = userWalletId, - blockchainId = blockchainError.blockchainId, - derivationPath = blockchainError.derivationPath, + networkId = networkId, ) } @@ -181,11 +179,10 @@ class EmailMessageBodyResolver( val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) - val blockchainInfo = blockchainError?.let { + val blockchainInfo = blockchainError?.networkId?.let { networkId -> feedbackRepository.getBlockchainInfo( userWalletId = userWalletId, - blockchainId = blockchainError.blockchainId, - derivationPath = blockchainError.derivationPath, + networkId = networkId, ) } @@ -210,11 +207,10 @@ class EmailMessageBodyResolver( val userWalletId = requireNotNull(type.walletMetaInfo.userWalletId) { "UserWalletId must be not null" } val blockchainError = feedbackRepository.getBlockchainErrorInfo(userWalletId = userWalletId) - val blockchainInfo = blockchainError?.let { + val blockchainInfo = blockchainError?.networkId?.let { networkId -> feedbackRepository.getBlockchainInfo( userWalletId = userWalletId, - blockchainId = blockchainError.blockchainId, - derivationPath = blockchainError.derivationPath, + networkId = networkId, ) } diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt b/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt index 7242d70f51..d9f06df359 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/GetConstructedStakingTransactionUseCase.kt @@ -4,10 +4,11 @@ import arrow.core.Either import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee +import com.tangem.domain.models.network.Network import com.tangem.domain.staking.model.stakekit.StakingError import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction -import com.tangem.domain.staking.repositories.StakingErrorResolver import com.tangem.domain.staking.repositories.StakeKitRepository +import com.tangem.domain.staking.repositories.StakingErrorResolver class GetConstructedStakingTransactionUseCase( private val stakeKitRepository: StakeKitRepository, @@ -15,7 +16,7 @@ class GetConstructedStakingTransactionUseCase( ) { suspend operator fun invoke( - networkId: String, + networkId: Network.RawID, fee: Fee, amount: Amount, transactionId: String, diff --git a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakeKitRepository.kt b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakeKitRepository.kt index e614a685bc..876e026c8b 100644 --- a/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakeKitRepository.kt +++ b/domain/staking/src/main/java/com/tangem/domain/staking/repositories/StakeKitRepository.kt @@ -5,11 +5,11 @@ import com.tangem.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network +import com.tangem.domain.models.staking.NetworkType import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.StakingIntegrationID -import com.tangem.domain.models.staking.NetworkType import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus @@ -55,7 +55,7 @@ interface StakeKitRepository { suspend fun estimateGas(userWalletId: UserWalletId, network: Network, params: ActionParams): StakingGasEstimate suspend fun constructTransaction( - networkId: String, + networkId: Network.RawID, fee: Fee, amount: Amount, transactionId: String, 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 4c9a5154ca..e201062981 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 @@ -74,10 +74,10 @@ import com.tangem.features.send.v2.subcomponents.amount.SendAmountReduceTrigger import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.orZero import com.tangem.utils.extensions.stripZeroPlainString +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import javax.inject.Inject import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @@ -281,8 +281,7 @@ internal class SendConfirmModel @Inject constructor( saveBlockchainErrorUseCase( error = BlockchainErrorInfo( errorMessage = errorMessage, - blockchainId = cryptoCurrency.network.rawId, - derivationPath = cryptoCurrency.network.derivationPath.value, + networkId = cryptoCurrency.network.id, destinationAddress = confirmData.enteredDestination.orEmpty(), tokenSymbol = if (amount?.type is AmountType.Token) { amount.currencySymbol diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt index de06ea2a08..3a0d2b40c8 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/send/model/SendModel.kt @@ -18,6 +18,7 @@ import com.tangem.core.decompose.navigation.Router import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.core.ui.utils.parseBigDecimalOrNull import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -33,7 +34,6 @@ import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.qrscanning.models.SourceType import com.tangem.domain.qrscanning.usecases.ListenToQrScanningUseCase import com.tangem.domain.qrscanning.usecases.ParseQrCodeUseCase -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.models.TransactionFeeExtended import com.tangem.domain.transaction.usecase.CreateTransferTransactionUseCase @@ -65,9 +65,9 @@ import com.tangem.features.send.v2.subcomponents.destination.model.transformers. import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import javax.inject.Inject import kotlin.properties.Delegates @@ -528,8 +528,7 @@ internal class SendModel @Inject constructor( saveBlockchainErrorUseCase( error = BlockchainErrorInfo( errorMessage = errorMessage.orEmpty(), - blockchainId = cryptoCurrency.network.rawId, - derivationPath = cryptoCurrency.network.derivationPath.value, + networkId = cryptoCurrency.network.id, destinationAddress = "", tokenSymbol = "", amount = "", diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt index b15638b859..15754e5e6a 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/confirm/model/NFTSendConfirmModel.kt @@ -20,13 +20,13 @@ 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.models.wallet.isHotWallet import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase import com.tangem.domain.feedback.models.BlockchainErrorInfo import com.tangem.domain.feedback.models.FeedbackEmailType +import com.tangem.domain.models.wallet.isHotWallet import com.tangem.domain.settings.IsSendTapHelpEnabledUseCase import com.tangem.domain.settings.NeverShowTapHelpUseCase import com.tangem.domain.transaction.usecase.CreateNFTTransferTransactionUseCase @@ -57,10 +57,10 @@ import com.tangem.features.send.v2.sendnft.confirm.model.transformers.NFTSendCon import com.tangem.features.send.v2.sendnft.ui.state.NFTSendUM import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.stripZeroPlainString +import com.tangem.utils.logging.TangemLogger import com.tangem.utils.transformer.update import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import javax.inject.Inject import com.tangem.features.send.v2.api.entity.FeeSelectorUM as FeeSelectorUMRedesigned @@ -197,8 +197,7 @@ internal class NFTSendConfirmModel @Inject constructor( saveBlockchainErrorUseCase( error = BlockchainErrorInfo( errorMessage = errorMessage, - blockchainId = cryptoCurrency.network.rawId, - derivationPath = cryptoCurrency.network.derivationPath.value, + networkId = cryptoCurrency.network.id, destinationAddress = confirmData.enteredDestination.orEmpty(), tokenSymbol = null, amount = params.nftAsset.amount?.toString().orEmpty(), @@ -379,7 +378,7 @@ internal class NFTSendConfirmModel @Inject constructor( flow = uiState, flow2 = params.currentRoute, transform = { state, route -> state to route }, - ).onEach { (state, route) -> + ).onEach { (state, _) -> val confirmUM = state.confirmUM params.callback.onResult( state.copy( diff --git a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt index 54b8e11901..962b35e833 100644 --- a/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt +++ b/features/send-v2/impl/src/main/java/com/tangem/features/send/v2/sendnft/model/NFTSendModel.kt @@ -15,6 +15,7 @@ import com.tangem.core.decompose.model.ParamsContainer import com.tangem.core.decompose.navigation.Router import com.tangem.datasource.local.nft.converter.NFTSdkAssetConverter import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency @@ -27,7 +28,6 @@ import com.tangem.domain.models.account.Account import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesProducer import com.tangem.domain.tokens.MultiWalletCryptoCurrenciesSupplier import com.tangem.domain.transaction.error.GetFeeError @@ -223,8 +223,7 @@ internal class NFTSendModel @Inject constructor( saveBlockchainErrorUseCase( error = BlockchainErrorInfo( errorMessage = errorMessage.orEmpty(), - blockchainId = cryptoCurrency.network.rawId, - derivationPath = cryptoCurrency.network.derivationPath.value, + networkId = cryptoCurrency.network.id, destinationAddress = "", tokenSymbol = null, amount = "", 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 ccceb6379c..a2081de45c 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 @@ -2,6 +2,9 @@ package com.tangem.features.staking.impl.presentation.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.blockchain.common.TransactionData import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee @@ -10,13 +13,8 @@ import com.tangem.common.routing.AppRouter import com.tangem.common.ui.amountScreen.converters.AmountReduceByTransformer.ReduceByData import com.tangem.common.ui.amountScreen.models.AmountState import com.tangem.common.ui.amountScreen.models.EnterAmountBoundary -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.bottomsheet.permission.state.ApproveType import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig -import com.tangem.features.approval.api.GiveApprovalComponent -import com.tangem.features.approval.api.GiveApprovalFeatureToggles import com.tangem.common.ui.notifications.NotificationUM import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.analytics.api.ParamsInterceptorHolder @@ -36,11 +34,11 @@ import com.tangem.core.ui.haptic.TangemHapticEffect import com.tangem.core.ui.haptic.VibratorHapticManager import com.tangem.core.ui.message.DialogMessage import com.tangem.domain.account.status.usecase.GetAccountCurrencyStatusUseCase +import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase 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.utils.coroutines.AppCoroutineScope import com.tangem.domain.feedback.GetWalletMetaInfoUseCase import com.tangem.domain.feedback.SaveBlockchainErrorUseCase import com.tangem.domain.feedback.SendFeedbackEmailUseCase @@ -61,12 +59,13 @@ import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.domain.staking.repositories.P2PEthPoolRepository -import com.tangem.domain.account.status.usecase.GetFeePaidCryptoCurrencyStatusSyncUseCase import com.tangem.domain.tokens.* import com.tangem.domain.transaction.error.GetFeeError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.utils.convertToSdkAmount import com.tangem.domain.wallets.usecase.GetUserWalletUseCase +import com.tangem.features.approval.api.GiveApprovalComponent +import com.tangem.features.approval.api.GiveApprovalFeatureToggles import com.tangem.features.staking.api.StakingComponent import com.tangem.features.staking.impl.R import com.tangem.features.staking.impl.analytics.StakingParamsInterceptor @@ -100,13 +99,13 @@ import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_I import com.tangem.utils.coroutines.* import com.tangem.utils.extensions.isSingleItem import com.tangem.utils.extensions.orZero +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal import java.util.concurrent.CopyOnWriteArrayList import javax.inject.Inject @@ -1067,8 +1066,7 @@ internal class StakingModel @Inject constructor( saveBlockchainErrorUseCase( error = BlockchainErrorInfo( errorMessage = errorMessage, - blockchainId = network.rawId, - derivationPath = network.derivationPath.value, + networkId = network.id, destinationAddress = target?.address.orEmpty(), tokenSymbol = (cryptoCurrencyStatus.currency as? CryptoCurrency.Token)?.symbol, amount = amount?.run { value?.toPlainString() + currencySymbol }.orEmpty(), diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt index ca9f4e5992..8e16ba2154 100644 --- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt +++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/helpers/StakeKitTransactionSender.kt @@ -33,13 +33,13 @@ import com.tangem.features.staking.impl.presentation.state.StakingUiState import com.tangem.features.staking.impl.presentation.state.utils.checkAndCalculateSubtractedAmount import com.tangem.features.staking.impl.presentation.state.utils.isCompositePendingActions import com.tangem.utils.extensions.orZero +import com.tangem.utils.logging.TangemLogger import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope -import com.tangem.utils.logging.TangemLogger import java.math.BigDecimal @Suppress("LongParameterList") @@ -165,7 +165,7 @@ internal class StakeKitTransactionSender @AssistedInject constructor( ?.map { transaction -> async { getConstructedStakingTransactionUseCase( - networkId = cryptoCurrencyStatus.currency.network.rawId, + networkId = cryptoCurrencyStatus.currency.network.id.rawId, fee = fee, amount = amount.convertToSdkAmount(cryptoCurrencyStatus), transactionId = transaction.id, 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 22ead70b9b..7e53fda3a2 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 @@ -75,8 +75,7 @@ internal class SwapAlertFactory @Inject constructor( saveBlockchainErrorUseCase( error = BlockchainErrorInfo( errorMessage = errorMessage.orEmpty(), - blockchainId = cryptoCurrency?.network?.rawId.orEmpty(), - derivationPath = cryptoCurrency?.network?.derivationPath?.value.orEmpty(), + networkId = cryptoCurrency?.network?.id, destinationAddress = confirmData?.enteredDestination.orEmpty(), tokenSymbol = confirmData?.toCryptoCurrencyStatus?.currency?.symbol.orEmpty(), amount = confirmData?.enteredFromAmount?.toString().orEmpty(), 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 5416aaad86..47be410214 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 @@ -2112,8 +2112,7 @@ internal class SwapModel @Inject constructor( saveBlockchainErrorUseCase( error = BlockchainErrorInfo( errorMessage = errorMessage, - blockchainId = network.rawId, - derivationPath = network.derivationPath.value, + networkId = network.id, destinationAddress = transaction?.txTo.orEmpty(), tokenSymbol = fromCurrencyStatus.currency.symbol, amount = dataState.amount.orEmpty(), 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 cec6ab682a..d6de0e0316 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 @@ -61,8 +61,7 @@ class YieldSupplyAlertFactory @Inject constructor( saveBlockchainErrorUseCase( error = BlockchainErrorInfo( errorMessage = errorMessage.orEmpty(), - blockchainId = cryptoCurrency?.network?.rawId.orEmpty(), - derivationPath = cryptoCurrency?.network?.derivationPath?.value.orEmpty(), + networkId = cryptoCurrency?.network?.id, tokenSymbol = cryptoCurrency?.symbol.orEmpty(), destinationAddress = "", amount = "", From b38ebb399538b797b641c8daeffc360aebd81b89 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 13 Apr 2026 19:48:06 +0300 Subject: [PATCH 29/32] Updated on 2026-08-14 --- .../di/domain/DynamicAddressesDomainModule.kt | 8 +-- .../tangemTech/models/UserTokensResponse.kt | 4 +- .../DefaultDynamicAddressesRepository.kt | 16 ++++- .../DynamicAddressesInitializer.kt | 42 +++++++++++++ data/networks/build.gradle.kts | 1 + .../fetcher/CommonNetworkStatusFetcher.kt | 2 + .../multi/DefaultMultiNetworkStatusFetcher.kt | 15 ++++- .../DefaultMultiNetworkStatusFetcherTest.kt | 21 ++++++- .../DefaultWalletManagersFacade.kt | 17 +++++- .../DisableDynamicAddressesUseCase.kt | 2 +- .../DynamicAddressesSupportedBlockchains.kt | 23 +++---- .../dynamicaddresses/GetDerivedXpubUseCase.kt | 60 +++++++++++++++++++ .../dynamicaddresses/IsXpubDerivedUseCase.kt | 37 ------------ .../repository/DynamicAddressesRepository.kt | 2 +- .../walletmanager/WalletManagersFacade.kt | 2 + .../model/DynamicAddressesDelegate.kt | 15 +++-- 16 files changed, 196 insertions(+), 71 deletions(-) create mode 100644 data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt create mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDerivedXpubUseCase.kt delete mode 100644 domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt index 3b9ec07528..0ab55063f9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/DynamicAddressesDomainModule.kt @@ -5,7 +5,7 @@ import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase import com.tangem.domain.dynamicaddresses.GetDynamicAddressesStatusUseCase import com.tangem.domain.dynamicaddresses.GetDynamicReceiveAddressUseCase -import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase +import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase import com.tangem.domain.dynamicaddresses.IsXpubSupportedUseCase import com.tangem.domain.dynamicaddresses.repository.ConsolidationRepository import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository @@ -69,10 +69,10 @@ internal object DynamicAddressesDomainModule { @Provides @Singleton - fun provideIsXpubDerivedUseCase( + fun provideGetDerivedXpubUseCase( walletManagersFacade: WalletManagersFacade, derivationsRepository: DerivationsRepository, - ): IsXpubDerivedUseCase { - return IsXpubDerivedUseCase(walletManagersFacade, derivationsRepository) + ): GetDerivedXpubUseCase { + return GetDerivedXpubUseCase(walletManagersFacade, derivationsRepository) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt index 4fe788204a..a4dccdfd90 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/UserTokensResponse.kt @@ -42,7 +42,8 @@ data class UserTokensResponse( return otherToken.contractAddress == this.contractAddress && otherToken.networkId == this.networkId && otherToken.derivationPath == this.derivationPath && - otherToken.decimals == this.decimals + otherToken.decimals == this.decimals && + otherToken.dynamicAddressesEnabled == this.dynamicAddressesEnabled } override fun hashCode(): Int = calculateHashCode( @@ -50,6 +51,7 @@ data class UserTokensResponse( networkId.hashCode(), derivationPath.hashCode(), decimals.hashCode(), + dynamicAddressesEnabled.hashCode(), ) } diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt index c2c6203803..ce82539915 100644 --- a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DefaultDynamicAddressesRepository.kt @@ -49,7 +49,12 @@ internal class DefaultDynamicAddressesRepository( } updateTokenDynamicAddressesFlag(userWalletId, network, enabled = true) runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } - .onFailure { TangemLogger.e("Failed to sync tokens after DA enable for $userWalletId", it) } + .onFailure { throwable -> + TangemLogger.e( + messageString = "Failed to sync tokens after dynamic addresses enable for $userWalletId", + throwable = throwable, + ) + } } } @@ -61,7 +66,12 @@ internal class DefaultDynamicAddressesRepository( } updateTokenDynamicAddressesFlag(userWalletId, network, enabled = false) runSuspendCatching { accountsCRUDRepository.syncTokens(userWalletId) } - .onFailure { TangemLogger.e("Failed to sync tokens after DA disable for $userWalletId", it) } + .onFailure { throwable -> + TangemLogger.e( + messageString = "Failed to sync tokens after dynamic addresses disable for $userWalletId", + throwable = throwable, + ) + } } } @@ -140,7 +150,7 @@ internal class DefaultDynamicAddressesRepository( } private suspend fun isXpubAvailable(userWalletId: UserWalletId, network: Network): Boolean { - // Check if WalletManager is already in XPUB mode (DA was previously enabled on this device) + // Check if WalletManager is already in XPUB mode (dynamic addresses was previously enabled on this device) return walletManagersFacade.getDynamicAddressesReceiveAddress(userWalletId, network) != null } diff --git a/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt new file mode 100644 index 0000000000..6eae2bf3b5 --- /dev/null +++ b/data/dynamic-addresses/src/main/java/com/tangem/data/dynamicaddresses/DynamicAddressesInitializer.kt @@ -0,0 +1,42 @@ +package com.tangem.data.dynamicaddresses + +import com.tangem.domain.dynamicaddresses.DynamicAddressesFeatureToggles +import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase +import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus +import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.utils.logging.TangemLogger +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Provides XPUB strings for networks that need dynamic addresses restore (ENABLED_REQUIRES_SETUP). + * Uses only already-derived keys — no card scan triggered. + */ +@Singleton +class DynamicAddressesInitializer @Inject constructor( + private val dynamicAddressesRepository: DynamicAddressesRepository, + private val dynamicAddressesFeatureToggles: DynamicAddressesFeatureToggles, + private val getDerivedXpubUseCase: GetDerivedXpubUseCase, +) { + + suspend fun getXpubs(userWalletId: UserWalletId, networks: Set): Map { + if (!dynamicAddressesFeatureToggles.isDynamicAddressesEnabled) return emptyMap() + + val result = mutableMapOf() + for (network in networks) { + val status = dynamicAddressesRepository.getStatus(userWalletId, network).firstOrNull() + if (status != DynamicAddressesStatus.ENABLED_REQUIRES_SETUP) continue + + val xpub = getDerivedXpubUseCase(userWalletId, network) + if (xpub != null) { + result[network] = xpub + } else { + TangemLogger.w("Dynamic addresses enabled but XPUB not available for ${network.id}") + } + } + return result + } +} \ No newline at end of file diff --git a/data/networks/build.gradle.kts b/data/networks/build.gradle.kts index d90e4e2a52..67c7f4eb40 100644 --- a/data/networks/build.gradle.kts +++ b/data/networks/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { // region Project - Data implementation(projects.data.common) + implementation(projects.data.dynamicAddresses) // endregion // region Project - Domain diff --git a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt index 38625e0f4a..17293dccca 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/fetcher/CommonNetworkStatusFetcher.kt @@ -43,6 +43,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor( userWalletId: UserWalletId, network: Network, networkCurrencies: Set, + xpub: String? = null, ): Either { return Either.catchOn(dispatchers.default) { val result = withContext(dispatchers.io) { @@ -52,6 +53,7 @@ internal class CommonNetworkStatusFetcher @Inject constructor( extraTokens = networkCurrencies .filterIsInstance() .toSet(), + xpub = xpub, ) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt index e429c134da..1f3e0860a0 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcher.kt @@ -2,16 +2,18 @@ package com.tangem.data.networks.multi import arrow.core.raise.catch import arrow.core.raise.ensure -import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStore import com.tangem.data.networks.store.setSourceAsCache import com.tangem.data.networks.store.setSourceAsOnlyCache +import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer +import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.domain.core.utils.eitherOn import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.network.Network import com.tangem.domain.networks.multi.MultiNetworkStatusFetcher import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.logging.TangemLogger import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope @@ -27,11 +29,11 @@ import javax.inject.Inject * [REDACTED_AUTHOR] */ -@Suppress("LongParameterList") internal class DefaultMultiNetworkStatusFetcher @Inject constructor( private val networksStatusesStore: NetworksStatusesStore, private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory, private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher, + private val dynamicAddressesInitializer: DynamicAddressesInitializer, private val dispatchers: CoroutineDispatcherProvider, ) : MultiNetworkStatusFetcher { @@ -50,6 +52,14 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( }, ) + val xpubByNetwork = catch( + block = { dynamicAddressesInitializer.getXpubs(params.userWalletId, params.networks) }, + catch = { error -> + TangemLogger.e("Failed to build XPUBs for restore", error) + emptyMap() + }, + ) + val result = coroutineScope { params.networks .map { network -> @@ -58,6 +68,7 @@ internal class DefaultMultiNetworkStatusFetcher @Inject constructor( userWalletId = params.userWalletId, network = network, networkCurrencies = networksCurrencies[network].orEmpty().toSet(), + xpub = xpubByNetwork[network], ) } } diff --git a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt index f02bcd3cd3..8a1a396ed2 100644 --- a/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt +++ b/data/networks/src/test/java/com/tangem/data/networks/multi/DefaultMultiNetworkStatusFetcherTest.kt @@ -3,6 +3,7 @@ package com.tangem.data.networks.multi import arrow.core.Either import arrow.core.left import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.data.dynamicaddresses.DynamicAddressesInitializer import com.tangem.domain.common.tokens.CardCryptoCurrencyFactory import com.tangem.data.networks.fetcher.CommonNetworkStatusFetcher import com.tangem.data.networks.store.NetworksStatusesStore @@ -27,17 +28,21 @@ internal class DefaultMultiNetworkStatusFetcherTest { private val networksStatusesStore: NetworksStatusesStore = mockk(relaxUnitFun = true) private val cardCryptoCurrencyFactory: CardCryptoCurrencyFactory = mockk() private val commonNetworkStatusFetcher: CommonNetworkStatusFetcher = mockk() + private val dynamicAddressesInitializer: DynamicAddressesInitializer = mockk() private val fetcher = DefaultMultiNetworkStatusFetcher( networksStatusesStore = networksStatusesStore, cardCryptoCurrencyFactory = cardCryptoCurrencyFactory, commonNetworkStatusFetcher = commonNetworkStatusFetcher, + dynamicAddressesInitializer = dynamicAddressesInitializer, dispatchers = TestingCoroutineDispatcherProvider(), ) @BeforeEach fun resetMocks() { - clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher) + clearMocks(networksStatusesStore, cardCryptoCurrencyFactory, commonNetworkStatusFetcher, dynamicAddressesInitializer) + // No dynamic addresses restore by default + coEvery { dynamicAddressesInitializer.getXpubs(any(), any()) } returns emptyMap() } @Test @@ -62,6 +67,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) } returns ethereumFetcherResult @@ -70,6 +76,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } returns cardanoFetcherResult @@ -87,11 +94,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } @@ -122,6 +131,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) } returns ethereumFetcherResult @@ -130,6 +140,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } returns cardanoFetcherResult @@ -147,11 +158,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } @@ -182,6 +195,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) } returns ethereumFetcherResult @@ -190,6 +204,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } returns cardanoFetcherResult @@ -207,11 +222,13 @@ internal class DefaultMultiNetworkStatusFetcherTest { userWalletId = params.userWalletId, network = ethereum.network, networkCurrencies = setOf(ethereum), + xpub = null, ) commonNetworkStatusFetcher.fetch( userWalletId = params.userWalletId, network = cardano.network, networkCurrencies = setOf(cardano), + xpub = null, ) } @@ -245,7 +262,7 @@ internal class DefaultMultiNetworkStatusFetcherTest { networksStatusesStore.setSourceAsOnlyCache(params.userWalletId, params.networks) } - coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any()) } + coVerify(inverse = true) { commonNetworkStatusFetcher.fetch(any(), any(), any(), any()) } } private companion object { diff --git a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt index d86119e490..c23bb4c58e 100644 --- a/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt +++ b/data/wallet-manager/src/main/java/com/tangem/data/walletmanager/DefaultWalletManagersFacade.kt @@ -59,7 +59,7 @@ import java.util.EnumSet import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject -@Suppress("LargeClass", "TooManyFunctions") +@Suppress("LargeClass", "TooManyFunctions", "LongParameterList") internal class DefaultWalletManagersFacade @Inject constructor( private val walletManagersStore: WalletManagersStore, private val userWalletsListRepository: UserWalletsListRepository, @@ -85,6 +85,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( userWalletId: UserWalletId, network: Network, extraTokens: Set, + xpub: String?, ): UpdateWalletManagerResult { val userWallet = getUserWallet(userWalletId) val blockchain = network.toBlockchain() @@ -95,6 +96,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( blockchain = blockchain, derivationPath = derivationPath, extraTokens = extraTokens, + xpub = xpub, ) } @@ -309,6 +311,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( blockchain: Blockchain, derivationPath: String?, extraTokens: Set, + xpub: String? = null, ): UpdateWalletManagerResult { if (derivationPath != null && !userWallet.hasDerivation(blockchain, derivationPath)) { TangemLogger.w("Derivation missed for: $blockchain") @@ -326,6 +329,7 @@ internal class DefaultWalletManagersFacade @Inject constructor( } val isUpdated = updateWalletManagerTokensIfNeeded(walletManager, extraTokens) + if (xpub != null) restoreXpubModeIfNeeded(walletManager, xpub) return try { if (userWallet is UserWallet.Cold && demoConfig.isDemoCardId(userWallet.scanResponse.card.cardId)) { @@ -499,6 +503,17 @@ internal class DefaultWalletManagersFacade @Inject constructor( } } + private fun restoreXpubModeIfNeeded(walletManager: WalletManager, xpub: String) { + val dynamicAddressesManager = walletManager as? DynamicAddressesManager ?: return + + try { + dynamicAddressesManager.enableDynamicAddresses(xpub) + TangemLogger.i("Restored XPUB mode for ${walletManager.wallet.blockchain}") + } catch (e: Exception) { + TangemLogger.e("Failed to restore XPUB mode: ${e.message}") + } + } + // endregion Dynamic Addresses @Deprecated("Will be removed in future") diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt index 214c7ee0b7..d0bcbb4660 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DisableDynamicAddressesUseCase.kt @@ -11,7 +11,7 @@ class DisableDynamicAddressesUseCase( /** * Returns true when consolidation is required before disabling (non-base balances exist), - * or false when DA was disabled immediately. + * or false when dynamic addresses was disabled immediately. */ suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Either = Either.catch { diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt index 3e732ca6dc..c3df141dd3 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/DynamicAddressesSupportedBlockchains.kt @@ -1,12 +1,13 @@ package com.tangem.domain.dynamicaddresses import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.toNetworkId /** * List of blockchains that support Dynamic Addresses (XPUB-based multi-address mode). * Must match [Blockchain.isBip44DerivationStyleXPUB] from blockchain-sdk minus Kaspa (deferred). * - * Per ASMPT-005: DA is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses. + * Dynamic addresses is NOT used for Legacy (m/44' for BTC/LTC) or Taproot (m/86') addresses. * Only the default derivation style per blockchain is supported. */ object DynamicAddressesSupportedBlockchains { @@ -26,22 +27,22 @@ object DynamicAddressesSupportedBlockchains { Blockchain.RavencoinTestnet, ) - private val supportedNetworkIds = supported.map { it.id }.toSet() + private val supportedNetworkIds = supported.map { it.toNetworkId() }.toSet() /** * Allowed BIP purpose nodes per network ID. * BTC/LTC use BIP-84 (SegWit), others use BIP-44 (Legacy P2PKH). */ private val allowedPurposeByNetworkId: Map = buildMap { - put(Blockchain.Bitcoin.id, BIP84_PURPOSE) - put(Blockchain.BitcoinTestnet.id, BIP84_PURPOSE) - put(Blockchain.Litecoin.id, BIP84_PURPOSE) - put(Blockchain.BitcoinCash.id, BIP44_PURPOSE) - put(Blockchain.BitcoinCashTestnet.id, BIP44_PURPOSE) - put(Blockchain.Dogecoin.id, BIP44_PURPOSE) - put(Blockchain.Dash.id, BIP44_PURPOSE) - put(Blockchain.Ravencoin.id, BIP44_PURPOSE) - put(Blockchain.RavencoinTestnet.id, BIP44_PURPOSE) + put(Blockchain.Bitcoin.toNetworkId(), BIP84_PURPOSE) + put(Blockchain.BitcoinTestnet.toNetworkId(), BIP84_PURPOSE) + put(Blockchain.Litecoin.toNetworkId(), BIP84_PURPOSE) + put(Blockchain.BitcoinCash.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.BitcoinCashTestnet.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.Dogecoin.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.Dash.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.Ravencoin.toNetworkId(), BIP44_PURPOSE) + put(Blockchain.RavencoinTestnet.toNetworkId(), BIP44_PURPOSE) } fun isSupported(blockchain: Blockchain): Boolean = blockchain in supported diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDerivedXpubUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDerivedXpubUseCase.kt new file mode 100644 index 0000000000..4c3b3cd2bf --- /dev/null +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/GetDerivedXpubUseCase.kt @@ -0,0 +1,60 @@ +package com.tangem.domain.dynamicaddresses + +import com.tangem.blockchainsdk.utils.toBlockchain +import com.tangem.common.extensions.ByteArrayKey +import com.tangem.common.extensions.calculateRipemd160 +import com.tangem.common.extensions.calculateSha256 +import com.tangem.crypto.NetworkType +import com.tangem.crypto.hdWallet.DerivationPath +import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey +import com.tangem.domain.models.network.Network +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.derivations.DerivationsRepository + +/** + * Returns the XPUB string if account-level keys are already derived (no card scan needed), + * or null if keys are not available. + */ +class GetDerivedXpubUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val derivationsRepository: DerivationsRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId, network: Network): String? { + val blockchain = network.toBlockchain() + if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return null + if (!blockchain.isBip44DerivationStyleXPUB()) return null + + val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return null + val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return null + if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return null + + val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey) + val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey) + + val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT)) + val parentPath = DerivationPath(accountPath.nodes.dropLast(1)) + + val childExtKey = existingKeys[accountPath] ?: return null + val parentExtKey = existingKeys[parentPath] ?: return null + + val parentFingerprint = parentExtKey.publicKey + .calculateSha256().calculateRipemd160() + .take(PARENT_FINGERPRINT_SIZE).toByteArray() + + val net = if (blockchain.isTestnet()) NetworkType.Testnet else NetworkType.Mainnet + return ExtendedPublicKey( + publicKey = childExtKey.publicKey, + chainCode = childExtKey.chainCode, + depth = accountPath.nodes.size, + parentFingerprint = parentFingerprint, + childNumber = accountPath.nodes.last().index, + ).serialize(net) + } + + private companion object { + const val ACCOUNT_PATH_DROP_COUNT = 2 + const val PARENT_FINGERPRINT_SIZE = 4 + } +} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt deleted file mode 100644 index 3a62af2f51..0000000000 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/IsXpubDerivedUseCase.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.tangem.domain.dynamicaddresses - -import com.tangem.blockchainsdk.utils.toBlockchain -import com.tangem.common.extensions.ByteArrayKey -import com.tangem.crypto.hdWallet.DerivationPath -import com.tangem.domain.models.network.Network -import com.tangem.domain.models.wallet.UserWalletId -import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.domain.wallets.derivations.DerivationsRepository - -/** - * Checks if the account-level XPUB key is already derived (no card scan needed). - */ -class IsXpubDerivedUseCase( - private val walletManagersFacade: WalletManagersFacade, - private val derivationsRepository: DerivationsRepository, -) { - - suspend operator fun invoke(userWalletId: UserWalletId, network: Network): Boolean { - val blockchain = network.toBlockchain() - if (!DynamicAddressesSupportedBlockchains.isSupported(blockchain)) return false - if (!blockchain.isBip44DerivationStyleXPUB()) return false - - val walletManager = walletManagersFacade.getOrCreateWalletManager(userWalletId, network) ?: return false - val hdKey = walletManager.wallet.publicKey.derivationType?.hdKey ?: return false - if (hdKey.path.nodes.size <= ACCOUNT_PATH_DROP_COUNT) return false - val accountPath = DerivationPath(hdKey.path.nodes.dropLast(ACCOUNT_PATH_DROP_COUNT)) - - val seedKey = ByteArrayKey(walletManager.wallet.publicKey.seedKey) - val existingKeys = derivationsRepository.getExistingDerivedKeys(userWalletId, seedKey) - return existingKeys[accountPath] != null - } - - private companion object { - const val ACCOUNT_PATH_DROP_COUNT = 2 - } -} \ No newline at end of file diff --git a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt index 4bcb627e16..eec289800e 100644 --- a/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt +++ b/domain/dynamic-addresses/src/main/kotlin/com/tangem/domain/dynamicaddresses/repository/DynamicAddressesRepository.kt @@ -20,6 +20,6 @@ interface DynamicAddressesRepository { suspend fun hasNonBaseBalances(userWalletId: UserWalletId, network: Network): Boolean - /** Returns true if there are custom tokens with change/index ≠ 0 that conflict with DA */ + /** Returns true if there are custom tokens with change/index ≠ 0 that conflict with dynamic addresses */ suspend fun hasConflictingCustomTokens(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt index af4b9462bb..92824ca373 100644 --- a/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/wallet-manager/src/main/kotlin/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -38,12 +38,14 @@ interface WalletManagersFacade { * @param userWalletId The ID of the user's wallet. * @param network The network. * @param extraTokens Additional tokens. + * @param xpub XPUB string to restore dynamic addresses mode if not yet active. * @return The result of updating the wallet manager. */ suspend fun update( userWalletId: UserWalletId, network: Network, extraTokens: Set, + xpub: String? = null, ): UpdateWalletManagerResult /** diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt index a6a548aa4e..63f7200f14 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/DynamicAddressesDelegate.kt @@ -12,7 +12,7 @@ import com.tangem.domain.dynamicaddresses.CreateConsolidationTransactionUseCase import com.tangem.domain.dynamicaddresses.DisableDynamicAddressesUseCase import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesError import com.tangem.domain.dynamicaddresses.EnableDynamicAddressesUseCase -import com.tangem.domain.dynamicaddresses.IsXpubDerivedUseCase +import com.tangem.domain.dynamicaddresses.GetDerivedXpubUseCase import com.tangem.domain.dynamicaddresses.model.DynamicAddressesStatus import com.tangem.domain.dynamicaddresses.repository.DynamicAddressesRepository import com.tangem.domain.models.currency.CryptoCurrencyStatus @@ -44,7 +44,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( private val createConsolidationTransactionUseCase: CreateConsolidationTransactionUseCase, private val getFeeUseCase: GetFeeUseCase, private val sendTransactionUseCase: SendTransactionUseCase, - private val isXpubDerivedUseCase: IsXpubDerivedUseCase, + private val getDerivedXpubUseCase: GetDerivedXpubUseCase, private val dynamicAddressesRepository: DynamicAddressesRepository, private val getExtendedPublicKeyUseCase: GetExtendedPublicKeyForCurrencyUseCase, @GlobalUiMessageSender private val uiMessageSender: UiMessageSender, @@ -138,7 +138,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( ) } is EnableDynamicAddressesError.ServiceError -> { - TangemLogger.e("Failed to enable DA: ${error.cause.message}") + TangemLogger.e("Failed to enable dynamic addresses: ${error.cause.message}") _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( onDismissClick = dismissBottomSheet, ) @@ -201,7 +201,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( ) } .onFailure { e -> - TangemLogger.e("Failed to disable DA: ${e.message}") + TangemLogger.e("Failed to disable dynamic addresses: ${e.message}") _bottomSheetConfig.value = DynamicAddressesBottomSheetConfig.ServiceUnavailable( onDismissClick = dismissBottomSheet, ) @@ -277,9 +277,8 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( ) } - // TODO: Replace with actual DA documentation URL private fun onReadMoreClick() { - // Stub: open documentation about Dynamic Addresses consolidation + // TODO: Replace with actual URL } private fun onDisableClick() { @@ -319,7 +318,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( try { dynamicAddressesRepository.disable(userWalletId, network) } catch (e: Exception) { - TangemLogger.e("Failed to disable DA after consolidation: ${e.message}") + TangemLogger.e("Failed to disable dynamic addresses after consolidation: ${e.message}") } dismissBottomSheet() onDynamicAddressesStateChanged() @@ -338,7 +337,7 @@ internal class DynamicAddressesDelegate @AssistedInject constructor( // region Common private suspend fun isXpubAlreadyDerived(network: Network): Boolean { - return isXpubDerivedUseCase(userWalletId, network) + return getDerivedXpubUseCase(userWalletId, network) != null } private fun isUserCancellation(error: Throwable): Boolean { From ecb1c6d2722a818cfeeefc94dc50f2cf24415e61 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Apr 2026 12:00:17 +0300 Subject: [PATCH 30/32] Updated on 2026-08-14 --- .../send/warnings/CardanoWarningsTest.kt | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/send/warnings/CardanoWarningsTest.kt diff --git a/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/CardanoWarningsTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/CardanoWarningsTest.kt new file mode 100644 index 0000000000..e7137a0293 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/send/warnings/CardanoWarningsTest.kt @@ -0,0 +1,151 @@ +package com.tangem.tests.send.warnings + +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.CARDANO_ADDRESS +import com.tangem.common.constants.TestConstants.QUOTES_API_SCENARIO +import com.tangem.common.constants.TestConstants.USER_TOKENS_API_SCENARIO +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.utils.resetWireMockScenarioState +import com.tangem.scenarios.checkSendWarning +import com.tangem.scenarios.openSendScreen +import com.tangem.screens.onSendAddressScreen +import com.tangem.screens.onSendScreen +import com.tangem.wallet.R +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 CardanoWarningsTest : BaseTestCase() { + private val tokenName = "Cardano" + private val minAmount = "ADA 1.00" + + private val invalidAmountTitle = getResourceString(R.string.send_notification_invalid_amount_title) + private val invalidAmountMessage = + getResourceString(R.string.send_notification_invalid_minimum_amount_text, minAmount, minAmount) + + @AllureId("4204") + @DisplayName("Warnings: check warning, when remains less than 1 ADA") + @Test + fun afterTransactionRemainsLessThanMinimumAmountTest() { + val sendAmount = "19" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + + step("Open 'Send Screen' with token: $tokenName") { + openSendScreen(tokenName) + } + step("Type '$sendAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(sendAmount) + } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(CARDANO_ADDRESS) } + } + step("Click on 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } + step("Assert 'Invalid amount' warning is displayed") { + checkSendWarning( + title = invalidAmountTitle, + message = invalidAmountMessage, + isDisplayed = true + ) + } + } + } + + @AllureId("4207") + @DisplayName("Warnings: check warning, when amount more than 1 ADA") + @Test + fun transactionAmountMoreThanOneTest() { + val sendAmount = "2.5" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + + step("Open 'Send Screen' with token: $tokenName") { + openSendScreen(tokenName) + } + step("Type '$sendAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(sendAmount) + } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(CARDANO_ADDRESS) } + } + step("Click on 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } + step("Assert 'Invalid amount' warning is not displayed") { + checkSendWarning( + title = invalidAmountTitle, + message = invalidAmountMessage, + isDisplayed = false + ) + } + } + } + + @AllureId("4210") + @DisplayName("Warnings: check warning, when remains more than 1 ADA") + @Test + fun afterTransactionRemainsMoreThanMinimumAmountTest() { + val sendAmount = "18" + + setupHooks( + additionalAfterSection = { + resetWireMockScenarioState(USER_TOKENS_API_SCENARIO) + resetWireMockScenarioState(QUOTES_API_SCENARIO) + } + ).run { + + step("Open 'Send Screen' with token: $tokenName") { + openSendScreen(tokenName) + } + step("Type '$sendAmount' in input text field") { + onSendScreen { + amountInputTextField.performClick() + amountInputTextField.performTextReplacement(sendAmount) + } + } + step("Click on 'Next' button") { + onSendScreen { nextButton.clickWithAssertion() } + } + step("Type address in input text field") { + onSendAddressScreen { addressTextField.performTextReplacement(CARDANO_ADDRESS) } + } + step("Click on 'Next' button") { + onSendAddressScreen { nextButton.clickWithAssertion() } + } + step("Assert 'Invalid amount' warning is not displayed") { + checkSendWarning( + title = invalidAmountTitle, + message = invalidAmountMessage, + isDisplayed = false + ) + } + } + } +} \ No newline at end of file From 822ed8eb8089e5f944babac2319becdbf5f95b4e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Apr 2026 12:46:07 +0300 Subject: [PATCH 31/32] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 18 ++- .../tangem/common/constants/TestConstants.kt | 11 ++ .../common/utils/AddressComparisonHelper.kt | 88 +++++++++++++ .../com/tangem/common/utils/NetworkUtils.kt | 61 ++++++++- .../com/tangem/scenarios/AddressScenarios.kt | 122 ++++++++++++++++++ .../com/tangem/scenarios/BaseScenarios.kt | 51 ++++++++ .../screens/CreateMobileWalletPageObject.kt | 23 ++++ .../screens/CreateWalletStartPageObject.kt | 5 + .../com/tangem/screens/DialogPageObject.kt | 5 + .../tangem/screens/ImportWalletPageObject.kt | 54 ++++++++ .../tangem/screens/TesterMenuPageObject.kt | 39 ++++++ .../tangem/tests/hotWallet/AddressesTest.kt | 86 ++++++++++++ .../ui/test/ImportWalletScreenTestTags.kt | 6 + .../port/ui/AddExistingWalletImportContent.kt | 10 +- 14 files changed, 570 insertions(+), 9 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/common/utils/AddressComparisonHelper.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/CreateMobileWalletPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/ImportWalletPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/TesterMenuPageObject.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/tests/hotWallet/AddressesTest.kt create mode 100644 core/ui/src/main/java/com/tangem/core/ui/test/ImportWalletScreenTestTags.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 3b89b53e54..6acf072e3c 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -8,6 +8,8 @@ import androidx.test.core.app.ActivityScenario import androidx.test.espresso.intent.Intents import androidx.test.platform.app.InstrumentationRegistry import androidx.test.rule.GrantPermissionRule +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeoutOrNull import com.kaspersky.components.alluresupport.interceptors.step.ScreenshotStepInterceptor import com.kaspersky.components.alluresupport.withForcedAllureSupport import com.kaspersky.components.composesupport.config.addComposeSupport @@ -20,13 +22,14 @@ import com.tangem.common.rules.ApiEnvironmentRule import com.tangem.datasource.api.common.config.managers.ApiConfigsManager import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.datasource.utils.WireMockRedirectInterceptor import com.tangem.domain.promo.PromoRepository import com.tangem.domain.promo.models.PromoId +import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.tap.MainActivity import dagger.hilt.android.testing.HiltAndroidRule import io.qameta.allure.kotlin.Allure -import kotlinx.coroutines.runBlocking import org.junit.Rule import org.junit.rules.RuleChain import org.junit.rules.TestRule @@ -58,6 +61,12 @@ abstract class BaseTestCase : TestCase( @Inject lateinit var promoRepository: PromoRepository + @Inject + lateinit var walletManagersStore: WalletManagersStore + + @Inject + lateinit var getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase + private val hiltRule = HiltAndroidRule(this) private val apiEnvironmentRule = ApiEnvironmentRule() private val permissionRule = GrantPermissionRule.grant( @@ -72,7 +81,11 @@ abstract class BaseTestCase : TestCase( private val semanticTreePrinterRule = object : TestWatcher() { override fun failed(e: Throwable?, description: Description?) { - runCatching { printAllRoots() } + runCatching { + runBlocking { + withTimeoutOrNull(SEMANTIC_TREE_PRINT_TIMEOUT_MS) { printAllRoots() } + } + } } } @@ -174,5 +187,6 @@ abstract class BaseTestCase : TestCase( private companion object { const val WIREMOCK_BASE_URL_ARG = "wiremockBaseUrl" + const val SEMANTIC_TREE_PRINT_TIMEOUT_MS = 5_000L } } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt index fd5d74d374..8ad9db91da 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/constants/TestConstants.kt @@ -35,6 +35,7 @@ object TestConstants { const val WAIT_UNTIL_TIMEOUT = 20_000L const val WAIT_UNTIL_TIMEOUT_LONG = 30_000L + const val WAIT_UNTIL_TIMEOUT_VERY_LONG = 60_000L const val MARKETS_MAIN_NETWORK_SUFFIX = "MAIN" @@ -43,4 +44,14 @@ object TestConstants { const val USER_TOKENS_API_SCENARIO = "user_tokens_api" const val QUOTES_API_SCENARIO = "quotes_api" + + const val SEED_PHRASE_12 = "they cram join fantasy unfair observe true theory buffalo bus exchange walk" + const val SEED_PHRASE_15 = "genuine try deer upset connect sausage diary rule price shallow fit faculty leopard " + + "hawk when" + const val SEED_PHRASE_18 = "crush idle include refuse expose kiss slot budget uphold when dinner certain holiday " + + "slow word armor butter suffer" + const val SEED_PHRASE_21 = "employ space oval venue wash clog zebra cover icon wash assist word debris inform " + + "cable meadow add game meat rigid pride" + const val SEED_PHRASE_24 = "force visit fresh brown razor target ill scissors figure cave feel genre cargo category " + + "bread much nature basic fun iron benefit egg error prosper" } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/AddressComparisonHelper.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/AddressComparisonHelper.kt new file mode 100644 index 0000000000..2faf0b96ab --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/AddressComparisonHelper.kt @@ -0,0 +1,88 @@ +package com.tangem.common.utils + +import com.tangem.utils.logging.TangemLogger +import org.json.JSONArray +import org.json.JSONObject +import org.junit.Assert.assertTrue + +/** + * Compares addresses from the app (clipboard JSON) with reference addresses from the QA tools API. + * + * Both JSON arrays contain objects with fields: blockchain, derivationPath, token (nullable), addresses (array). + * Comparison normalizes and sorts both arrays before diffing. + */ +object AddressComparisonHelper { + + fun compareAddresses(appJson: String, apiJson: String) { + val appEntries = parseAndNormalize(appJson) + val apiEntries = parseAndNormalize(apiJson) + + val missingInApp = apiEntries - appEntries.toSet() + val extraInApp = appEntries - apiEntries.toSet() + + if (missingInApp.isEmpty() && extraInApp.isEmpty()) { + TangemLogger.i("Address comparison passed: ${appEntries.size} entries match") + return + } + + val report = buildString { + appendLine("Address comparison FAILED") + if (missingInApp.isNotEmpty()) { + appendLine("\nMissing in app (expected from API but not found):") + missingInApp.forEach { appendLine(" - $it") } + } + if (extraInApp.isNotEmpty()) { + appendLine("\nExtra in app (found in app but not in API):") + extraInApp.forEach { appendLine(" - $it") } + } + appendLine("\nApp entries: ${appEntries.size}, API entries: ${apiEntries.size}") + } + + TangemLogger.e(report) + assertTrue(report, false) + } + + private fun parseAndNormalize(json: String): List { + val array = JSONArray(json) + val entries = mutableListOf() + + for (i in 0 until array.length()) { + val obj = array.getJSONObject(i) + entries.add( + AddressEntry( + blockchain = normalizeBlockchainName(obj.getString("blockchain")), + derivationPath = obj.getString("derivationPath").trim(), + token = obj.optString("token", null)?.trim()?.lowercase(), + addresses = parseAddresses(obj).sorted(), + ), + ) + } + + return entries.sortedWith( + compareBy { it.blockchain } + .thenBy { it.derivationPath } + .thenBy { it.token }, + ) + } + + private val blockchainNameOverrides = mapOf( + "chia network" to "chia", + ) + + private fun normalizeBlockchainName(name: String): String { + val normalized = name.trim().lowercase() + return blockchainNameOverrides[normalized] ?: normalized + } + + private fun parseAddresses(obj: JSONObject): List { + val addressesArray = obj.getJSONArray("addresses") + return (0 until addressesArray.length()).map { addressesArray.getString(it) } + } + + private data class AddressEntry( + val blockchain: String, + val derivationPath: String, + val token: String?, + val addresses: List, + ) +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt index 48c2a5cc5a..27b8c945ad 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/utils/NetworkUtils.kt @@ -16,10 +16,10 @@ fun getWcUri( TangemLogger.i("Getting WC URI for network: $network") val client = OkHttpClient.Builder() - .connectTimeout(30, TimeUnit.SECONDS) // Таймаут подключения - .readTimeout(60, TimeUnit.SECONDS) // Таймаут чтения ответа - .writeTimeout(30, TimeUnit.SECONDS) // Таймаут записи - .callTimeout(90, TimeUnit.SECONDS) // Общий таймаут запроса + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .callTimeout(90, TimeUnit.SECONDS) .build() val request = Request.Builder() @@ -58,6 +58,59 @@ fun getWcUri( } } +fun getAddressesFromApi( + seedKey: String, + baseUrl: String = "[REDACTED_ENV_URL]", +): String? { + TangemLogger.i("Getting addresses for seed key: $seedKey") + + val client = OkHttpClient.Builder() + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(60, TimeUnit.SECONDS) + .callTimeout(90, TimeUnit.SECONDS) + .build() + + val request = Request.Builder() + .url("$baseUrl/addresses") + .get() + .build() + + return try { + client.newCall(request).execute().use { response -> + TangemLogger.i("Response code: ${response.code}") + + if (response.isSuccessful) { + val body = response.body?.string() ?: "" + + val contentType = response.header("Content-Type") ?: "" + if (!contentType.contains("application/json") && !body.trimStart().startsWith("{")) { + TangemLogger.e("Unexpected response (not JSON), Content-Type: $contentType, body: $body") + return null + } + + val jsonObject = JSONObject(body) + val data = jsonObject.optJSONObject("data") ?: jsonObject + val seedData = data.optJSONArray(seedKey) + + if (seedData != null) { + TangemLogger.i("Got addresses for $seedKey: ${seedData.length()} entries") + seedData.toString() + } else { + TangemLogger.e("No data found for seed key: $seedKey") + null + } + } else { + val errorBody = response.body?.string() ?: "No error body" + TangemLogger.e("Request failed: ${response.code}, body: $errorBody") + null + } + } + } catch (e: Exception) { + TangemLogger.e("Error getting addresses", e) + null + } +} + fun checkServiceHealth( baseUrl: String = "[REDACTED_ENV_URL]" ): String? { diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt new file mode 100644 index 0000000000..f259dd60c4 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt @@ -0,0 +1,122 @@ +package com.tangem.scenarios + +import android.view.KeyEvent +import androidx.test.core.app.ApplicationProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.constants.TestConstants.WAIT_UNTIL_TIMEOUT_VERY_LONG +import com.tangem.common.utils.AddressComparisonHelper +import com.tangem.common.utils.getClipboardText +import com.tangem.screens.onMainScreen +import com.tangem.screens.onTesterMenuScreen +import com.tangem.utils.StringsSigns.DASH_SIGN +import com.tangem.utils.logging.TangemLogger +import io.qameta.allure.kotlin.Allure.step +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import org.junit.Assert.assertNotNull + +private const val WALLET_MANAGERS_SETTLE_MS = 5_000L +private const val WALLET_MANAGERS_POLL_INTERVAL_MS = 500L + +fun BaseTestCase.verifyAddresses(seedPhrase: String, apiAddressesJson: String) { + var appAddressesJson: String? = null + + step("Open 'Main Screen' with existing hot wallet") { + openMainScreenWithExistingHotWallet(seedPhrase) + } + step("Assert wallet balance = '$DASH_SIGN'") { + composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) { + runCatching { onMainScreen { totalBalanceText.assertTextContains(DASH_SIGN) } }.isSuccess + } + } + step("Assert 'Organize tokens' button is enabled") { + composeTestRule.waitUntil(timeoutMillis = WAIT_UNTIL_TIMEOUT_VERY_LONG) { + runCatching { onMainScreen { organizeTokensButton().assertIsEnabled() } }.isSuccess + } + } + step("Wait for all wallet managers to initialize") { + awaitWalletManagersStabilized() + } + step("Open tester menu") { + openTesterMenu() + } + step("Click on 'Addresses info' button") { + onTesterMenuScreen { addressesInfoButton.performClick() } + } + step("Click on 'JSON' tab") { + onTesterMenuScreen { jsonTab.performClick() } + } + step("Click on 'Copy' button") { + onTesterMenuScreen { copyButton.performClick() } + } + step("Get addresses JSON from clipboard") { + appAddressesJson = getClipboardText(ApplicationProvider.getApplicationContext()) + assertNotNull("Clipboard is empty after copying addresses", appAddressesJson) + } + step("Compare app addresses with API reference") { + AddressComparisonHelper.compareAddresses( + appJson = requireNotNull(appAddressesJson), + apiJson = apiAddressesJson, + ) + } +} + +private const val TESTER_MENU_MAX_ATTEMPTS = 3 + +/** + * Presses 'Volume Down' twice to open tester menu. + * Retries up to [TESTER_MENU_MAX_ATTEMPTS] times if the menu doesn't appear. + */ +private fun BaseTestCase.openTesterMenu() { + repeat(TESTER_MENU_MAX_ATTEMPTS) { attempt -> + waitForIdle() + device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN) + device.uiDevice.pressKeyCode(KeyEvent.KEYCODE_VOLUME_DOWN) + + val opened = runCatching { + onTesterMenuScreen { addressesInfoButton.assertIsDisplayed() } + }.isSuccess + + if (opened) { + TangemLogger.i("Tester menu opened on attempt ${attempt + 1}") + return + } + TangemLogger.w("Tester menu not opened on attempt ${attempt + 1}, retrying...") + } + error("Failed to open tester menu after $TESTER_MENU_MAX_ATTEMPTS attempts") +} + +/** + * Polls [walletManagersStore] until the wallet manager count stops growing for [WALLET_MANAGERS_SETTLE_MS]. + + * + * Uses [getAllSync] with a polling interval instead of Flow, because the Flow only emits on changes — + * if the count stabilizes, there would be no new emission to check the settle timeout against. + */ +private fun BaseTestCase.awaitWalletManagersStabilized() { + val walletId = getSelectedWalletSyncUseCase().getOrNull()?.walletId + ?: error("No selected wallet found") + var lastSize = -1 + var stableStart = System.currentTimeMillis() + + runBlocking { + withTimeout(WAIT_UNTIL_TIMEOUT_VERY_LONG) { + while (true) { + val currentSize = walletManagersStore.getAllSync(walletId).size + val now = System.currentTimeMillis() + + if (currentSize != lastSize) { + TangemLogger.i("Wallet managers count: $currentSize (was $lastSize)") + lastSize = currentSize + stableStart = now + } else if (now - stableStart >= WALLET_MANAGERS_SETTLE_MS) { + TangemLogger.i("Wallet managers stabilized at $currentSize entries") + return@withTimeout + } + + delay(WALLET_MANAGERS_POLL_INTERVAL_MS) + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt index 2d11046a5c..beb2b2d14f 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/BaseScenarios.kt @@ -59,6 +59,57 @@ fun BaseTestCase.openMainScreen( } } +fun BaseTestCase.openMainScreenWithExistingHotWallet(seedPhrase: String) { + step("Click on 'Accept' button") { + onDisclaimerScreen { acceptButton.clickWithAssertion() } + } + step("Click on 'Get started' button") { + onStoriesScreen { getStartedButton.clickWithAssertion() } + } + step("Click on 'Start with Mobile Wallet' button") { + onCreateWalletStartScreen { startWithMobileWalletButton.performClick() } + } + step("Click on 'Import existing wallet' button") { + onCreateMobileWalletScreen { importExistingWalletButton.performClick() } + } + step("Click on 'Phrase text field'") { + onImportWalletScreen { phraseTextField.performClick() } + } + step("Type seed phrase in 'Phrase text field'") { + onImportWalletScreen { phraseTextField.performTextReplacement(seedPhrase) } + } + step("Click on 'Import' button") { + onImportWalletScreen { + importButton.assertIsEnabled() + importButton.performClick() + } + } + step("Click on 'Continue' button") { + onImportWalletScreen { + continueButton.assertIsEnabled() + continueButton.performClick() + } + } + step("Click on 'Skip' button") { + onImportWalletScreen { skipButton.performClick() } + } + step("Click on 'Skip anyway' dialog button") { + onDialog { skipAnywayButton.performClick() } + } + step("Click on 'Finish' button") { + onImportWalletScreen { + finishButton.assertIsEnabled() + finishButton.performClick() + } + } + step("Assert 'Main' screen is displayed") { + onMainScreen { screenContainer.assertIsDisplayed() } + } + step("Dismiss Market Tooltip by clicking close button") { + onMarketsTooltipScreen { closeButton.clickWithAssertion() } + } +} + fun BaseTestCase.synchronizeAddresses( balance: String? = null, isBalanceAvailable: Boolean = true diff --git a/app/src/androidTest/kotlin/com/tangem/screens/CreateMobileWalletPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/CreateMobileWalletPageObject.kt new file mode 100644 index 0000000000..e83def9363 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/CreateMobileWalletPageObject.kt @@ -0,0 +1,23 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.core.ui.test.BaseButtonTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class CreateMobileWalletPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val importExistingWalletButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(R.string.hw_import_existing_wallet)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onCreateMobileWalletScreen(function: CreateMobileWalletPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt index b06335d752..6d875cd034 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/CreateWalletStartPageObject.kt @@ -17,6 +17,11 @@ class CreateWalletStartPageObject(semanticsProvider: SemanticsNodeInteractionsPr hasText(getResourceString(OnboardingImplR.string.welcome_unlock_card)) useUnmergedTree = true } + + val startWithMobileWalletButton: KNode = child { + hasText(getResourceString(OnboardingImplR.string.welcome_create_wallet_mobile_title)) + useUnmergedTree = true + } } internal fun BaseTestCase.onCreateWalletStartScreen(function: CreateWalletStartPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt index 13eef6b87d..ff268b8969 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/DialogPageObject.kt @@ -64,6 +64,11 @@ class DialogPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : hasTestTag(BaseButtonTestTags.BUTTON) hasText(getResourceString(R.string.common_change)) } + + val skipAnywayButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasText(getResourceString(R.string.access_code_alert_skip_ok)) + } } internal fun BaseTestCase.onDialog(function: DialogPageObject.() -> Unit) = diff --git a/app/src/androidTest/kotlin/com/tangem/screens/ImportWalletPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/ImportWalletPageObject.kt new file mode 100644 index 0000000000..26d1af7a54 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/ImportWalletPageObject.kt @@ -0,0 +1,54 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.common.R +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.ImportWalletScreenTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import androidx.compose.ui.test.hasText as withText + +class ImportWalletPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val phraseTextField: KNode = child { + hasTestTag(ImportWalletScreenTestTags.PHRASE_TEXT_FIELD) + useUnmergedTree = true + } + + val passphraseTextField: KNode = child { + hasTestTag(ImportWalletScreenTestTags.PASSPHRASE_TEXT_FIELD) + useUnmergedTree = true + } + + val importButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyChild(withText(getResourceString(R.string.common_import))) + useUnmergedTree = true + } + + val continueButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyChild(withText(getResourceString(R.string.common_continue))) + useUnmergedTree = true + } + + val skipButton: KNode = child { + hasTestTag(TopAppBarTestTags.MORE_BUTTON) + hasText(getResourceString(R.string.common_skip)) + useUnmergedTree = true + } + + val finishButton: KNode = child { + hasTestTag(BaseButtonTestTags.BUTTON) + hasAnyChild(withText(getResourceString(R.string.common_finish))) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onImportWalletScreen(function: ImportWalletPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/TesterMenuPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/TesterMenuPageObject.kt new file mode 100644 index 0000000000..dce99464a3 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/TesterMenuPageObject.kt @@ -0,0 +1,39 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import com.tangem.feature.tester.impl.R as TesterImplR +import com.tangem.core.ui.test.BaseButtonTestTags +import com.tangem.core.ui.test.TopAppBarTestTags +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString + +class TesterMenuPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val backButton: KNode = child { + hasTestTag(TopAppBarTestTags.CLOSE_BUTTON) + useUnmergedTree = true + } + + val addressesInfoButton: KNode = child { + hasTestTag(BaseButtonTestTags.TEXT) + hasText(getResourceString(TesterImplR.string.addresses_info)) + useUnmergedTree = true + } + + val jsonTab: KNode = child { + hasText("JSON") + useUnmergedTree = true + } + + val copyButton: KNode = child { + hasContentDescription("Copy") + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onTesterMenuScreen(function: TesterMenuPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/tests/hotWallet/AddressesTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/hotWallet/AddressesTest.kt new file mode 100644 index 0000000000..46695fa68b --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/tests/hotWallet/AddressesTest.kt @@ -0,0 +1,86 @@ +package com.tangem.tests.hotWallet + +import com.tangem.common.BaseTestCase +import com.tangem.common.annotations.ApiEnv +import com.tangem.common.annotations.ApiEnvConfig +import com.tangem.common.constants.TestConstants.SEED_PHRASE_12 +import com.tangem.common.constants.TestConstants.SEED_PHRASE_15 +import com.tangem.common.constants.TestConstants.SEED_PHRASE_18 +import com.tangem.common.constants.TestConstants.SEED_PHRASE_21 +import com.tangem.common.constants.TestConstants.SEED_PHRASE_24 +import com.tangem.common.utils.checkServiceHealth +import com.tangem.common.utils.getAddressesFromApi +import com.tangem.datasource.api.common.config.ApiConfig +import com.tangem.datasource.api.common.config.ApiEnvironment +import com.tangem.scenarios.verifyAddresses +import dagger.hilt.android.testing.HiltAndroidTest +import io.qameta.allure.kotlin.AllureId +import io.qameta.allure.kotlin.junit4.DisplayName +import org.junit.Assert.assertNotNull +import org.junit.Test + +@HiltAndroidTest +class AddressesTest : BaseTestCase() { + + private var apiAddressesJson: String? = null + + private fun setupAddressTestHooks(seedKey: String) = setupHooks( + additionalBeforeAppLaunchSection = { + val status = checkServiceHealth() + assertNotNull("QA tools service is unreachable ([REDACTED_ENV_URL]", status) + + apiAddressesJson = getAddressesFromApi(seedKey) + assertNotNull("Failed to fetch reference addresses for '$seedKey'", apiAddressesJson) + }, + ) + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("1792") + @DisplayName("Hot wallet: auto derivation addresses for seed 12") + @Test + fun seed12AddressesTest() { + setupAddressTestHooks("twelve").run { + verifyAddresses(SEED_PHRASE_12, requireNotNull(apiAddressesJson)) + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("5106") + @DisplayName("Hot wallet: auto derivation addresses for seed 15") + @Test + fun seed15AddressesTest() { + setupAddressTestHooks("fifteen").run { + verifyAddresses(SEED_PHRASE_15, requireNotNull(apiAddressesJson)) + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("5107") + @DisplayName("Hot wallet: auto derivation addresses for seed 18") + @Test + fun seed18AddressesTest() { + setupAddressTestHooks("eighteen").run { + verifyAddresses(SEED_PHRASE_18, requireNotNull(apiAddressesJson)) + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("5108") + @DisplayName("Hot wallet: auto derivation addresses for seed 21") + @Test + fun seed21AddressesTest() { + setupAddressTestHooks("twenty_one").run { + verifyAddresses(SEED_PHRASE_21, requireNotNull(apiAddressesJson)) + } + } + + @ApiEnv(ApiEnvConfig(ApiConfig.ID.TangemTech, ApiEnvironment.PROD)) + @AllureId("5109") + @DisplayName("Hot wallet: auto derivation addresses for seed 24") + @Test + fun seed24AddressesTest() { + setupAddressTestHooks("twenty_four").run { + verifyAddresses(SEED_PHRASE_24, requireNotNull(apiAddressesJson)) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/ImportWalletScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/ImportWalletScreenTestTags.kt new file mode 100644 index 0000000000..3e641be3e5 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/test/ImportWalletScreenTestTags.kt @@ -0,0 +1,6 @@ +package com.tangem.core.ui.test + +object ImportWalletScreenTestTags { + const val PHRASE_TEXT_FIELD = "IMPORT_WALLET_PHRASE_TEXT_FIELD" + const val PASSPHRASE_TEXT_FIELD = "IMPORT_WALLET_PASSPHRASE_TEXT_FIELD" +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt index d7bb9338b0..3b85b823dc 100644 --- a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/addexistingwallet/im/port/ui/AddExistingWalletImportContent.kt @@ -19,6 +19,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue @@ -39,6 +40,7 @@ import com.tangem.core.ui.components.PrimaryButton import com.tangem.core.ui.components.TangemTextFieldsDefault import com.tangem.core.ui.components.keyboardAsState import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.test.ImportWalletScreenTestTags import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -73,9 +75,10 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo ) OutlineTextFieldWithIcon( - modifier = modifier + modifier = Modifier .padding(horizontal = 16.dp) - .fillMaxWidth(), + .fillMaxWidth() + .testTag(ImportWalletScreenTestTags.PASSPHRASE_TEXT_FIELD), value = state.passPhrase, onValueChange = state.passPhraseChange, iconResId = R.drawable.ic_information_24, @@ -130,7 +133,8 @@ private fun PhraseBlock(state: AddExistingWalletImportUM, modifier: Modifier = M OutlinedTextField( modifier = Modifier .fillMaxWidth() - .height(TangemTheme.dimens.size142), + .height(TangemTheme.dimens.size142) + .testTag(ImportWalletScreenTestTags.PHRASE_TEXT_FIELD), value = state.words, onValueChange = state.wordsChange, textStyle = TangemTheme.typography.body1, From aa03d39dc0cbc7d981800dfed6bc3c99488c6d39 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 14 Apr 2026 12:44:34 +0200 Subject: [PATCH 32/32] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/inputrow/inner/PasteButton.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt index d816b39eb6..98b7616634 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/inner/PasteButton.kt @@ -45,7 +45,7 @@ fun PasteButton( ) { val clipboardManager = LocalClipboardManager.current val hapticFeedback = LocalHapticFeedback.current - val isPasteEnabled = !clipboardManager.getText()?.text.isNullOrEmpty() + val isPasteEnabled = clipboardManager.hasText() val color = if (isPasteEnabled) { backgroundColorEnabled } else {