From 13f139afa8f045e62f4c5ac6d1cf678092967915 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 10:35:39 +0200 Subject: [PATCH 01/12] Updated on 2026-08-14 --- .../format/bigdecimal/BigDecimalFiatFormat.kt | 2 +- .../bigdecimal/BigDecimalFiatFormatTest.kt | 48 +++++++++++++++++++ .../converter/MarketsTokenItemConverter.kt | 15 +++++- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index 2c29aedd8b..e8eb6add6d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -232,7 +232,7 @@ private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < * Returns amount with correct scale */ fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { - return if (value < BigDecimal.ONE) { + return if (value > BigDecimal.ZERO && value < BigDecimal.ONE) { val leadingZeroes = value.scale() - value.precision() val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES diff --git a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt index d5c2cb310e..fcc2eca898 100644 --- a/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt +++ b/core/ui/src/test/kotlin/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormatTest.kt @@ -294,4 +294,52 @@ internal class BigDecimalFiatFormatTest { Truth.assertThat(formatted) .isEqualTo("0.00000000000000000000123".addUsdSymbolLeft()) } + + @Test + fun `price zero is formatted with default precision and does not crash`() { + val testValue = BigDecimal.ZERO + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("0.00".addUsdSymbolLeft()) + } + + @Test + fun `price negative integer keeps sign and does not crash`() { + val testValue = BigDecimal("-500") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("-" + "500.00".addUsdSymbolLeft()) + } + + @Test + fun `price negative fractional keeps sign and does not crash`() { + val testValue = BigDecimal("-0.5") + + val formatted = testValue.format { + fiat( + fiatCurrencyCode = usdCurrencyCode, + fiatCurrencySymbol = usdSymbol, + locale = testLocale, + ).price() + } + + Truth.assertThat(formatted) + .isEqualTo("-" + "0.50".addUsdSymbolLeft()) + } } \ No newline at end of file diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt index dda2c04ee8..f9373e37a1 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt @@ -15,6 +15,7 @@ import com.tangem.domain.markets.TokenMarket import com.tangem.features.feed.impl.R import com.tangem.features.feed.model.market.list.state.MarketsListUM import com.tangem.utils.converter.Converter +import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import java.math.RoundingMode @@ -92,7 +93,15 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { val prevPrice = prev?.tokenQuotesShort?.currentPrice - val priceText = tokenQuotesShort.currentPrice.format { + val currentPrice = tokenQuotesShort.currentPrice + if (currentPrice < BigDecimal.ZERO) { + TangemLogger.withTag(MARKETS_PRICE_LOG_TAG).w( + messageString = "Unexpected non-positive price for tokenId=$id, symbol=$symbol, " + + "currency=${appCurrency.code}, value=${currentPrice.toPlainString()}", + ) + } + + val priceText = currentPrice.format { fiat( fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, @@ -162,4 +171,8 @@ internal class MarketsTokenItemConverter( return percent.format { percent() } } + + private companion object { + const val MARKETS_PRICE_LOG_TAG = "MarketsTokenItemConverter" + } } \ No newline at end of file From 14ac9e6f308ed2dec03de3f25e2e5cc342c12660 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 01:37:16 -0700 Subject: [PATCH 02/12] Updated on 2026-08-14 --- .../tangempay/limit/setup/TangemPayCardLimitSetupModel.kt | 2 +- .../tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt index d832e12eea..487e568445 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModel.kt @@ -113,7 +113,7 @@ internal class TangemPayCardLimitSetupModel @Inject constructor( } private fun onAmountChange(newValue: String) { - if (newValue.toBigDecimalOrNull() == null) return + if (newValue.isNotEmpty() && newValue.toBigDecimalOrNull() == null) return uiState.update { state -> state.copy( amountFieldModel = state.amountFieldModel.copy(value = newValue), diff --git a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt index b99ecc5bb7..f9a1defa96 100644 --- a/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt +++ b/features/tangempay/details/impl/src/test/kotlin/com/tangem/features/tangempay/limit/setup/TangemPayCardLimitSetupModelTest.kt @@ -101,6 +101,7 @@ internal class TangemPayCardLimitSetupModelTest { ) { val model = createModel() + model.uiState.value.amountFieldModel.onValueChange("100") model.uiState.value.amountFieldModel.onValueChange(amount) assertThat(model.uiState.value.isSubmitButtonEnabled).isEqualTo(expectedEnabled) @@ -149,7 +150,7 @@ internal class TangemPayCardLimitSetupModelTest { Arguments.of("100", true), Arguments.of("-1", false), Arguments.of("", false), - Arguments.of("abc", false), + Arguments.of("abc", true), Arguments.of("1001", false), ) } \ No newline at end of file From c7e2e7876ef03cc3f1ee54fbdfbc246b73b60426 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 11:39:05 +0300 Subject: [PATCH 03/12] Updated on 2026-08-14 --- .../src/main/assets/configs/feature_toggles_config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 71f159b32c..fa3065b6c9 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 @@ -57,7 +57,7 @@ }, { "name": "ADD_AND_MANAGE_TOKENS_ENABLED", - "version": "undefined" + "version": "5.38" }, { "name": "WALLET_CONNECT_BITCOIN_ENABLED", From e7edce0cd88be9f7858b3ba86f0036a1688292c7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 12:44:09 +0400 Subject: [PATCH 04/12] Updated on 2026-08-14 --- .../analytics/PortfolioAnalyticsEvent.kt | 14 +++ .../managetokens/model/AddAndManageModel.kt | 5 + .../intents/WalletContentClickIntents.kt | 2 + .../model/AddAndManageModelTest.kt | 101 ++++++++++++++++++ .../WalletContentClickIntentsAnalyticsTest.kt | 83 ++++++++++++++ 5 files changed, 205 insertions(+) create mode 100644 features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/analytics/PortfolioAnalyticsEvent.kt create mode 100644 features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModelTest.kt create mode 100644 features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntentsAnalyticsTest.kt diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/analytics/PortfolioAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/analytics/PortfolioAnalyticsEvent.kt new file mode 100644 index 0000000000..e69e15866b --- /dev/null +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/managetokens/analytics/PortfolioAnalyticsEvent.kt @@ -0,0 +1,14 @@ +package com.tangem.feature.wallet.child.managetokens.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +internal sealed class PortfolioAnalyticsEvent( + event: String, +) : AnalyticsEvent(category = "Portfolio", event = event) { + + class ButtonAddManage : PortfolioAnalyticsEvent(event = "Button - Add Manage") + + class ButtonAddTokens : PortfolioAnalyticsEvent(event = "Button - Add tokens") + + class ButtonOrganizeTokens : PortfolioAnalyticsEvent(event = "Button - Organize Tokens") +} \ 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 index 9600a357f8..ce64ceba7a 100644 --- 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 @@ -3,11 +3,13 @@ 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.analytics.api.AnalyticsEventHandler import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer import com.tangem.domain.models.account.AccountId import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent +import com.tangem.feature.wallet.child.managetokens.analytics.PortfolioAnalyticsEvent import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController @@ -21,6 +23,7 @@ internal class AddAndManageModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val portfolioFetcherFactory: PortfolioFetcher.Factory, + private val analyticsEventHandler: AnalyticsEventHandler, val portfolioSelectorController: PortfolioSelectorController, ) : Model() { @@ -45,6 +48,7 @@ internal class AddAndManageModel @Inject constructor( } fun onAddTokensClick() { + analyticsEventHandler.send(PortfolioAnalyticsEvent.ButtonAddTokens()) modelScope.launch { val data = portfolioFetcher.data.first() val isSingleAccount = data.isSingleChoice(params.userWalletId) @@ -65,6 +69,7 @@ internal class AddAndManageModel @Inject constructor( } fun onOrganizeTokensClick() { + analyticsEventHandler.send(PortfolioAnalyticsEvent.ButtonOrganizeTokens()) params.onDismiss() params.onOrganizeTokensClick() } 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 71220bf8a9..0e34ecf2d4 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 @@ -23,6 +23,7 @@ import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase import com.tangem.domain.yield.supply.usecase.YieldSupplySetShouldShowMainPromoUseCase +import com.tangem.feature.wallet.child.managetokens.analytics.PortfolioAnalyticsEvent import com.tangem.feature.wallet.presentation.account.AccountDependencies import com.tangem.feature.wallet.presentation.wallet.analytics.utils.TokenListAnalyticsSender import com.tangem.feature.wallet.presentation.wallet.domain.OnrampStatusFactory @@ -123,6 +124,7 @@ internal class WalletContentClickIntentsImplementor @Inject constructor( override fun onOrganizeTokensClick() { val userWalletId = stateHolder.getSelectedWalletId() if (walletFeatureToggles.isAddAndManageTokensEnabled) { + analyticsEventHandler.send(PortfolioAnalyticsEvent.ButtonAddManage()) router.openAddAndManageBottomSheet(userWalletId = userWalletId) } else { router.openOrganizeTokensScreen(userWalletId = userWalletId) diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModelTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModelTest.kt new file mode 100644 index 0000000000..73e1539274 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/managetokens/model/AddAndManageModelTest.kt @@ -0,0 +1,101 @@ +package com.tangem.feature.wallet.child.managetokens.model + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.core.decompose.model.MutableParamsContainer +import com.tangem.domain.models.account.AccountId +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent +import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioFetcher +import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorController +import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +internal class AddAndManageModelTest { + + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val portfolioFetcher: PortfolioFetcher = mockk(relaxed = true) { + every { data } returns flowOf( + PortfolioFetcher.Data( + appCurrency = mockk(relaxed = true), + isBalanceHidden = false, + balances = emptyMap(), + ), + ) + } + private val portfolioFetcherFactory: PortfolioFetcher.Factory = mockk(relaxed = true) { + every { create(any(), any()) } returns portfolioFetcher + } + private val portfolioSelectorController: PortfolioSelectorController = mockk(relaxed = true) { + every { selectedAccount } returns flowOf(null) + } + + private val onDismiss: () -> Unit = mockk(relaxed = true) + private val onOrganizeTokensClick: () -> Unit = mockk(relaxed = true) + private val onManageTokensClick: (AccountId) -> Unit = mockk(relaxed = true) + + private val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF") + + private val params = AddAndManageBottomSheetComponent.Params( + userWalletId = userWalletId, + onDismiss = onDismiss, + onOrganizeTokensClick = onOrganizeTokensClick, + onManageTokensClick = onManageTokensClick, + ) + + private fun createModel(): AddAndManageModel = AddAndManageModel( + paramsContainer = MutableParamsContainer(params), + dispatchers = TestingCoroutineDispatcherProvider(), + portfolioFetcherFactory = portfolioFetcherFactory, + analyticsEventHandler = analyticsEventHandler, + portfolioSelectorController = portfolioSelectorController, + ) + + @Test + fun `GIVEN bottom sheet model WHEN onAddTokensClick THEN sends ButtonAddTokens event with correct payload`() = + runTest { + val model = createModel() + val captured = slot() + + model.onAddTokensClick() + + verify(exactly = 1) { analyticsEventHandler.send(capture(captured)) } + assertThat(captured.captured.category).isEqualTo("Portfolio") + assertThat(captured.captured.event).isEqualTo("Button - Add tokens") + assertThat(captured.captured.params).isEmpty() + } + + @Test + fun `GIVEN bottom sheet model WHEN onOrganizeTokensClick THEN sends ButtonOrganizeTokens event with correct payload`() = + runTest { + val model = createModel() + val captured = slot() + + model.onOrganizeTokensClick() + + verify(exactly = 1) { analyticsEventHandler.send(capture(captured)) } + assertThat(captured.captured.category).isEqualTo("Portfolio") + assertThat(captured.captured.event).isEqualTo("Button - Organize Tokens") + assertThat(captured.captured.params).isEmpty() + } + + @Test + fun `GIVEN bottom sheet model WHEN onOrganizeTokensClick THEN dismisses bottom sheet and forwards to params callback`() = + runTest { + val model = createModel() + + model.onOrganizeTokensClick() + + verify(exactly = 1) { onDismiss() } + verify(exactly = 1) { onOrganizeTokensClick() } + } +} \ No newline at end of file diff --git a/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntentsAnalyticsTest.kt b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntentsAnalyticsTest.kt new file mode 100644 index 0000000000..d6a468a205 --- /dev/null +++ b/features/wallet/impl/src/test/kotlin/com/tangem/feature/wallet/child/wallet/model/intents/WalletContentClickIntentsAnalyticsTest.kt @@ -0,0 +1,83 @@ +package com.tangem.feature.wallet.child.wallet.model.intents + +import com.google.common.truth.Truth.assertThat +import com.tangem.core.analytics.api.AnalyticsEventHandler +import com.tangem.core.analytics.models.AnalyticsEvent +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.feature.wallet.presentation.router.InnerWalletRouter +import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController +import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +internal class WalletContentClickIntentsAnalyticsTest { + + private val stateHolder: WalletStateController = mockk(relaxed = true) + private val analyticsEventHandler: AnalyticsEventHandler = mockk(relaxed = true) + private val walletFeatureToggles: WalletFeatureToggles = mockk(relaxed = true) + private val router: InnerWalletRouter = mockk(relaxed = true) + + private val userWalletId = UserWalletId(stringValue = "0123456789ABCDEF") + + private fun createImplementor(): WalletContentClickIntentsImplementor { + every { stateHolder.getSelectedWalletId() } returns userWalletId + + val implementor = WalletContentClickIntentsImplementor( + stateHolder = stateHolder, + currencyActionsClickIntents = mockk(relaxed = true), + onrampStatusFactory = mockk(relaxed = true), + getUserWalletUseCase = mockk(relaxed = true), + singleAccountStatusListSupplier = mockk(relaxed = true), + getCryptoCurrencyActionsUseCase = mockk(relaxed = true), + getExplorerTransactionUrlUseCase = mockk(relaxed = true), + shouldShowMarketsTooltipUseCase = mockk(relaxed = true), + dispatchers = mockk(relaxed = true), + walletEventSender = mockk(relaxed = true), + analyticsEventHandler = analyticsEventHandler, + accountDependencies = mockk(relaxed = true), + yieldSupplySetShouldShowMainPromoUseCase = mockk(relaxed = true), + tokenListAnalyticsSender = mockk(relaxed = true), + uiMessageSender = mockk(relaxed = true), + walletFeatureToggles = walletFeatureToggles, + ) + implementor.initialize(router = router, coroutineScope = TestScope()) + return implementor + } + + @Test + fun `GIVEN add and manage toggle enabled WHEN onOrganizeTokensClick THEN sends ButtonAddManage event and opens bottom sheet`() = + runTest { + every { walletFeatureToggles.isAddAndManageTokensEnabled } returns true + val implementor = createImplementor() + val captured = slot() + + implementor.onOrganizeTokensClick() + + verify(exactly = 1) { analyticsEventHandler.send(capture(captured)) } + assertThat(captured.captured.category).isEqualTo("Portfolio") + assertThat(captured.captured.event).isEqualTo("Button - Add Manage") + assertThat(captured.captured.params).isEmpty() + verify(exactly = 1) { router.openAddAndManageBottomSheet(userWalletId = userWalletId) } + verify(exactly = 0) { router.openOrganizeTokensScreen(any()) } + } + + @Test + fun `GIVEN add and manage toggle disabled WHEN onOrganizeTokensClick THEN does not send analytics and opens organize screen`() = + runTest { + every { walletFeatureToggles.isAddAndManageTokensEnabled } returns false + val implementor = createImplementor() + + implementor.onOrganizeTokensClick() + + verify(exactly = 0) { analyticsEventHandler.send(any()) } + verify(exactly = 1) { router.openOrganizeTokensScreen(userWalletId = userWalletId) } + verify(exactly = 0) { router.openAddAndManageBottomSheet(any()) } + } +} \ No newline at end of file From 08514bf0204529f5f0c12b8fa88d8e114bb462f7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 18:36:29 +0500 Subject: [PATCH 05/12] Updated on 2026-08-14 --- .../tangempay/TangemPayAnalyticsEvents.kt | 25 +++++++++++++++++++ .../tangempay/model/TangemPayCardPageModel.kt | 1 + .../tangempay/model/TangemPayDetailsModel.kt | 9 ++++++- .../utils/TangemPayMessagesFactory.kt | 7 ++++-- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt index 03db2b8e49..a8229e803a 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/tangempay/TangemPayAnalyticsEvents.kt @@ -217,4 +217,29 @@ sealed class TangemPayAnalyticsEvents( categoryName = "Visa Onboarding", event = "Visa Permanent Button Clicked", ) + + class CardIconClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Card Icon Clicked", + ) + + class CardManagementScreenOpened : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Card Management Screen Opened", + ) + + class AddExtraCardClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Add Extra Card Clicked", + ) + + class FakeDoorPopupDisplayed : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Fakedoor Popup Displayed", + ) + + class FakeDoorGotitClicked : TangemPayAnalyticsEvents( + categoryName = "Visa Card Management", + event = "Visa Fakedoor Gotit Clicked", + ) } \ 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 index 45a98d4156..735d0b9398 100644 --- 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 @@ -82,6 +82,7 @@ internal class TangemPayCardPageModel @Inject constructor( // TODO v_rodionov: #[REDACTED_TASK_KEY] check reissue order state before card details are showed init { + analytics.send(TangemPayAnalyticsEvents.CardManagementScreenOpened()) fetchAddToWalletBanner() paymentAccountStatusSupplier.invoke(params.userWalletId) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index 355908fcb0..a500999cdc 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -322,11 +322,18 @@ internal class TangemPayDetailsModel @Inject constructor( } override fun onCardClick() { + analytics.send(TangemPayAnalyticsEvents.CardIconClicked()) router.push(TangemPayAccountDetailsInnerRoute.CardDetails) } override fun onAddCardClick() { - uiMessageSender.send(message = TangemPayMessagesFactory.createFutureFeature()) + analytics.send(TangemPayAnalyticsEvents.AddExtraCardClicked()) + analytics.send(TangemPayAnalyticsEvents.FakeDoorPopupDisplayed()) + uiMessageSender.send( + message = TangemPayMessagesFactory.createFutureFeature( + onGotItClick = { analytics.send(TangemPayAnalyticsEvents.FakeDoorGotitClicked()) }, + ), + ) } private fun showBottomSheetError(type: TangemPayDetailsErrorType) { diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt index e10ee99a4e..ad15a49737 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/utils/TangemPayMessagesFactory.kt @@ -114,7 +114,7 @@ internal object TangemPayMessagesFactory { } } - fun createFutureFeature(): BottomSheetMessage { + fun createFutureFeature(onGotItClick: () -> Unit): BottomSheetMessage { return bottomSheetMessage { infoBlock { icon(R.drawable.ic_credit_card_add_24) { @@ -125,7 +125,10 @@ internal object TangemPayMessagesFactory { } primaryButton { text = resourceReference(R.string.common_got_it) - onClick { closeBs() } + onClick { + onGotItClick() + closeBs() + } } } } From 9f7f0b519f8e062aa309764546e3127eede54373 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 16:41:08 +0300 Subject: [PATCH 06/12] Updated on 2026-08-14 --- .../kotlin/com/tangem/common/BaseTestCase.kt | 1 + .../com/tangem/scenarios/AddressScenarios.kt | 5 --- .../scenarios/CheckMainScreenScenarios.kt | 8 ++-- .../tangem/scenarios/MainScreenScenarios.kt | 22 ++++++++++ .../AddAndManageBottomSheetPageObject.kt | 31 +++++++++++++ .../tangem/screens/MainScreenPageObject.kt | 17 ++++---- .../com/tangem/tests/OrganizeTokensTest.kt | 43 +++++-------------- .../kotlin/com/tangem/tests/StakingTest.kt | 12 +++--- .../com/tangem/tests/main/MainScreenTest.kt | 16 +++---- .../tangem/core/ui/test/MainScreenTestTags.kt | 1 + .../MultiCurrencyOrganizeButton.kt | 8 +++- 11 files changed, 100 insertions(+), 64 deletions(-) create mode 100644 app/src/androidTest/kotlin/com/tangem/scenarios/MainScreenScenarios.kt create mode 100644 app/src/androidTest/kotlin/com/tangem/screens/AddAndManageBottomSheetPageObject.kt diff --git a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt index 3b22023f6e..b340c7b317 100644 --- a/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt +++ b/app/src/androidTest/kotlin/com/tangem/common/BaseTestCase.kt @@ -188,6 +188,7 @@ abstract class BaseTestCase : TestCase( "ACCOUNTS_FEATURE_ENABLED" to true, "GASLESS_APPROVAL_ENABLED" to true, "MAIN_SCREEN_QR_SCANNING_ENABLED" to true, + "ADD_AND_MANAGE_TOKENS_ENABLED" to true, ) ) } diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt index f259dd60c4..e4d65afb99 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/AddressScenarios.kt @@ -30,11 +30,6 @@ fun BaseTestCase.verifyAddresses(seedPhrase: String, apiAddressesJson: String) { 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() } diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt index 82c7f1feab..b8baf03580 100644 --- a/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/CheckMainScreenScenarios.kt @@ -72,8 +72,8 @@ fun BaseTestCase.checkSingleCurrencyMainScreen( onMainScreen { emptyTransactionBlockExploreButton.assertIsDisplayed() } } } - step("Assert 'Organize tokens' button is not displayed") { - onMainScreen { organizeTokensButtonWithoutLazySearch.assertIsNotDisplayed() } + step("Assert 'Add & Manage' button is not displayed") { + onMainScreen { addAndManageButtonWithoutLazySearch.assertIsNotDisplayed() } } } @@ -112,8 +112,8 @@ fun BaseTestCase.checkMultiCurrencyMainScreen( step("Assert 'Receive' button is not displayed") { onMainScreen { receiveButton.assertIsNotDisplayed() } } - step("Assert 'Organize tokens' button is displayed") { - onMainScreen { organizeTokensButton().assertIsDisplayed() } + step("Assert 'Add & Manage' button is displayed") { + onMainScreen { addAndManageButton().assertIsDisplayed() } } } diff --git a/app/src/androidTest/kotlin/com/tangem/scenarios/MainScreenScenarios.kt b/app/src/androidTest/kotlin/com/tangem/scenarios/MainScreenScenarios.kt new file mode 100644 index 0000000000..9c9b025c84 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/scenarios/MainScreenScenarios.kt @@ -0,0 +1,22 @@ +package com.tangem.scenarios + +import com.tangem.common.BaseTestCase +import com.tangem.common.extensions.SwipeDirection +import com.tangem.common.extensions.clickWithAssertion +import com.tangem.common.extensions.swipeVertical +import com.tangem.screens.onAddAndManageBottomSheet +import com.tangem.screens.onMainScreen +import io.qameta.allure.kotlin.Allure.step + +fun BaseTestCase.openOrganizeTokensScreen() { + step("Swipe to 'Add & Manage' button") { + swipeVertical(SwipeDirection.UP) + swipeVertical(SwipeDirection.UP) + } + step("Click on 'Add & Manage' button") { + onMainScreen { addAndManageButton().clickWithAssertion() } + } + step("Click on 'Organize tokens' button in bottom sheet") { + onAddAndManageBottomSheet { organizeTokensButton.clickWithAssertion() } + } +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/AddAndManageBottomSheetPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/AddAndManageBottomSheetPageObject.kt new file mode 100644 index 0000000000..38efc65897 --- /dev/null +++ b/app/src/androidTest/kotlin/com/tangem/screens/AddAndManageBottomSheetPageObject.kt @@ -0,0 +1,31 @@ +package com.tangem.screens + +import androidx.compose.ui.test.SemanticsNodeInteractionsProvider +import com.tangem.common.BaseTestCase +import io.github.kakaocup.compose.node.element.ComposeScreen +import io.github.kakaocup.compose.node.element.ComposeScreen.Companion.onComposeScreen +import io.github.kakaocup.compose.node.element.KNode +import io.github.kakaocup.kakao.common.utilities.getResourceString +import com.tangem.core.res.R as CoreResR + +class AddAndManageBottomSheetPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : + ComposeScreen(semanticsProvider = semanticsProvider) { + + val title: KNode = child { + hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens)) + useUnmergedTree = true + } + + val addTokensButton: KNode = child { + hasText(getResourceString(CoreResR.string.add_and_manage_sheet_manage_title)) + useUnmergedTree = true + } + + val organizeTokensButton: KNode = child { + hasText(getResourceString(CoreResR.string.add_and_manage_sheet_organize_title)) + useUnmergedTree = true + } +} + +internal fun BaseTestCase.onAddAndManageBottomSheet(function: AddAndManageBottomSheetPageObject.() -> Unit) = + onComposeScreen(composeTestRule, function) \ No newline at end of file diff --git a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt index d281e57344..82defdf7f0 100644 --- a/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt +++ b/app/src/androidTest/kotlin/com/tangem/screens/MainScreenPageObject.kt @@ -19,6 +19,7 @@ import io.github.kakaocup.compose.node.element.lazylist.KLazyListNode import io.github.kakaocup.kakao.common.utilities.getResourceString import androidx.compose.ui.test.hasTestTag as withTestTag import androidx.compose.ui.test.hasText as withText +import com.tangem.core.res.R as CoreResR import com.tangem.core.ui.R as CoreUiR class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) : @@ -214,8 +215,8 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) hasText(getResourceString(CoreUiR.string.wallet_notification_address_copied)) } - val organizeTokensButtonNode: KNode = child { - hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON) + val addAndManageButtonNode: KNode = child { + hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON) useUnmergedTree = true } @@ -265,18 +266,18 @@ class MainScreenPageObject(semanticsProvider: SemanticsNodeInteractionsProvider) } @OptIn(ExperimentalTestApi::class) - fun organizeTokensButton(): KNode { + fun addAndManageButton(): KNode { return lazyList.childWith { - hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON) + hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON) }.child { - hasText(getResourceString(R.string.organize_tokens_title)) + hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens)) useUnmergedTree = true } } - val organizeTokensButtonWithoutLazySearch: KNode = child { - hasTestTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON) - hasText(getResourceString(R.string.organize_tokens_title)) + val addAndManageButtonWithoutLazySearch: KNode = child { + hasTestTag(MainScreenTestTags.ADD_AND_MANAGE_BUTTON) + hasText(getResourceString(CoreResR.string.main_add_and_manage_tokens)) useUnmergedTree = true } diff --git a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt index fb132f5e1d..81fe762f61 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/OrganizeTokensTest.kt @@ -2,10 +2,9 @@ package com.tangem.tests import androidx.compose.ui.test.onAllNodesWithText import com.tangem.common.BaseTestCase -import com.tangem.common.extensions.SwipeDirection import com.tangem.common.extensions.clickWithAssertion -import com.tangem.common.extensions.swipeVertical import com.tangem.scenarios.openMainScreen +import com.tangem.scenarios.openOrganizeTokensScreen import com.tangem.scenarios.synchronizeAddresses import com.tangem.screens.onMainScreen import com.tangem.screens.onOrganizeTokensScreen @@ -31,12 +30,8 @@ class OrganizeTokensTest : BaseTestCase() { step("Click on 'Synchronize addresses' button") { onMainScreen { synchronizeAddressesButton.clickWithAssertion() } } - step("Swipe to 'Organize tokens' button") { - swipeVertical(SwipeDirection.UP) - swipeVertical(SwipeDirection.UP) - } - step("Click 'Organize tokens' button") { - onMainScreen { organizeTokensButton().clickWithAssertion() } + step("Open 'Organize tokens' screen") { + openOrganizeTokensScreen() } step("Assert 'Organize tokens' screen is opened") { onOrganizeTokensScreen { @@ -56,12 +51,8 @@ class OrganizeTokensTest : BaseTestCase() { step("Assert tokens were grouped on 'Main screen'") { onMainScreen { tokenNetworkGroupTitle(tokenNetwork).assertIsDisplayed() } } - step("Swipe to 'Organize tokens' button") { - swipeVertical(SwipeDirection.UP) - swipeVertical(SwipeDirection.UP) - } - step("Click 'Organize tokens' button") { - onMainScreen { organizeTokensButton().clickWithAssertion() } + step("Open 'Organize tokens' screen") { + openOrganizeTokensScreen() } step("Assert 'Organize tokens' screen is opened") { onOrganizeTokensScreen { @@ -104,12 +95,8 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() } } - step("Swipe to 'Organize tokens' button") { - swipeVertical(SwipeDirection.UP) - swipeVertical(SwipeDirection.UP) - } - step("Click 'Organize tokens' button") { - onMainScreen { organizeTokensButton().clickWithAssertion() } + step("Open 'Organize tokens' screen") { + openOrganizeTokensScreen() } step("Check positions of tokens on 'Organize tokens' screen") { onOrganizeTokensScreen { @@ -141,12 +128,8 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(ethereumTitle, 1).assertIsDisplayed() } } - step("Swipe to 'Organize tokens' button") { - swipeVertical(SwipeDirection.UP) - swipeVertical(SwipeDirection.UP) - } - step("Click 'Organize tokens' button") { - onMainScreen { organizeTokensButton().clickWithAssertion() } + step("Open 'Organize tokens' screen") { + openOrganizeTokensScreen() } step("Drag $bitcoinTitle down on 'Organize tokens' screen") { composeTestRule.waitUntil(timeoutMillis = 100_000) { @@ -191,12 +174,8 @@ class OrganizeTokensTest : BaseTestCase() { tokenWithTitleAndPosition(polygonTitle, 2).assertIsDisplayed() } } - step("Swipe to 'Organize tokens' button") { - swipeVertical(SwipeDirection.UP) - swipeVertical(SwipeDirection.UP) - } - step("Click 'Organize tokens' button") { - onMainScreen { organizeTokensButton().clickWithAssertion() } + step("Open 'Organize tokens' screen") { + openOrganizeTokensScreen() } step("Check positions of tokens on 'Organize tokens' screen") { onOrganizeTokensScreen { diff --git a/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt index f91fe7df1d..d02ce135ee 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/StakingTest.kt @@ -40,8 +40,8 @@ class StakingTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Assert 'Organize tokens' button is displayed") { - onMainScreen { organizeTokensButton().assertIsDisplayed() } + step("Assert 'Add & Manage' button is displayed") { + onMainScreen { addAndManageButton().assertIsDisplayed() } } step("Swipe up") { swipeVertical(SwipeDirection.UP) @@ -98,8 +98,8 @@ class StakingTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Assert 'Organize tokens' button is displayed") { - onMainScreen { organizeTokensButton().assertIsDisplayed() } + step("Assert 'Add & Manage' button is displayed") { + onMainScreen { addAndManageButton().assertIsDisplayed() } } step("Swipe up") { swipeVertical(SwipeDirection.UP) @@ -156,8 +156,8 @@ class StakingTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Assert 'Organize tokens' button is displayed") { - onMainScreen { organizeTokensButton().assertIsDisplayed() } + step("Assert 'Add & Manage' button is displayed") { + onMainScreen { addAndManageButton().assertIsDisplayed() } } step("Swipe up") { swipeVertical(SwipeDirection.UP) diff --git a/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt b/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt index 172bfbf08f..ac99d2e42e 100644 --- a/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt +++ b/app/src/androidTest/kotlin/com/tangem/tests/main/MainScreenTest.kt @@ -28,8 +28,8 @@ class MainScreenTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Assert 'Organize tokens' button is displayed") { - onMainScreen { organizeTokensButton().assertIsDisplayed() } + step("Assert 'Add & Manage' button is displayed") { + onMainScreen { addAndManageButton().assertIsDisplayed() } } } } @@ -56,8 +56,8 @@ class MainScreenTest : BaseTestCase() { step("Synchronize addresses") { synchronizeAddresses() } - step("Assert 'Organize tokens' button is not displayed") { - onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()} + step("Assert 'Add & Manage' button is not displayed") { + onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()} } } } @@ -81,8 +81,8 @@ class MainScreenTest : BaseTestCase() { step("Open 'Main Screen'") { openMainScreen() } - step("Assert 'Organize tokens' button is not displayed") { - onMainScreen { organizeTokensButtonNode.assertIsNotDisplayed()} + step("Assert 'Add & Manage' button is not displayed") { + onMainScreen { addAndManageButtonNode.assertIsNotDisplayed()} } } } @@ -106,8 +106,8 @@ class MainScreenTest : BaseTestCase() { step("Open 'Main Screen'") { openMainScreen() } - step("Assert 'Organize tokens' button is not displayed") { - onMainScreen { organizeTokensButtonNode.assertIsDisplayed()} + step("Assert 'Add & Manage' button is displayed") { + onMainScreen { addAndManageButtonNode.assertIsDisplayed()} } } } diff --git a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt index 269fc77316..b8d5867f2e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/test/MainScreenTestTags.kt @@ -7,6 +7,7 @@ object MainScreenTestTags { const val TOKEN_LIST_ITEM = "MAIN_SCREEN_TOKEN_LIST_ITEM" const val WALLET_LIST_ITEM = "MAIN_SCREEN_WALLET_LIST_ITEM" const val ORGANIZE_TOKENS_BUTTON = "MAIN_SCREEN_ORGANIZE_TOKENS_BUTTON" + const val ADD_AND_MANAGE_BUTTON = "MAIN_SCREEN_ADD_AND_MANAGE_BUTTON" const val CARD_TITLE = "MAIN_SCREEN_CARD_TITLE" const val CARD_IMAGE = "MAIN_SCREEN_CARD_IMAGE" const val DEVICES_COUNT = "MAIN_SCREEN_DEVICES_COUNT" 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 6e5177817c..e1e2ba0ce6 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,6 +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" @@ -24,8 +25,13 @@ internal fun LazyListScope.organizeTokensButton( modifier: Modifier = Modifier, ) { item(key = ORGANIZE_BUTTON_CONTENT_TYPE, contentType = ORGANIZE_BUTTON_CONTENT_TYPE) { + val testTag = if (config.textRes == R.string.main_add_and_manage_tokens) { + MainScreenTestTags.ADD_AND_MANAGE_BUTTON + } else { + MainScreenTestTags.ORGANIZE_TOKENS_BUTTON + } RoundedActionButton( - modifier = modifier.testTag(MainScreenTestTags.ORGANIZE_TOKENS_BUTTON), + modifier = modifier.testTag(testTag), config = ActionButtonConfig( text = resourceReference(id = config.textRes), iconResId = config.iconRes, From 3b1e6421502423e35e68cf602a5c108c4033ed9f Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 16:25:41 +0200 Subject: [PATCH 07/12] Updated on 2026-08-14 --- .../entry/components/FeedEntryComponent.kt | 1 + .../components/DefaultFeedEntryComponent.kt | 3 + .../tangem/features/feed/ui/EntryContent.kt | 61 +++++++++++++++---- .../wallet/child/wallet/WalletComponent.kt | 8 ++- .../presentation/wallet/ui/WalletScreen.kt | 12 ++-- .../presentation/wallet/ui/WalletScreen2.kt | 12 ++-- 6 files changed, 73 insertions(+), 24 deletions(-) diff --git a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt index f609996618..c99eeb7c18 100644 --- a/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt +++ b/features/feed/api/src/main/kotlin/com/tangem/features/feed/entry/components/FeedEntryComponent.kt @@ -17,6 +17,7 @@ interface FeedEntryComponent : ComposableContentComponent { fun BottomSheetContent( bottomSheetState: State, onHeaderSizeChange: (Dp) -> Unit, + onExpandSheet: () -> Unit, modifier: Modifier, ) diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt index 94c272503e..ed639b8931 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/DefaultFeedEntryComponent.kt @@ -172,6 +172,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( override fun BottomSheetContent( bottomSheetState: State, onHeaderSizeChange: (Dp) -> Unit, + onExpandSheet: () -> Unit, modifier: Modifier, ) { val bsState by bottomSheetState @@ -191,6 +192,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( bottomSheetState = bottomSheetState, stackState = stackStack, onHeaderSizeChange = onHeaderSizeChange, + onExpandSheet = onExpandSheet, isOpenedInBottomSheet = true, ) } @@ -213,6 +215,7 @@ internal class DefaultFeedEntryComponent @AssistedInject constructor( bottomSheetState = bottomSheetState, stackState = stack.subscribeAsState(), onHeaderSizeChange = {}, + onExpandSheet = {}, isOpenedInBottomSheet = false, ) } diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt index d0bf1b73b9..93ab10f9bf 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/EntryContent.kt @@ -1,6 +1,7 @@ package com.tangem.features.feed.ui import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface @@ -9,6 +10,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.arkivanov.decompose.ExperimentalDecomposeApi @@ -33,6 +35,7 @@ internal fun EntryContent( bottomSheetState: State, stackState: State>, onHeaderSizeChange: (Dp) -> Unit, + onExpandSheet: () -> Unit, isOpenedInBottomSheet: Boolean, ) { if (LocalRedesignEnabled.current) { @@ -40,6 +43,7 @@ internal fun EntryContent( bottomSheetState = bottomSheetState, stackState = stackState, onHeaderSizeChange = onHeaderSizeChange, + onExpandSheet = onExpandSheet, isOpenedInBottomSheet = isOpenedInBottomSheet, ) } else { @@ -47,6 +51,7 @@ internal fun EntryContent( bottomSheetState = bottomSheetState, stackState = stackState, onHeaderSizeChange = onHeaderSizeChange, + onExpandSheet = onExpandSheet, isOpenedInBottomSheet = isOpenedInBottomSheet, ) } @@ -57,6 +62,7 @@ private fun EntryContentV1( bottomSheetState: State, stackState: State>, onHeaderSizeChange: (Dp) -> Unit, + onExpandSheet: () -> Unit, isOpenedInBottomSheet: Boolean, ) { val density = LocalDensity.current @@ -68,7 +74,7 @@ private fun EntryContentV1( containerColor = background, contentWindowInsets = WindowInsetsZero, topBar = { - Children( + Box( modifier = Modifier .then( if (!isOpenedInBottomSheet) { @@ -84,10 +90,17 @@ private fun EntryContentV1( } } }, - stack = stackState.value, - animation = stackAnimation, - ) { child -> - child.instance.Title(bottomSheetState) + ) { + Children( + stack = stackState.value, + animation = stackAnimation, + ) { child -> + child.instance.Title(bottomSheetState) + } + CollapsedTitleClickOverlay( + bottomSheetState = bottomSheetState, + onExpandSheet = onExpandSheet, + ) } }, content = { contentPadding -> @@ -111,6 +124,7 @@ private fun EntryContentV2( bottomSheetState: State, stackState: State>, onHeaderSizeChange: (Dp) -> Unit, + onExpandSheet: () -> Unit, isOpenedInBottomSheet: Boolean, ) { val density = LocalDensity.current @@ -142,7 +156,7 @@ private fun EntryContentV2( bottomSheetState = bottomSheetState, ) } - AnimatedContent( + Box( modifier = Modifier .align(Alignment.TopStart) .then( @@ -161,14 +175,37 @@ private fun EntryContentV2( } } }, - targetState = stackState.value.active, - transitionSpec = animationAppBar, - contentKey = { it.key }, - label = "FeedEntryAppBar", - ) { state -> - state.instance.Title(bottomSheetState) + ) { + AnimatedContent( + targetState = stackState.value.active, + transitionSpec = animationAppBar, + contentKey = { it.key }, + label = "FeedEntryAppBar", + ) { state -> + state.instance.Title(bottomSheetState) + } + CollapsedTitleClickOverlay( + bottomSheetState = bottomSheetState, + onExpandSheet = onExpandSheet, + ) } } } } +} + +@Composable +private fun BoxScope.CollapsedTitleClickOverlay(bottomSheetState: State, onExpandSheet: () -> Unit) { + if (bottomSheetState.value == BottomSheetState.COLLAPSED) { + Box( + modifier = Modifier + .matchParentSize() + .clickable( + interactionSource = null, + indication = null, + onClick = onExpandSheet, + ) + .clearAndSetSemantics {}, + ) + } } \ 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 d6f6a2e7bc..cc0155e82f 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 @@ -260,10 +260,11 @@ internal class WalletComponent @AssistedInject constructor( WalletScreen2( state = uiState, tangemPayComponent = tangemPayMainBlockComponent, - bottomSheetContent = { + bottomSheetContent = { onExpandSheet -> BottomSheetContent( bottomSheetState = bottomSheetState, onHeaderSizeChange = { headerSize = it }, + onExpandSheet = onExpandSheet, modifier = modifier, ) }, @@ -275,10 +276,11 @@ internal class WalletComponent @AssistedInject constructor( state = uiState, promoBannersBlockComponent = promoBannersBlockComponent, tangemPayComponent = tangemPayMainBlockComponent, - bottomSheetContent = { + bottomSheetContent = { onExpandSheet -> BottomSheetContent( bottomSheetState = bottomSheetState, onHeaderSizeChange = { headerSize = it }, + onExpandSheet = onExpandSheet, modifier = modifier, ) }, @@ -305,11 +307,13 @@ internal class WalletComponent @AssistedInject constructor( private fun BottomSheetContent( bottomSheetState: State, onHeaderSizeChange: (Dp) -> Unit, + onExpandSheet: () -> Unit, modifier: Modifier = Modifier, ) { feedEntryComponent.BottomSheetContent( bottomSheetState = bottomSheetState, onHeaderSizeChange = onHeaderSizeChange, + onExpandSheet = onExpandSheet, modifier = modifier, ) } 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 23de72403e..e033dbe5ac 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 @@ -95,7 +95,7 @@ internal fun WalletScreen( state: WalletScreenState, tangemPayComponent: TangemPayMainBlockComponent, promoBannersBlockComponent: ComposableContentComponent? = null, - bottomSheetContent: @Composable (() -> Unit), + bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, ) { @@ -139,7 +139,7 @@ private fun WalletContent( promoBannersBlockComponent: ComposableContentComponent? = null, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, - bottomSheetContent: @Composable (() -> Unit), + bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit, ) { /* * Don't pass key to remember, because it will brake scroll animation. @@ -295,7 +295,7 @@ private inline fun BaseScaffoldWithMarkets( snackbarHostState: SnackbarHostState, bottomSheetHeaderHeightProvider: () -> Dp, noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, - crossinline bottomSheetContent: @Composable () -> Unit, + crossinline bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit, crossinline content: @Composable (PaddingValues) -> Unit, ) { val isKeyboardVisible by rememberIsKeyboardVisible() @@ -382,7 +382,7 @@ private inline fun BaseScaffoldWithMarkets( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() - // expand bottom sheet when clicked on the header + // expand bottom sheet when clicked on the drag handle .clickable( enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded, indication = null, @@ -400,7 +400,9 @@ private inline fun BaseScaffoldWithMarkets( isSearchFieldFocused = it.isFocused }, ) { - bottomSheetContent() + bottomSheetContent { + coroutineScope.launch { bottomSheetState.expand() } + } } } }, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt index 376948e5ae..e9a58bd659 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/WalletScreen2.kt @@ -86,7 +86,7 @@ internal fun WalletScreen2( state: WalletScreenState, tangemPayComponent: TangemPayMainBlockComponent, modifier: Modifier = Modifier, - bottomSheetContent: @Composable (() -> Unit), + bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, ) { @@ -162,7 +162,7 @@ private fun WalletContent2( modifier: Modifier = Modifier, bottomSheetHeaderHeightProvider: () -> Dp, onBottomSheetStateChange: (BottomSheetState) -> Unit, - bottomSheetContent: @Composable (() -> Unit), + bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit, ) { val density = LocalDensity.current val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() } @@ -343,7 +343,7 @@ private inline fun BaseScaffoldWithMarkets( modifier: Modifier = Modifier, noinline onBottomSheetStateChange: (BottomSheetState) -> Unit, crossinline appBarContent: @Composable () -> Unit, - crossinline bottomSheetContent: @Composable () -> Unit, + crossinline bottomSheetContent: @Composable (onExpandSheet: () -> Unit) -> Unit, crossinline content: @Composable (PaddingValues, TangemSheetState) -> Unit, ) { val density = LocalDensity.current @@ -384,7 +384,9 @@ private inline fun BaseScaffoldWithMarkets( isSearchFieldFocused = focusState.isFocused }, ) { - bottomSheetContent() + bottomSheetContent { + coroutineScope.launch { bottomSheetState.expand() } + } } }, content = { paddingValues -> @@ -456,7 +458,7 @@ private fun BottomSheet( Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - // expand bottom sheet when clicked on the header + // expand bottom sheet when clicked on the drag handle .clickable( enabled = bottomSheetState.currentValue == TangemSheetValue.PartiallyExpanded, indication = null, From d5af712b2bedb0246349ce0124243d41ff43b779 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 20:41:16 +0500 Subject: [PATCH 08/12] Updated on 2026-08-14 --- .../com/tangem/feature/swap/ui/TransactionCard.kt | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index d511cdb1ce..65aad222b7 100644 --- a/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/impl/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -225,8 +225,7 @@ private fun TransactionCardLoading(modifier: Modifier = Modifier) { horizontalAlignment = Alignment.Start, verticalArrangement = Arrangement.spacedBy(6.dp), ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, + Column( modifier = Modifier.fillMaxWidth(), ) { TextShimmer( @@ -278,7 +277,7 @@ private fun TransactionCardLoading(modifier: Modifier = Modifier) { @Composable private fun Header(type: TransactionCardType, balance: String, modifier: Modifier = Modifier) { - Row( + Column( modifier = modifier .fillMaxWidth() .padding( @@ -288,8 +287,6 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie end = TangemTheme.dimens.spacing12, ) .testTag(SwapTokenScreenTestTags.SWAP_BLOCK_HEADER), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, ) { val titleColor = if (type.inputError is TransactionCardType.InputError.Empty) { TangemTheme.colors.text.tertiary @@ -310,9 +307,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie text = balanceText, color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, - modifier = Modifier - .align(Alignment.CenterVertically) - .testTag(SwapTokenScreenTestTags.BALANCE), + modifier = Modifier.testTag(SwapTokenScreenTestTags.BALANCE), ) } } else { From 27820ec8adea6f7569b473a460276779a3191890 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 May 2026 22:27:57 +0500 Subject: [PATCH 09/12] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 1 + .../tangem/data/pay/di/TangemPayDataModule.kt | 9 ++ .../pay/flow/PaymentAccountStatusFetcher.kt | 6 + .../usecase/SetTangemPayCardLimitUseCase.kt | 5 +- .../usecase/UpdateTangemPayCardNameUseCase.kt | 22 +++ ...faultTangemPayDetailsContainerComponent.kt | 4 + .../TangemPayAddToWalletComponent.kt | 2 +- .../TangemPayCardPageScreenComponent.kt | 2 +- .../TangemPayEditDisplayNameComponent.kt | 4 +- .../TangemPayCardDetailsBlockComponent.kt | 2 +- .../TangemPayCardDetailsBlockStateFactory.kt | 39 +++-- .../entity/TangemPayDetailsStateFactory.kt | 1 + .../tangempay/entity/TangemPayDetailsUM.kt | 14 +- .../entity/TangemPayEditDisplayNameUM.kt | 6 +- .../model/TangemPayCardDetailsBlockModel.kt | 39 +++-- .../tangempay/model/TangemPayDetailsModel.kt | 52 ++++++- .../model/TangemPayEditDisplayNameModel.kt | 60 ++++++-- .../DetailsAddToWalletBannerTransformer.kt | 22 +++ ...ngemPayCardDetailsUpdateNameTransformer.kt | 15 ++ .../TangemPayAccountDetailsInnerRoute.kt | 3 + .../tangempay/ui/TangemPayCardDetailsBlock.kt | 139 +++++++++--------- .../tangempay/ui/TangemPayCardPageScreen.kt | 31 ++-- .../tangempay/ui/TangemPayDetailsScreen.kt | 18 ++- .../ui/TangemPayEditDisplayNameScreen.kt | 2 +- 24 files changed, 356 insertions(+), 142 deletions(-) create mode 100644 domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/UpdateTangemPayCardNameUseCase.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsAddToWalletBannerTransformer.kt create mode 100644 features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDetailsUpdateNameTransformer.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 9e3feaa6e5..e20ccf50f8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -1690,6 +1690,7 @@ Withdraw unavailable now You can\'t initiate swap or new withdrawal till the current one is finished Withdrawal in progress + Card name Set a limit from %s We couldn’t set the limit. Please try again Change 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 7f41dabc9c..ade825296a 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 @@ -29,6 +29,7 @@ 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.SetTangemPayCardLimitUseCase +import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase import com.tangem.domain.tangempay.GetTangemPayCurrencyStatusUseCase import com.tangem.domain.tangempay.GetTangemPayCustomerIdUseCase import com.tangem.domain.tangempay.TangemPayWithdrawUseCase @@ -148,6 +149,14 @@ internal interface TangemPayDataModule { return SetTangemPayCardLimitUseCase(cardDetailsRepository, paymentAccountStatusFetcher) } + @Provides + fun provideUpdateTangemPayCardNameUseCase( + cardDetailsRepository: TangemPayCardDetailsRepository, + paymentAccountStatusFetcher: PaymentAccountStatusFetcher, + ): UpdateTangemPayCardNameUseCase { + return UpdateTangemPayCardNameUseCase(cardDetailsRepository, paymentAccountStatusFetcher) + } + @Provides @Singleton fun provideGetTangemPayCryptoCurrencyStatusUseCase( diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt index 740d9d0824..eed0daaec2 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/flow/PaymentAccountStatusFetcher.kt @@ -1,8 +1,14 @@ package com.tangem.domain.pay.flow +import arrow.core.Either import com.tangem.domain.core.flow.FlowFetcher import com.tangem.domain.models.wallet.UserWalletId interface PaymentAccountStatusFetcher : FlowFetcher { + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + return invoke(Params(userWalletId)) + } + data class Params(val userWalletId: UserWalletId) } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTangemPayCardLimitUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTangemPayCardLimitUseCase.kt index 9c66f8d75e..b3fd1ebb56 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTangemPayCardLimitUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/SetTangemPayCardLimitUseCase.kt @@ -17,9 +17,6 @@ class SetTangemPayCardLimitUseCase( amount: BigDecimal, ): Either { return cardDetailsRepository.updateCardLimit(cardId, userWalletId, amount.toPlainString()) - .onRight { - val params = PaymentAccountStatusFetcher.Params(userWalletId) - paymentAccountStatusFetcher.invoke(params) - } + .onRight { paymentAccountStatusFetcher.invoke(userWalletId) } } } \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/UpdateTangemPayCardNameUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/UpdateTangemPayCardNameUseCase.kt new file mode 100644 index 0000000000..5020313889 --- /dev/null +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/UpdateTangemPayCardNameUseCase.kt @@ -0,0 +1,22 @@ +package com.tangem.domain.pay.usecase + +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.flow.PaymentAccountStatusFetcher +import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository + +class UpdateTangemPayCardNameUseCase( + private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher, +) { + suspend operator fun invoke( + cardId: String, + userWalletId: UserWalletId, + displayName: CardDisplayName, + ): Either { + return cardDetailsRepository.updateCardDisplayName(cardId, userWalletId, displayName) + .onRight { paymentAccountStatusFetcher(userWalletId) } + } +} \ No newline at end of file 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 52b0069572..94de672f75 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 @@ -69,6 +69,10 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru context = childByContext(componentContext = componentContext, router = innerRouter), params = TangemPayCardPageComponent.Params(userWalletId = params.userWalletId, config = params.config), ) + TangemPayAccountDetailsInnerRoute.AddToWallet -> TangemPayAddToWalletComponent( + appComponentContext = childByContext(componentContext = componentContext, router = innerRouter), + params = params, + ) } 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 91d394396e..f04ea0b8de 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 @@ -25,7 +25,7 @@ internal class TangemPayAddToWalletComponent( appComponentContext = child("cardDetailsBlockComponent"), params = TangemPayCardDetailsBlockComponent.Params( params = params, - isDisplayCardNameEnabled = false, + isEditingNameEnabled = false, ), ) 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 9d3e29f799..87a347f661 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 @@ -39,7 +39,7 @@ internal class TangemPayCardPageScreenComponent( appComponentContext = child("cardDetailsBlockComponent"), params = TangemPayCardDetailsBlockComponent.Params( params = containerParams, - isDisplayCardNameEnabled = true, + isEditingNameEnabled = true, ), ) 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 index 8b4bb6214d..571c294824 100644 --- 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 @@ -24,7 +24,7 @@ internal class TangemPayEditDisplayNameComponent( private val cardDetailsBlockComponent = DefaultTangemPayCardDetailsBlockComponent( appComponentContext = child("editDisplayNameCardDetails"), - params = TangemPayCardDetailsBlockComponent.Params(params = params, isDisplayCardNameEnabled = true), + params = TangemPayCardDetailsBlockComponent.Params(params = params, isEditingNameEnabled = false), ) @Composable @@ -33,7 +33,7 @@ internal class TangemPayEditDisplayNameComponent( val cardDetailsState by cardDetailsBlockComponent.state.collectAsStateWithLifecycle() val editingCardDetailsState = cardDetailsState.copy( displayNameState = DisplayNameState.Editing( - displayName = state.editingValue, + displayName = state.editingValue.text, editingValue = state.editingValue, onValueChanged = state.onValueChanged, onSubmit = state.onDoneClick, 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 9b80de3eb5..c877c6cddb 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 @@ -16,6 +16,6 @@ internal interface TangemPayCardDetailsBlockComponent { data class Params( val params: TangemPayDetailsContainerComponent.Params, - val isDisplayCardNameEnabled: Boolean, + val isEditingNameEnabled: Boolean, ) } \ 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 d57030a177..698d35ec97 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 @@ -1,6 +1,7 @@ package com.tangem.features.tangempay.entity import com.tangem.core.ui.extensions.resourceReference +import com.tangem.domain.models.account.CardDisplayName import com.tangem.domain.visa.model.TangemPayCardFrozenState import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.model.CardDataType @@ -8,21 +9,33 @@ import com.tangem.utils.StringsSigns internal class TangemPayCardDetailsBlockStateFactory( private val cardNumberEnd: String, - private val displayNameState: DisplayNameState?, + private val displayName: CardDisplayName?, + private val isEditingNameEnabled: Boolean, + private val onEditNameClick: () -> Unit, private val onReveal: () -> Unit, private val onCopy: (String, CardDataType) -> Unit, ) { - fun getInitialState() = TangemPayCardDetailsUM( - number = "", - numberShort = "${StringsSigns.ASTERISK}$cardNumberEnd", - expiry = "", - cvv = "", - buttonText = resourceReference(R.string.tangempay_card_details_reveal_text), - onClick = onReveal, - onCopy = onCopy, - isHidden = true, - cardFrozenState = TangemPayCardFrozenState.Unfrozen, - displayNameState = displayNameState, - ) + fun getInitialState(): TangemPayCardDetailsUM { + return TangemPayCardDetailsUM( + number = "", + numberShort = "${StringsSigns.ASTERISK}$cardNumberEnd", + expiry = "", + cvv = "", + buttonText = resourceReference(R.string.tangempay_card_details_reveal_text), + onClick = onReveal, + onCopy = onCopy, + isHidden = true, + cardFrozenState = TangemPayCardFrozenState.Unfrozen, + displayNameState = if (displayName != null) { + DisplayNameState.Display( + displayName = displayName.value, + onClick = onEditNameClick, + isEditingEnabled = isEditingNameEnabled, + ) + } else { + null + }, + ) + } } \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt index ea86449252..2f630968ac 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/entity/TangemPayDetailsStateFactory.kt @@ -59,6 +59,7 @@ internal class TangemPayDetailsStateFactory( ), isBalanceHidden = false, addFundsEnabled = true, + addToWalletBlockState = null, accountDeactivatedNotificationConfig = NotificationConfig( title = resourceReference(R.string.tangempay_account_deactivated_message_title), subtitle = resourceReference(R.string.tangempay_account_deactivated_message_subtitle), 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 b6b591544f..50fb7a2683 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 @@ -1,5 +1,6 @@ package com.tangem.features.tangempay.entity +import androidx.compose.ui.text.input.TextFieldValue import com.tangem.core.ui.components.buttons.actions.ActionButtonConfig import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.notifications.NotificationConfig @@ -12,6 +13,7 @@ internal data class TangemPayDetailsUM( val topBarConfig: TangemPayDetailsTopBarConfig, val pullToRefreshConfig: PullToRefreshConfig, val balanceBlockState: TangemPayDetailsBalanceBlockState, + val addToWalletBlockState: AddToWalletBlockState?, val isBalanceHidden: Boolean, val addFundsEnabled: Boolean, val accountDeactivatedNotificationConfig: NotificationConfig?, @@ -39,15 +41,23 @@ internal sealed interface DisplayNameState { data class Display( override val displayName: String, val onClick: () -> Unit, + val isEditingEnabled: Boolean, ) : DisplayNameState data class Editing( override val displayName: String, - val editingValue: String, - val onValueChanged: (String) -> Unit, + val editingValue: TextFieldValue, + val onValueChanged: (TextFieldValue) -> Unit, val onSubmit: () -> Unit, val onDismiss: () -> Unit, ) : DisplayNameState + + fun copySealed(displayName: String): DisplayNameState { + return when (this) { + is Display -> copy(displayName = displayName) + is Editing -> copy(displayName = displayName) + } + } } internal sealed class TangemPayDetailsBalanceBlockState { 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 index a13fdbc1d0..8d4c09a816 100644 --- 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 @@ -1,9 +1,11 @@ package com.tangem.features.tangempay.entity +import androidx.compose.ui.text.input.TextFieldValue + internal data class TangemPayEditDisplayNameUM( - val editingValue: String, + val editingValue: TextFieldValue, val isLoading: Boolean, - val onValueChanged: (String) -> Unit, + val onValueChanged: (TextFieldValue) -> 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 9bcf1fcdd1..4e97b58c7d 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 @@ -10,11 +10,16 @@ import com.tangem.core.decompose.ui.UiMessageSender import com.tangem.core.ui.clipboard.ClipboardManager import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.domain.models.StatusSource +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.account.hasCardWithId +import com.tangem.domain.models.account.requireCardWithId +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier 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 @@ -22,10 +27,12 @@ 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.TangemPayCardDetailsUpdateNameTransformer import com.tangem.features.tangempay.navigation.TangemPayCardDetailsInnerRoute import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.JobHolder import com.tangem.utils.coroutines.saveIn +import com.tangem.utils.transformer.update import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch @@ -46,20 +53,16 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( private val cardDetailsEventListener: CardDetailsEventListener, private val analytics: AnalyticsEventHandler, private val router: Router, + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, ) : 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 - }, + displayName = params.params.config.displayName, + isEditingNameEnabled = params.isEditingNameEnabled, + onEditNameClick = ::startEditingDisplayName, onReveal = ::revealCardDetails, onCopy = ::copyData, ) @@ -71,6 +74,7 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( private val showCardDetailsTimerJobHolder = JobHolder() init { + subscribeToCardNameChanges(cardId = params.params.config.cardId, userWalletId = params.params.userWalletId) subscribeToCardFrozenState() modelScope.launch { cardDetailsEventListener.event.collectLatest { event -> @@ -82,6 +86,23 @@ internal class TangemPayCardDetailsBlockModel @Inject constructor( } } + private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) { + paymentAccountStatusSupplier.invoke(userWalletId) + .onEach { state -> + val status = state.value + if (status is PaymentAccountStatusValue.Loaded && + status.source == StatusSource.ACTUAL && + status.hasCardWithId(cardId) + ) { + val card = status.requireCardWithId(cardId) + val displayName = card.displayName ?: return@onEach + + uiState.update(TangemPayCardDetailsUpdateNameTransformer(displayName)) + } + } + .launchIn(modelScope) + } + private fun subscribeToCardFrozenState() { cardDetailsRepository .cardFrozenState(params.params.config.cardId) diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt index a500999cdc..dded4437b7 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/TangemPayDetailsModel.kt @@ -39,10 +39,7 @@ import com.tangem.features.tangempay.entity.TangemPayDetailsStateFactory import com.tangem.features.tangempay.entity.TangemPayDetailsUM import com.tangem.features.tangempay.model.listener.CardDetailsEvent import com.tangem.features.tangempay.model.listener.CardDetailsEventListener -import com.tangem.features.tangempay.model.transformers.DetailBalanceVisibilityTransformer -import com.tangem.features.tangempay.model.transformers.DetailsBalanceTransformer -import com.tangem.features.tangempay.model.transformers.TangemPayDetailsRefreshTransformer -import com.tangem.features.tangempay.model.transformers.TangemPayFreezeUnfreezeStateTransformer +import com.tangem.features.tangempay.model.transformers.* import com.tangem.features.tangempay.navigation.TangemPayAccountDetailsInnerRoute import com.tangem.features.tangempay.utils.TangemPayDetailIntents import com.tangem.features.tangempay.utils.TangemPayMessagesFactory @@ -56,7 +53,10 @@ import com.tangem.utils.coroutines.saveIn import com.tangem.utils.logging.TangemLogger import com.tangem.utils.transformer.update import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject @@ -100,6 +100,7 @@ internal class TangemPayDetailsModel @Inject constructor( private val refreshStateJobHolder = JobHolder() private val fetchBalanceJobHolder = JobHolder() + private val addToWalletBannerJobHolder = JobHolder() private var balance: TangemPayCardBalance? = null @@ -116,6 +117,7 @@ internal class TangemPayDetailsModel @Inject constructor( fetchBalance() if (!params.config.isTangemPayDeactivated) { subscribeToCardFrozenState() + fetchAddToWalletBanner() } } @@ -231,6 +233,24 @@ internal class TangemPayDetailsModel @Inject constructor( }.saveIn(fetchBalanceJobHolder) } + private fun fetchAddToWalletBanner() { + modelScope.launch { + val isDone = try { + cardDetailsRepository.isAddToWalletDone(params.userWalletId).getOrNull() == true + } catch (e: Exception) { + TangemLogger.e("Error", e) + return@launch + } + uiState.update( + transformer = DetailsAddToWalletBannerTransformer( + onClickBanner = ::onClickAddToWalletBlock, + onClickCloseBanner = ::onClickCloseAddToWalletBlock, + isDone = isDone, + ), + ) + }.saveIn(addToWalletBannerJobHolder) + } + private fun handleBalanceHiding() { getBalanceHidingSettingsUseCase().onEach { uiState.update(DetailBalanceVisibilityTransformer(isHidden = it.isBalanceHidden)) @@ -260,6 +280,28 @@ internal class TangemPayDetailsModel @Inject constructor( }.saveIn(refreshStateJobHolder) } + private fun onClickAddToWalletBlock() { + analytics.send(TangemPayAnalyticsEvents.AddToWalletClicked()) + router.push(TangemPayAccountDetailsInnerRoute.AddToWallet) + } + + private fun onClickCloseAddToWalletBlock() { + modelScope.launch { + try { + cardDetailsRepository.setAddToWalletAsDone(params.userWalletId) + } catch (e: Exception) { + TangemLogger.e("Error", e) + } + uiState.update( + transformer = DetailsAddToWalletBannerTransformer( + onClickBanner = ::onClickAddToWalletBlock, + onClickCloseBanner = ::onClickCloseAddToWalletBlock, + isDone = true, + ), + ) + }.saveIn(addToWalletBannerJobHolder) + } + private fun onOpenMenu() { analytics.send(TangemPayAnalyticsEvents.CardSettingsClicked()) } 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 index f0ed2d6e98..352d5d1373 100644 --- 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 @@ -1,6 +1,8 @@ package com.tangem.features.tangempay.model import androidx.compose.runtime.Stable +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import com.tangem.core.decompose.di.ModelScoped import com.tangem.core.decompose.model.Model import com.tangem.core.decompose.model.ParamsContainer @@ -8,15 +10,19 @@ 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.StatusSource import com.tangem.domain.models.account.CardDisplayName -import com.tangem.domain.pay.repository.TangemPayCardDetailsRepository +import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.account.hasCardWithId +import com.tangem.domain.models.account.requireCardWithId +import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.pay.flow.PaymentAccountStatusSupplier +import com.tangem.domain.pay.usecase.UpdateTangemPayCardNameUseCase 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.flow.* import kotlinx.coroutines.launch import javax.inject.Inject @@ -26,8 +32,9 @@ internal class TangemPayEditDisplayNameModel @Inject constructor( paramsContainer: ParamsContainer, override val dispatchers: CoroutineDispatcherProvider, private val router: Router, - private val cardDetailsRepository: TangemPayCardDetailsRepository, + private val updateCardNameUseCase: UpdateTangemPayCardNameUseCase, private val uiMessageSender: UiMessageSender, + private val paymentAccountStatusSupplier: PaymentAccountStatusSupplier, ) : Model() { private val params: TangemPayDetailsContainerComponent.Params = paramsContainer.require() @@ -37,7 +44,10 @@ internal class TangemPayEditDisplayNameModel @Inject constructor( val uiState: StateFlow field = MutableStateFlow( TangemPayEditDisplayNameUM( - editingValue = originalDisplayName, + editingValue = TextFieldValue( + text = originalDisplayName, + selection = TextRange(originalDisplayName.length), + ), isLoading = false, onValueChanged = ::onValueChanged, onDoneClick = ::onDoneClick, @@ -45,23 +55,51 @@ internal class TangemPayEditDisplayNameModel @Inject constructor( ), ) - private fun onValueChanged(value: String) { - if (value.length <= CardDisplayName.MAX_LENGTH) { + init { + subscribeToCardNameChanges(params.config.cardId, params.userWalletId) + } + + private fun subscribeToCardNameChanges(cardId: String, userWalletId: UserWalletId) { + paymentAccountStatusSupplier.invoke(userWalletId) + .onEach { state -> + val status = state.value + if (status is PaymentAccountStatusValue.Loaded && + status.source == StatusSource.ACTUAL && + status.hasCardWithId(cardId) + ) { + val card = status.requireCardWithId(cardId) + val displayName = card.displayName ?: return@onEach + + uiState.update { uiState -> + uiState.copy( + editingValue = TextFieldValue( + text = displayName.value, + selection = TextRange(displayName.value.length), + ), + ) + } + } + } + .launchIn(modelScope) + } + + private fun onValueChanged(value: TextFieldValue) { + if (value.text.length <= CardDisplayName.MAX_LENGTH) { uiState.update { it.copy(editingValue = value) } } } private fun onDoneClick() { - val currentValue = uiState.value.editingValue + val currentValue = uiState.value.editingValue.text if (currentValue.trim() == originalDisplayName.trim()) { router.pop() return } CardDisplayName(currentValue) .onRight { cardDisplayName -> - uiState.update { it.copy(isLoading = true) } modelScope.launch { - cardDetailsRepository.updateCardDisplayName( + uiState.update { it.copy(isLoading = true) } + updateCardNameUseCase( cardId = params.config.cardId, userWalletId = params.userWalletId, displayName = cardDisplayName, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsAddToWalletBannerTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsAddToWalletBannerTransformer.kt new file mode 100644 index 0000000000..3fe7bddbf8 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/DetailsAddToWalletBannerTransformer.kt @@ -0,0 +1,22 @@ +package com.tangem.features.tangempay.model.transformers + +import com.tangem.features.tangempay.entity.AddToWalletBlockState +import com.tangem.features.tangempay.entity.TangemPayDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class DetailsAddToWalletBannerTransformer( + private val onClickBanner: () -> Unit, + private val onClickCloseBanner: () -> Unit, + private val isDone: Boolean, +) : Transformer { + + override fun transform(prevState: TangemPayDetailsUM): TangemPayDetailsUM { + return prevState.copy( + addToWalletBlockState = if (isDone) { + null + } else { + AddToWalletBlockState(onClick = onClickBanner, onClickClose = onClickCloseBanner) + }, + ) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDetailsUpdateNameTransformer.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDetailsUpdateNameTransformer.kt new file mode 100644 index 0000000000..9c7df001c2 --- /dev/null +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/model/transformers/TangemPayCardDetailsUpdateNameTransformer.kt @@ -0,0 +1,15 @@ +package com.tangem.features.tangempay.model.transformers + +import com.tangem.domain.models.account.CardDisplayName +import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM +import com.tangem.utils.transformer.Transformer + +internal class TangemPayCardDetailsUpdateNameTransformer( + private val displayName: CardDisplayName, +) : Transformer { + + override fun transform(prevState: TangemPayCardDetailsUM): TangemPayCardDetailsUM { + val displayNameState = prevState.displayNameState ?: return prevState + return prevState.copy(displayNameState = displayNameState.copySealed(displayName = displayName.value)) + } +} \ No newline at end of file diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt index 6bc9172607..8ef5c5e9a3 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/navigation/TangemPayAccountDetailsInnerRoute.kt @@ -10,4 +10,7 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route { @Serializable data object CardDetails : TangemPayAccountDetailsInnerRoute() + + @Serializable + data object AddToWallet : TangemPayAccountDetailsInnerRoute() } \ No newline at end of file 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 efe8c8a8dd..0a58c2672a 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,24 +1,22 @@ 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.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -46,6 +44,7 @@ import com.tangem.core.ui.components.SpacerWMax 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.extensions.conditional import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringResourceSafe import com.tangem.core.ui.res.TangemTheme @@ -58,7 +57,7 @@ import com.tangem.features.tangempay.entity.DisplayNameState import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM import com.tangem.features.tangempay.model.CardDataType -private const val ICON_FADE_DURATION_MS = 300 +private const val TEXT_WIDTH_PADDING = 2 private val CustomCardBlockColor = Color(0x1F828282) @Suppress("MagicNumber") @@ -228,80 +227,55 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif @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, - ) - } - } + when (state) { + is DisplayNameState.Display -> DisplayOnlyCardDisplayName(modifier = modifier, state = state) + is DisplayNameState.Editing -> EditingCardDisplayName(modifier = modifier, state = state) } } @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, - ) + Row( + modifier = modifier.conditional( + condition = state.isEditingEnabled, + modifier = { clickable(onClick = state.onClick) }, + ), + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = state.displayName, + style = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite), + maxLines = 1, + ) + if (state.isEditingEnabled) { + Icon( + painter = painterResource(id = R.drawable.ic_edit_new_12), + contentDescription = null, + modifier = Modifier.size(10.dp), + tint = TangemTheme.colors.text.constantWhite, + ) + } + } } @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 placeholder = stringResourceSafe(R.string.tangempay_card_edit_name_placeholder) val textStyle = TangemTheme.typography.caption1.copy(color = TangemTheme.colors.text.constantWhite) val textMeasurer = rememberTextMeasurer() + val measuredText = state.editingValue.text.ifEmpty { placeholder } val textWidthDp = with(LocalDensity.current) { - textMeasurer.measure(textFieldValue.text, textStyle).size.width.toDp() + 2.dp + textMeasurer.measure(measuredText, textStyle).size.width.toDp() + TEXT_WIDTH_PADDING.dp } BasicTextField( - value = textFieldValue, + value = state.editingValue, onValueChange = { newValue -> - if (newValue.text.length in 0..CardDisplayName.MAX_LENGTH) { - textFieldValue = newValue - state.onValueChanged(newValue.text) + if (newValue.text.length <= CardDisplayName.MAX_LENGTH) { + state.onValueChanged(newValue) } }, modifier = modifier @@ -312,6 +286,17 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo cursorBrush = SolidColor(TangemTheme.colors.text.constantWhite), keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), keyboardActions = KeyboardActions(onDone = { state.onSubmit() }), + decorationBox = { innerTextField -> + Box { + if (state.editingValue.text.isEmpty()) { + Text( + text = placeholder, + style = textStyle.copy(color = TangemTheme.colors.text.tertiary), + ) + } + innerTextField() + } + }, ) LaunchedEffect(Unit) { focusRequester.requestFocus() } @@ -492,7 +477,13 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide onCopy = { _, _ -> }, isHidden = true, cardFrozenState = TangemPayCardFrozenState.Frozen, - displayNameState = DisplayNameState.Display(displayName = "Tangem", onClick = {}), + displayNameState = DisplayNameState.Editing( + displayName = "Tangem", + editingValue = TextFieldValue(text = "movet", selection = TextRange("movet".length)), + onValueChanged = {}, + onSubmit = {}, + onDismiss = {}, + ), ), TangemPayCardDetailsUM( isLoading = false, @@ -505,7 +496,13 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide onCopy = { _, _ -> }, isHidden = true, cardFrozenState = TangemPayCardFrozenState.Unfrozen, - displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), + displayNameState = DisplayNameState.Editing( + displayName = "Tangem", + editingValue = TextFieldValue(text = ""), + onValueChanged = {}, + onSubmit = {}, + onDismiss = {}, + ), ), TangemPayCardDetailsUM( isLoading = false, @@ -518,7 +515,11 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide onCopy = { _, _ -> }, isHidden = true, cardFrozenState = TangemPayCardFrozenState.Pending, - displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), + displayNameState = DisplayNameState.Display( + displayName = "Tangem Pay Card", + onClick = {}, + isEditingEnabled = true, + ), ), TangemPayCardDetailsUM( isLoading = false, @@ -531,7 +532,11 @@ private class TangemPayCardDetailsUMProvider : CollectionPreviewParameterProvide expiry = "12/34", cvv = "123", cardFrozenState = TangemPayCardFrozenState.Unfrozen, - displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), + displayNameState = DisplayNameState.Display( + displayName = "Tangem Pay Card", + onClick = {}, + isEditingEnabled = false, + ), ), ), ) \ 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 4b8dbc7446..5217c1be53 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 @@ -8,18 +8,7 @@ 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 -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.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyListScope @@ -46,11 +35,7 @@ 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 com.tangem.features.tangempay.entity.TangemPayDailyLimitBlockState +import com.tangem.features.tangempay.entity.* import kotlinx.collections.immutable.ImmutableList private const val CONTENT_FADE_DURATION_MS = 300 @@ -217,7 +202,11 @@ private fun preview() = TangemThemePreview { onCopy = { _, _ -> }, onClick = {}, cardFrozenState = TangemPayCardFrozenState.Unfrozen, - displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), + displayNameState = DisplayNameState.Display( + displayName = "Tangem Pay Card", + onClick = {}, + isEditingEnabled = true, + ), ), ), cardDetailsState = TangemPayCardDetailsUM( @@ -228,7 +217,11 @@ private fun preview() = TangemThemePreview { onCopy = { _, _ -> }, onClick = {}, cardFrozenState = TangemPayCardFrozenState.Unfrozen, - displayNameState = DisplayNameState.Display(displayName = "Tangem Pay Card", onClick = {}), + displayNameState = DisplayNameState.Display( + displayName = "Tangem Pay Card", + onClick = {}, + isEditingEnabled = false, + ), ), ) } \ 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 2349905b14..c6f426ec27 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 @@ -56,10 +56,7 @@ import com.tangem.features.tangempay.components.express.PreviewEmptyExpressTrans import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent import com.tangem.features.tangempay.details.impl.R -import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState -import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig -import com.tangem.features.tangempay.entity.TangemPayDetailsUM -import com.tangem.features.tangempay.entity.TangemPayTxHistoryUM +import com.tangem.features.tangempay.entity.* import com.tangem.features.tokendetails.ExpressTransactionsComponent import com.tangem.utils.StringsSigns.DASH_SIGN import kotlinx.collections.immutable.persistentListOf @@ -116,6 +113,17 @@ internal fun TangemPayDetailsScreen( SpacerH12() }, ) + if (state.addToWalletBlockState != null) { + item( + key = AddToWalletBlockState::class.java, + content = { + TangemPayAddToWalletBlock( + state = state.addToWalletBlockState, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), + ) + }, + ) + } if (state.accountDeactivatedNotificationConfig != null) { item( key = "DEACTIVATION_MESSAGE", @@ -420,6 +428,7 @@ private class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider Date: Wed, 6 May 2026 22:27:01 +0200 Subject: [PATCH 10/12] Updated on 2026-08-14 --- .../format/bigdecimal/BigDecimalFiatFormat.kt | 40 ++++++++++++++----- .../converter/MarketsTokenItemConverter.kt | 16 +------- 2 files changed, 30 insertions(+), 26 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt index e8eb6add6d..7629ed6986 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/format/bigdecimal/BigDecimalFiatFormat.kt @@ -98,13 +98,22 @@ fun BigDecimalFiatFormatStyled.defaultAmount(spanStyleReference: SpanStyleRefere } val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator - val formattedAmount = formatter.format(formattingAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + val currencySymbol = formatterCurrency.getSymbol(locale) + val rawFormatted = formatter.format(formattingAmount) - val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + val separatorIndex = decimalSeparator?.let { rawFormatted.indexOf(it).takeIf { i -> i >= 0 } } + ?: rawFormatted.length - val wholePart = formattedAmount.take(separatorIndex) - val fractionalPart = formattedAmount.drop(separatorIndex) + val formattedAmount = rawFormatted.replace(currencySymbol, fiatCurrencySymbol) + val offset = fiatCurrencySymbol.length - currencySymbol.length + val adjustedIndex = if (rawFormatted.indexOf(currencySymbol) in 0 until separatorIndex) { + separatorIndex + offset + } else { + separatorIndex + } + + val wholePart = formattedAmount.take(adjustedIndex) + val fractionalPart = formattedAmount.drop(adjustedIndex) combinedReference( if (formattingAmount.isLessThanThreshold()) stringReference(CAN_BE_LOWER_SIGN) else TextReference.EMPTY, @@ -192,13 +201,22 @@ private fun BigDecimalFiatFormatStyled.price(spanStyleReference: SpanStyleRefere } val decimalSeparator = (formatter as? DecimalFormat)?.decimalFormatSymbols?.decimalSeparator - val formattedAmount = formatter.format(priceAmount) - .replace(formatterCurrency.getSymbol(locale), fiatCurrencySymbol) + val currencySymbol = formatterCurrency.getSymbol(locale) + val rawFormatted = formatter.format(priceAmount) - val separatorIndex = decimalSeparator?.let { formattedAmount.indexOf(it) } ?: formattedAmount.length + val separatorIndex = decimalSeparator?.let { rawFormatted.indexOf(it).takeIf { i -> i >= 0 } } + ?: rawFormatted.length - val wholePart = formattedAmount.take(separatorIndex) - val fractionalPart = formattedAmount.drop(separatorIndex) + val formattedAmount = rawFormatted.replace(currencySymbol, fiatCurrencySymbol) + val offset = fiatCurrencySymbol.length - currencySymbol.length + val adjustedIndex = if (rawFormatted.indexOf(currencySymbol) in 0 until separatorIndex) { + separatorIndex + offset + } else { + separatorIndex + } + + val wholePart = formattedAmount.take(adjustedIndex) + val fractionalPart = formattedAmount.drop(adjustedIndex) combinedReference( stringReference(wholePart), @@ -232,7 +250,7 @@ private fun BigDecimal.isLessThanThreshold() = this > BigDecimal.ZERO && this < * Returns amount with correct scale */ fun getFiatPriceAmountWithScale(value: BigDecimal): Pair { - return if (value > BigDecimal.ZERO && value < BigDecimal.ONE) { + return if (value < BigDecimal.ONE) { val leadingZeroes = value.scale() - value.precision() val scale = leadingZeroes + FRACTIONAL_PART_LENGTH_AFTER_LEADING_ZEROES diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt index f9373e37a1..2d25b64eb9 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/model/converter/MarketsTokenItemConverter.kt @@ -15,7 +15,6 @@ import com.tangem.domain.markets.TokenMarket import com.tangem.features.feed.impl.R import com.tangem.features.feed.model.market.list.state.MarketsListUM import com.tangem.utils.converter.Converter -import com.tangem.utils.logging.TangemLogger import kotlinx.collections.immutable.toImmutableList import java.math.BigDecimal import java.math.RoundingMode @@ -92,16 +91,7 @@ internal class MarketsTokenItemConverter( private fun TokenMarket.getCurrentPrice(prev: TokenMarket? = null): MarketsListItemUM.Price { val prevPrice = prev?.tokenQuotesShort?.currentPrice - - val currentPrice = tokenQuotesShort.currentPrice - if (currentPrice < BigDecimal.ZERO) { - TangemLogger.withTag(MARKETS_PRICE_LOG_TAG).w( - messageString = "Unexpected non-positive price for tokenId=$id, symbol=$symbol, " + - "currency=${appCurrency.code}, value=${currentPrice.toPlainString()}", - ) - } - - val priceText = currentPrice.format { + val priceText = tokenQuotesShort.currentPrice.format { fiat( fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol, @@ -171,8 +161,4 @@ internal class MarketsTokenItemConverter( return percent.format { percent() } } - - private companion object { - const val MARKETS_PRICE_LOG_TAG = "MarketsTokenItemConverter" - } } \ No newline at end of file From 31fbd0872e841031e371eea2078b4615532a1f20 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 May 2026 07:42:36 +0000 Subject: [PATCH 11/12] 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 94aa5668ec..7179ef2644 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-1494" +tangemBlockchainSdk = "releases-5.38-1503" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.37-603" +tangemCardSdk = "releases-5.38-615" #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 7367e7c1b7559f1644cef40dea3cfd5e6c4d4b22 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 7 May 2026 07:53:03 +0000 Subject: [PATCH 12/12] 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 7179ef2644..2e558b3c64 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.38-1503" +tangemBlockchainSdk = "develop-1502" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "releases-5.38-615" +tangemCardSdk = "develop-614" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "tangem-master-21" #tangemVico = "0.0.1" # Keep it! - used for local builds ^