diff --git a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt index 8f1f852eb9..832eab2ad7 100644 --- a/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt +++ b/app/src/main/java/com/tangem/tap/routing/utils/ChildFactory.kt @@ -628,6 +628,7 @@ internal class ChildFactory @Inject constructor( params = SendEntryPointComponent.Params( userWalletId = route.userWalletId, cryptoCurrency = route.currency, + shouldStartWithSwap = route.shouldStartWithSwap, ), componentFactory = sendEntryPointComponentFactory, ) diff --git a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt index 1f04371c14..3beaf7322c 100644 --- a/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt +++ b/common/routing/src/main/kotlin/com/tangem/common/routing/AppRoute.kt @@ -449,6 +449,7 @@ sealed class AppRoute(val path: String) : Route { data class SendEntryPoint( val userWalletId: UserWalletId, val currency: CryptoCurrency, + val shouldStartWithSwap: Boolean = false, ) : AppRoute( path = "/send_entry_point/${userWalletId.stringValue}/${currency.id.value}?", ) diff --git a/common/ui-markets/build.gradle.kts b/common/ui-markets/build.gradle.kts index c665f48b03..4155090cee 100644 --- a/common/ui-markets/build.gradle.kts +++ b/common/ui-markets/build.gradle.kts @@ -44,4 +44,8 @@ dependencies { /** DI */ implementation(deps.hilt.android) kapt(deps.hilt.kapt) + + /** Test */ + testImplementation(projects.test.core) + testImplementation(projects.common.test) } \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionUM.kt index 88b65e767a..bc41c68a0f 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionUM.kt @@ -60,6 +60,24 @@ sealed class QuickActionUM( description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), icon = R.drawable.ic_analytics_up_mini_24, ) + + data object Send : V1( + title = resourceReference(R.string.common_send), + description = resourceReference(R.string.quick_action_send_description), + icon = R.drawable.ic_arrow_up_24, + ) + + data object Sell : V1( + title = resourceReference(R.string.common_sell), + description = resourceReference(R.string.quick_action_sell_description), + icon = R.drawable.ic_currency_24, + ) + + data object SwapAndSend : V1( + title = resourceReference(R.string.common_send_with_swap), + description = resourceReference(R.string.quick_action_send_and_swap_description), + icon = R.drawable.ic_exchange_mini_24, + ) } sealed class V2( @@ -107,5 +125,23 @@ sealed class QuickActionUM( description = resourceReference(R.string.yield_module_main_screen_promo_banner_message, wrappedList(apy)), icon = R.drawable.ic_analytics_up_mini_24, ) + + data object Send : V2( + title = resourceReference(R.string.common_send), + description = resourceReference(R.string.quick_action_send_description), + icon = R.drawable.ic_arrow_up_24, + ) + + data object Sell : V2( + title = resourceReference(R.string.common_sell), + description = resourceReference(R.string.quick_action_sell_description), + icon = R.drawable.ic_currency_24, + ) + + data object SwapAndSend : V2( + title = resourceReference(R.string.common_send_with_swap), + description = resourceReference(R.string.quick_action_send_and_swap_description), + icon = R.drawable.ic_exchange_mini_24, + ) } } \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt index 47f53be359..de1e9704d1 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverter.kt @@ -11,52 +11,16 @@ object QuickActionsConverter { cryptoData: CryptoCurrencyData, tokenActionsHandler: TokenActionsHandler, isRedesignEnabled: Boolean, + context: TokenActionsContext = TokenActionsContext.Markets, ): QuickActions { return QuickActions( - actions = toQuickActions(cryptoData.actions, isRedesignEnabled), + actions = toQuickActions(cryptoData.actions, isRedesignEnabled, context), onQuickActionClick = { quickActionUM -> - when (quickActionUM) { - QuickActionUM.V1.Buy -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Buy, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.V1.Exchange -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Exchange, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.V1.Receive -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Receive, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.V1.Stake -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Stake, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.V1.YieldMode -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.YieldMode, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.V2.Buy -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Buy, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.V2.Exchange -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Exchange, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.V2.Receive -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Receive, - cryptoCurrencyData = cryptoData, - ) - QuickActionUM.V2.Stake -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.Stake, - cryptoCurrencyData = cryptoData, - ) - is QuickActionUM.V2.YieldMode -> tokenActionsHandler.handle( - action = TokenActionsBSContentUM.Action.YieldMode, - cryptoCurrencyData = cryptoData, - ) - } + tokenActionsHandler.handle( + action = quickActionUM.toHandledAction(), + cryptoCurrencyData = cryptoData, + context = context, + ) }, onQuickActionLongClick = { actionUM -> if (actionUM == QuickActionUM.V1.Receive || actionUM == QuickActionUM.V2.Receive) { @@ -69,44 +33,78 @@ object QuickActionsConverter { ) } - fun toQuickActions(actions: List, isRedesignEnabled: Boolean) = - if (isRedesignEnabled) { - redesignedQuickActions(actions) - } else { - legacyQuickActions(actions) - } + private fun QuickActionUM.toHandledAction(): TokenActionsBSContentUM.Action = when (this) { + QuickActionUM.V1.Buy, QuickActionUM.V2.Buy -> TokenActionsBSContentUM.Action.Buy + is QuickActionUM.V1.Exchange, is QuickActionUM.V2.Exchange -> TokenActionsBSContentUM.Action.Exchange + QuickActionUM.V1.Receive, QuickActionUM.V2.Receive -> TokenActionsBSContentUM.Action.Receive + QuickActionUM.V1.Stake, QuickActionUM.V2.Stake -> TokenActionsBSContentUM.Action.Stake + is QuickActionUM.V1.YieldMode, is QuickActionUM.V2.YieldMode -> TokenActionsBSContentUM.Action.YieldMode + QuickActionUM.V1.Send, QuickActionUM.V2.Send -> TokenActionsBSContentUM.Action.Send + QuickActionUM.V1.Sell, QuickActionUM.V2.Sell -> TokenActionsBSContentUM.Action.Sell + QuickActionUM.V1.SwapAndSend, QuickActionUM.V2.SwapAndSend -> TokenActionsBSContentUM.Action.SendWithSwap + } - private fun redesignedQuickActions(actions: List): ImmutableList { - return buildList { - actions.forEach { action -> - if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) { - when (action) { - is TokenActionsState.ActionState.Buy -> QuickActionUM.V2.Buy - is TokenActionsState.ActionState.Swap -> QuickActionUM.V2.Exchange(action.shouldShowBadge) - is TokenActionsState.ActionState.Receive -> QuickActionUM.V2.Receive - is TokenActionsState.ActionState.Stake -> QuickActionUM.V2.Stake - is TokenActionsState.ActionState.YieldMode -> QuickActionUM.V2.YieldMode(action.apy) - else -> null - }?.let(::add) - } + /** + * Returns available actions filtered to [context]'s allow-list and ordered by it. + * Omitting [context] (default [TokenActionsContext.Markets]) yields all available actions in source order; + * a context with a non-null [TokenActionsContext.allowedActionsInOrder] filters to and orders by that list. + */ + fun toQuickActions( + actions: List, + isRedesignEnabled: Boolean, + context: TokenActionsContext = TokenActionsContext.Markets, + ): ImmutableList { + val available = actions.filter { it.unavailabilityReason == ScenarioUnavailabilityReason.None } + val allowed = context.allowedActionsInOrder + ?: return available.mapNotNull { it.toQuickActionUM(isRedesignEnabled) }.toImmutableList() + + val byBsAction = available.associateBy { it.toBsAction() } + val hasExchange = byBsAction.containsKey(TokenActionsBSContentUM.Action.Exchange) + return allowed.mapNotNull { action -> + when (action) { + TokenActionsBSContentUM.Action.SendWithSwap -> + if (hasExchange) swapAndSendUM(isRedesignEnabled) else null + else -> byBsAction[action]?.toQuickActionUM(isRedesignEnabled) } }.toImmutableList() } - private fun legacyQuickActions(actions: List): ImmutableList { - return buildList { - actions.forEach { action -> - if (action.unavailabilityReason == ScenarioUnavailabilityReason.None) { - when (action) { - is TokenActionsState.ActionState.Buy -> QuickActionUM.V1.Buy - is TokenActionsState.ActionState.Swap -> QuickActionUM.V1.Exchange(action.shouldShowBadge) - is TokenActionsState.ActionState.Receive -> QuickActionUM.V1.Receive - is TokenActionsState.ActionState.Stake -> QuickActionUM.V1.Stake - is TokenActionsState.ActionState.YieldMode -> QuickActionUM.V1.YieldMode(action.apy) - else -> null - }?.let(::add) - } - } - }.toImmutableList() + private fun swapAndSendUM(isRedesignEnabled: Boolean): QuickActionUM = + if (isRedesignEnabled) QuickActionUM.V2.SwapAndSend else QuickActionUM.V1.SwapAndSend + + private fun TokenActionsState.ActionState.toQuickActionUM(isRedesignEnabled: Boolean): QuickActionUM? = + if (isRedesignEnabled) toV2() else toV1() + + private fun TokenActionsState.ActionState.toV2(): QuickActionUM? = when (this) { + is TokenActionsState.ActionState.Buy -> QuickActionUM.V2.Buy + is TokenActionsState.ActionState.Swap -> QuickActionUM.V2.Exchange(shouldShowBadge) + is TokenActionsState.ActionState.Receive -> QuickActionUM.V2.Receive + is TokenActionsState.ActionState.Send -> QuickActionUM.V2.Send + is TokenActionsState.ActionState.Sell -> QuickActionUM.V2.Sell + is TokenActionsState.ActionState.Stake -> QuickActionUM.V2.Stake + is TokenActionsState.ActionState.YieldMode -> QuickActionUM.V2.YieldMode(apy) + else -> null + } + + private fun TokenActionsState.ActionState.toV1(): QuickActionUM? = when (this) { + is TokenActionsState.ActionState.Buy -> QuickActionUM.V1.Buy + is TokenActionsState.ActionState.Swap -> QuickActionUM.V1.Exchange(shouldShowBadge) + is TokenActionsState.ActionState.Receive -> QuickActionUM.V1.Receive + is TokenActionsState.ActionState.Send -> QuickActionUM.V1.Send + is TokenActionsState.ActionState.Sell -> QuickActionUM.V1.Sell + is TokenActionsState.ActionState.Stake -> QuickActionUM.V1.Stake + is TokenActionsState.ActionState.YieldMode -> QuickActionUM.V1.YieldMode(apy) + else -> null + } + + private fun TokenActionsState.ActionState.toBsAction(): TokenActionsBSContentUM.Action? = when (this) { + is TokenActionsState.ActionState.Buy -> TokenActionsBSContentUM.Action.Buy + is TokenActionsState.ActionState.Swap -> TokenActionsBSContentUM.Action.Exchange + is TokenActionsState.ActionState.Receive -> TokenActionsBSContentUM.Action.Receive + is TokenActionsState.ActionState.Send -> TokenActionsBSContentUM.Action.Send + is TokenActionsState.ActionState.Sell -> TokenActionsBSContentUM.Action.Sell + is TokenActionsState.ActionState.Stake -> TokenActionsBSContentUM.Action.Stake + is TokenActionsState.ActionState.YieldMode -> TokenActionsBSContentUM.Action.YieldMode + else -> null } } \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsBSContentUM.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsBSContentUM.kt index f33fee7c3d..bcbc87b589 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsBSContentUM.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsBSContentUM.kt @@ -43,6 +43,10 @@ data class TokenActionsBSContentUM( text = resourceReference(R.string.common_exchange), iconRes = R.drawable.ic_exchange_horizontal_24, ), + SendWithSwap( + text = resourceReference(R.string.common_send_with_swap), + iconRes = R.drawable.ic_exchange_horizontal_24, + ), Stake( text = resourceReference(R.string.common_stake), iconRes = R.drawable.ic_staking_24, diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsContext.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsContext.kt new file mode 100644 index 0000000000..15b9d04654 --- /dev/null +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsContext.kt @@ -0,0 +1,32 @@ +package com.tangem.common.ui.markets.action + +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.markets.action.TokenActionsBSContentUM.Action + +/** + * Drives how the shared token-actions bottom sheet behaves per entry point. + * + * @property allowedActionsInOrder explicit allow-list + display order; `null` means "all actions + * in source order" (legacy Markets behaviour). + * @property swapPosition the [AppRoute.Swap.CurrencyPosition] used when the Swap/Exchange action is + * launched from this context. + */ +enum class TokenActionsContext( + val allowedActionsInOrder: List?, + val swapPosition: AppRoute.Swap.CurrencyPosition, +) { + Markets( + allowedActionsInOrder = null, + swapPosition = AppRoute.Swap.CurrencyPosition.ANY, + ), + AddFunds( + allowedActionsInOrder = listOf(Action.Buy, Action.Exchange, Action.Receive), + swapPosition = AppRoute.Swap.CurrencyPosition.TO, + ), + Transfer( + // Action.SendWithSwap has no TokenActionsState.ActionState counterpart and is never produced from the + // domain action list; it is injected as a synthetic row by the converter when Swap is available. + allowedActionsInOrder = listOf(Action.Send, Action.Exchange, Action.SendWithSwap, Action.Sell), + swapPosition = AppRoute.Swap.CurrencyPosition.FROM, + ), +} \ No newline at end of file diff --git a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt index 1680b7253d..3b59bc5471 100644 --- a/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt +++ b/common/ui-markets/src/main/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandler.kt @@ -51,7 +51,11 @@ class TokenActionsHandler @AssistedInject constructor( add(TokenActionsBSContentUM.Action.Sell) } - fun handle(action: TokenActionsBSContentUM.Action, cryptoCurrencyData: CryptoCurrencyData) { + fun handle( + action: TokenActionsBSContentUM.Action, + cryptoCurrencyData: CryptoCurrencyData, + context: TokenActionsContext = TokenActionsContext.Markets, + ) { if (isTopUpBlockedByBackupError(action, cryptoCurrencyData.userWallet)) return onHandleQuickAction( @@ -59,25 +63,37 @@ class TokenActionsHandler @AssistedInject constructor( action = action, cryptoCurrencyData = cryptoCurrencyData, ), - when (action) { - TokenActionsBSContentUM.Action.Receive, - TokenActionsBSContentUM.Action.CopyAddress, - TokenActionsBSContentUM.Action.Sell, - -> false - TokenActionsBSContentUM.Action.Send, - TokenActionsBSContentUM.Action.Stake, - TokenActionsBSContentUM.Action.YieldMode, - TokenActionsBSContentUM.Action.Buy, - TokenActionsBSContentUM.Action.Exchange, - -> true - }, + action.shouldDismissBottomSheet(), ) val userWallet = cryptoCurrencyData.userWallet if (userWallet is UserWallet.Cold && handleDemoMode(action, userWallet)) return + dispatchAction(action, cryptoCurrencyData, context) + } + + private fun TokenActionsBSContentUM.Action.shouldDismissBottomSheet(): Boolean = when (this) { + TokenActionsBSContentUM.Action.Receive, + TokenActionsBSContentUM.Action.CopyAddress, + TokenActionsBSContentUM.Action.Sell, + -> false + TokenActionsBSContentUM.Action.Send, + TokenActionsBSContentUM.Action.Stake, + TokenActionsBSContentUM.Action.YieldMode, + TokenActionsBSContentUM.Action.Buy, + TokenActionsBSContentUM.Action.Exchange, + TokenActionsBSContentUM.Action.SendWithSwap, + -> true + } + + private fun dispatchAction( + action: TokenActionsBSContentUM.Action, + cryptoCurrencyData: CryptoCurrencyData, + context: TokenActionsContext, + ) { when (action) { TokenActionsBSContentUM.Action.Buy -> onBuyClick(cryptoCurrencyData) - TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData) + TokenActionsBSContentUM.Action.Exchange -> onExchangeClick(cryptoCurrencyData, context) + TokenActionsBSContentUM.Action.SendWithSwap -> onSwapAndSendClick(cryptoCurrencyData) TokenActionsBSContentUM.Action.Receive -> Unit TokenActionsBSContentUM.Action.CopyAddress -> onCopyAddress(cryptoCurrencyData) TokenActionsBSContentUM.Action.Sell -> onSellClick(cryptoCurrencyData) @@ -155,12 +171,13 @@ class TokenActionsHandler @AssistedInject constructor( } } - private fun onExchangeClick(cryptoCurrencyData: CryptoCurrencyData) { + private fun onExchangeClick(cryptoCurrencyData: CryptoCurrencyData, context: TokenActionsContext) { router.push( AppRoute.Swap( fromCryptoCurrency = cryptoCurrencyData.status.currency, userWalletId = cryptoCurrencyData.userWallet.walletId, screenSource = AnalyticsParam.ScreensSources.Markets.value, + fromCurrencyPosition = context.swapPosition, ), ) } @@ -173,6 +190,16 @@ class TokenActionsHandler @AssistedInject constructor( router.push(route) } + private fun onSwapAndSendClick(cryptoCurrencyData: CryptoCurrencyData) { + router.push( + AppRoute.SendEntryPoint( + userWalletId = cryptoCurrencyData.userWallet.walletId, + currency = cryptoCurrencyData.status.currency, + shouldStartWithSwap = true, + ), + ) + } + private fun onStakeClick(cryptoCurrencyData: CryptoCurrencyData) { val option = cryptoCurrencyData.actions.firstOrNull { it is TokenActionsState.ActionState.Stake } ?.let { it as TokenActionsState.ActionState.Stake } diff --git a/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverterTest.kt b/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverterTest.kt new file mode 100644 index 0000000000..b0028b644c --- /dev/null +++ b/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/QuickActionsConverterTest.kt @@ -0,0 +1,130 @@ +package com.tangem.common.ui.markets.action + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason +import com.tangem.domain.tokens.model.TokenActionsState +import org.junit.jupiter.api.Test + +internal class QuickActionsConverterTest { + + @Test + fun `GIVEN actions in source order WHEN context is AddFunds THEN buy exchange receive in order with no stake`() { + // Arrange + val actions = listOf( + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None, option = null), + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, shouldShowBadge = false), + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.None), + ) + + // Act + val result = QuickActionsConverter.toQuickActions( + actions = actions, + isRedesignEnabled = true, + context = TokenActionsContext.AddFunds, + ) + + // Assert + assertThat(result).containsExactly( + QuickActionUM.V2.Buy, + QuickActionUM.V2.Exchange(shouldShowBadge = false), + QuickActionUM.V2.Receive, + ).inOrder() + assertThat(result).doesNotContain(QuickActionUM.V2.Stake) + } + + @Test + fun `GIVEN receive and stake WHEN context is Markets THEN preserves source order and keeps stake`() { + // Arrange + val actions = listOf( + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Stake(ScenarioUnavailabilityReason.None, option = null), + ) + + // Act + val result = QuickActionsConverter.toQuickActions( + actions = actions, + isRedesignEnabled = true, + context = TokenActionsContext.Markets, + ) + + // Assert + assertThat(result).containsExactly( + QuickActionUM.V2.Receive, + QuickActionUM.V2.Stake, + ).inOrder() + } + + @Test + fun `GIVEN buy with unavailability reason mixed with available actions WHEN toQuickActions THEN unavailable action is excluded`() { + // Arrange + val actions = listOf( + TokenActionsState.ActionState.Buy(ScenarioUnavailabilityReason.BuyUnavailable(cryptoCurrencyName = "BTC")), + TokenActionsState.ActionState.Receive(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, shouldShowBadge = false), + ) + + // Act + val result = QuickActionsConverter.toQuickActions( + actions = actions, + isRedesignEnabled = true, + context = TokenActionsContext.Markets, + ) + + // Assert + assertThat(result).containsExactly( + QuickActionUM.V2.Receive, + QuickActionUM.V2.Exchange(shouldShowBadge = false), + ).inOrder() + assertThat(result).doesNotContain(QuickActionUM.V2.Buy) + } + + @Test + fun `GIVEN send swap sell available WHEN context is Transfer THEN send exchange swapAndSend sell in order`() { + // Arrange + val actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, shouldShowBadge = false), + TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None), + ) + + // Act + val result = QuickActionsConverter.toQuickActions( + actions = actions, + isRedesignEnabled = true, + context = TokenActionsContext.Transfer, + ) + + // Assert + assertThat(result).containsExactly( + QuickActionUM.V2.Send, + QuickActionUM.V2.Exchange(shouldShowBadge = false), + QuickActionUM.V2.SwapAndSend, + QuickActionUM.V2.Sell, + ).inOrder() + } + + @Test + fun `GIVEN send and sell but no swap WHEN context is Transfer THEN only send and sell with no swapAndSend`() { + // Arrange + val actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None), + ) + + // Act + val result = QuickActionsConverter.toQuickActions( + actions = actions, + isRedesignEnabled = true, + context = TokenActionsContext.Transfer, + ) + + // Assert + assertThat(result).containsExactly( + QuickActionUM.V2.Send, + QuickActionUM.V2.Sell, + ).inOrder() + assertThat(result).doesNotContain(QuickActionUM.V2.SwapAndSend) + assertThat(result).doesNotContain(QuickActionUM.V2.Exchange(shouldShowBadge = false)) + } +} \ No newline at end of file diff --git a/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/TokenActionsContextTest.kt b/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/TokenActionsContextTest.kt new file mode 100644 index 0000000000..4e97c83e41 --- /dev/null +++ b/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/TokenActionsContextTest.kt @@ -0,0 +1,35 @@ +package com.tangem.common.ui.markets.action + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRoute +import com.tangem.common.ui.markets.action.TokenActionsBSContentUM.Action +import org.junit.jupiter.api.Test + +internal class TokenActionsContextTest { + + @Test + fun `GIVEN AddFunds context WHEN allowedActionsInOrder THEN Buy Swap Receive`() { + // Act + val actual = TokenActionsContext.AddFunds.allowedActionsInOrder + + // Assert + assertThat(actual).containsExactly(Action.Buy, Action.Exchange, Action.Receive).inOrder() + } + + @Test + fun `GIVEN AddFunds context WHEN swapPosition THEN TO`() { + assertThat(TokenActionsContext.AddFunds.swapPosition) + .isEqualTo(AppRoute.Swap.CurrencyPosition.TO) + } + + @Test + fun `GIVEN Transfer context WHEN swapPosition THEN FROM`() { + assertThat(TokenActionsContext.Transfer.swapPosition) + .isEqualTo(AppRoute.Swap.CurrencyPosition.FROM) + } + + @Test + fun `GIVEN Markets context WHEN allowedActionsInOrder THEN null meaning all`() { + assertThat(TokenActionsContext.Markets.allowedActionsInOrder).isNull() + } +} \ No newline at end of file diff --git a/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandlerSwapTest.kt b/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandlerSwapTest.kt new file mode 100644 index 0000000000..280aee77db --- /dev/null +++ b/common/ui-markets/src/test/kotlin/com/tangem/common/ui/markets/action/TokenActionsHandlerSwapTest.kt @@ -0,0 +1,104 @@ +package com.tangem.common.ui.markets.action + +import com.google.common.truth.Truth.assertThat +import com.tangem.common.routing.AppRoute +import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory +import com.tangem.core.decompose.navigation.Route +import com.tangem.core.decompose.navigation.Router +import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrency +import com.tangem.domain.models.currency.CryptoCurrencyStatus +import com.tangem.domain.models.wallet.UserWallet +import com.tangem.utils.Provider +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.jupiter.api.Test + +@OptIn(ExperimentalCoroutinesApi::class) +internal class TokenActionsHandlerSwapTest { + + private val router: Router = mockk(relaxed = true) + + private val handler = TokenActionsHandler( + router = router, + clipboardManager = mockk(relaxed = true), + uiMessageSender = mockk(relaxed = true), + getOfframpUrlUseCase = mockk(relaxed = true), + urlOpener = mockk(relaxed = true), + analyticsEventHandler = mockk(relaxed = true), + currentAppCurrency = Provider { mockk(relaxed = true) }, + onHandleQuickAction = { _, _ -> }, + coroutineScope = CoroutineScope(UnconfinedTestDispatcher()), + isDemoCardUseCase = mockk(relaxed = true), + isWalletBackupProblematicUseCase = mockk(relaxed = true), + sendBackupProblemEmailUseCase = mockk(relaxed = true), + messageSender = mockk(relaxed = true), + ) + + private val userWallet: UserWallet = mockk(relaxed = true) + private val currency: CryptoCurrency = MockCryptoCurrencyFactory().ethereum + private val data = CryptoCurrencyData( + userWallet = userWallet, + status = CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading), + actions = emptyList(), + isAccountMode = false, + account = mockk(relaxed = true), + ) + + @Test + fun `GIVEN AddFunds context WHEN handle Exchange THEN swap pushed with TO position`() { + // Arrange + val slot = slot() + every { router.push(capture(slot), any()) } returns Unit + + // Act + handler.handle( + action = TokenActionsBSContentUM.Action.Exchange, + cryptoCurrencyData = data, + context = TokenActionsContext.AddFunds, + ) + + // Assert + val pushed = slot.captured as AppRoute.Swap + assertThat(pushed.fromCurrencyPosition).isEqualTo(AppRoute.Swap.CurrencyPosition.TO) + } + + @Test + fun `GIVEN Transfer context WHEN handle Exchange THEN swap pushed with FROM position`() { + // Arrange + val slot = slot() + every { router.push(capture(slot), any()) } returns Unit + + // Act + handler.handle( + action = TokenActionsBSContentUM.Action.Exchange, + cryptoCurrencyData = data, + context = TokenActionsContext.Transfer, + ) + + // Assert + val pushed = slot.captured as AppRoute.Swap + assertThat(pushed.fromCurrencyPosition).isEqualTo(AppRoute.Swap.CurrencyPosition.FROM) + } + + @Test + fun `GIVEN default context WHEN handle Exchange THEN swap pushed with ANY position`() { + // Arrange + val slot = slot() + every { router.push(capture(slot), any()) } returns Unit + + // Act + handler.handle( + action = TokenActionsBSContentUM.Action.Exchange, + cryptoCurrencyData = data, + ) + + // Assert + val pushed = slot.captured as AppRoute.Swap + assertThat(pushed.fromCurrencyPosition).isEqualTo(AppRoute.Swap.CurrencyPosition.ANY) + } +} \ No newline at end of file diff --git a/core/config-toggles/src/main/assets/configs/feature_toggles_config.json b/core/config-toggles/src/main/assets/configs/feature_toggles_config.json index 72f2c9c5a0..e0464082f0 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 @@ -17,7 +17,7 @@ }, { "name": "APP_REDESIGN_ENABLED", - "version": "5.40" + "version": "6.0" }, { "name": "GASLESS_APPROVAL_ENABLED", @@ -57,7 +57,7 @@ }, { "name": "TWI_1326_YIELD_MODE_SWAP_ENABLED", - "version": "undefined" + "version": "6.0" }, { "name": "ADDRESS_SYNC_ENABLED", @@ -65,11 +65,11 @@ }, { "name": "AND_15207_SWAP_SWITCH_TO_TRANSFER_ENABLED", - "version": "undefined" + "version": "6.0" }, { "name": "AND_15120_SWAP_INTEGRATED_APPROVE", - "version": "undefined" + "version": "6.0" }, { "name": "SWAP_AB_ENABLED", @@ -81,12 +81,16 @@ }, { "name": "AND_15596_ONBOARDING_PUSH_NOTIFICATION_DOUBLE_ASK_AB_ENABLED", - "version": "undefined" + "version": "6.0" }, { "name": "AND_15310_ADD_FUNDS_STAGE1", "version": "5.39" }, + { + "name": "TWI_1377_MANAGE_FUNDS", + "version": "5.40" + }, { "name": "AND_15009_SWAP_PROVIDER_FILTER_ENABLED", "version": "5.39" @@ -121,19 +125,19 @@ }, { "name": "AND_15482_SURVEYSPARROW_ENABLED", - "version": "undefined" + "version": "6.0" }, { "name": "AND_15258_QUICK_TOP_UP_ENABLED", - "version": "undefined" + "version": "6.0" }, { "name": "AND_15368_VISA_PAY_REDESIGN", - "version": "undefined" + "version": "6.0" }, { "name": "AND_15364_VISA_PAY_CARD_CLOSE", - "version": "undefined" + "version": "6.0" }, { "name": "AND_15741_VISA_PAY_REMOVE_ACCOUNT", @@ -145,15 +149,15 @@ }, { "name": "AND_15489_EXPRESS_SHARE_BUTTON_ENABLED", - "version": "undefined" + "version": "6.0" }, { "name": "AND_15235_VISA_MULTIPLE_CARDS", - "version": "5.40" + "version": "6.0" }, { "name": "AND_15715_SWAP_BEST_DEX_RATE_ENABLED", - "version": "undefined" + "version": "6.0" }, { "name": "AND_15767_NEW_TX_HISTORY_ENABLED", diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index bfac57f269..3e4488d270 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -332,6 +332,7 @@ Error Top-up network fee Swap + Send&Swap Explore Explore transaction history Explorer @@ -1294,6 +1295,7 @@ Sell crypto securely Send to another wallet Between your portfolios + Send with swap to another token Other Quick top up No memo required diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt index e2cd0e558f..ec474e7288 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/bottomsheets/message/MessageBottomSheetV2.kt @@ -166,6 +166,7 @@ private fun BottomSheetIconContainer( } } +@Suppress("MagicNumber") @Composable private fun BottomSheetIcon(icon: MessageBottomSheetUM.Icon, modifier: Modifier = Modifier) { val tint = when (icon.type) { @@ -178,7 +179,9 @@ private fun BottomSheetIcon(icon: MessageBottomSheetUM.Icon, modifier: Modifier val backgroundColor = when (icon.backgroundType) { MessageBottomSheetUM.Icon.BackgroundType.Unspecified -> Color.Unspecified - MessageBottomSheetUM.Icon.BackgroundType.SameAsTint -> tint + MessageBottomSheetUM.Icon.BackgroundType.SameAsTint -> { + if (tint == Color.Unspecified) Color.Unspecified else tint.copy(alpha = 0.1f) + } MessageBottomSheetUM.Icon.BackgroundType.Accent -> TangemTheme.colors3.bg.status.infoSubtle MessageBottomSheetUM.Icon.BackgroundType.Informative -> TangemTheme.colors3.bg.status.infoSubtle MessageBottomSheetUM.Icon.BackgroundType.Attention -> TangemTheme.colors3.bg.status.warningSubtle diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionItem.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionItem.kt index 59858ddf0e..a0c9c33daa 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionItem.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionItem.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -25,6 +26,8 @@ import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview @@ -201,9 +204,15 @@ private fun SubtitleText(subtitle: ContentSubtitle, status: Status, modifier: Mo overflow = TextOverflow.Ellipsis, modifier = modifier, ) + is ContentSubtitle.PlainAddress -> PlainAddressText( + subtitle = subtitle, + status = status, + modifier = modifier, + ) is ContentSubtitle.ExternalAddress -> InlineImageSubtitle( template = stringResourceSafe(subtitle.direction.templateResId(), subtitle.briefAddress), color = tertiary, + afterIconColor = if (isFailed) tertiary else primary, modifier = modifier, ) { IdentIcon( @@ -256,6 +265,33 @@ private fun SubtitleText(subtitle: ContentSubtitle, status: Status, modifier: Mo } } +@Composable +private fun PlainAddressText(subtitle: ContentSubtitle.PlainAddress, status: Status, modifier: Modifier = Modifier) { + val full = subtitle.text.resolveReference() + val highlightColor = if (status is Status.Failed) { + TangemTheme.colors2.text.neutral.tertiary + } else { + TangemTheme.colors2.text.neutral.primary + } + val text = remember(full, subtitle.highlight, highlightColor) { + buildAnnotatedString { + append(full) + val start = full.lastIndexOf(subtitle.highlight) + if (start >= 0) { + addStyle(SpanStyle(color = highlightColor), start, start + subtitle.highlight.length) + } + } + } + Text( + text = text, + color = TangemTheme.colors2.text.neutral.tertiary, + style = TangemTheme.typography2.captionMedium12, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = modifier, + ) +} + private fun ContentSubtitle.Direction.templateResId(): Int = when (this) { ContentSubtitle.Direction.TO -> R.string.transaction_history_to_inline_address ContentSubtitle.Direction.FROM -> R.string.transaction_history_from_inline_address diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionItemUM.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionItemUM.kt index 2debebbbee..1c3adfcd0e 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionItemUM.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/state/TransactionItemUM.kt @@ -55,9 +55,16 @@ sealed interface TransactionItemUM { /** Subtitle variants for [Content] rows. */ @Immutable sealed interface ContentSubtitle { - /** Plain text — for types without a directly-displayable address (Operation, GaslessFee, ClaimRewards, etc.). */ + /** Plain text — for types without a displayable address (Operation, GaslessFee, ClaimRewards, etc.). */ data class Plain(val text: TextReference) : ContentSubtitle + /** + * Plain-text address subtitle without an identicon — renders "prefix:
", where [highlight] + * (a substring of the resolved [text]) is painted in the primary text color while the prefix stays + * tertiary. Used for contract / validator addresses, external swap addresses and yield "for:
". + */ + data class PlainAddress(val text: TextReference, val highlight: String) : ContentSubtitle + /** * External counterparty address — renders as "to/from: ". * Used for Transfer to/from external addresses. diff --git a/data/express/build.gradle.kts b/data/express/build.gradle.kts index 473892eedf..994d34af6d 100644 --- a/data/express/build.gradle.kts +++ b/data/express/build.gradle.kts @@ -25,6 +25,7 @@ dependencies { implementation(projects.domain.express.models) implementation(projects.domain.express) implementation(projects.domain.wallets.models) + implementation(projects.domain.txhistory) api(projects.domain.models) /** Other */ diff --git a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt index fd4abccfa9..ff1be9d939 100644 --- a/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt +++ b/data/express/src/main/java/com/tangem/data/express/DefaultExpressRepository.kt @@ -12,6 +12,7 @@ import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.models.ExpressProvider import com.tangem.domain.express.models.ExpressProviderType import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.extensions.filterIf import com.tangem.utils.logging.TangemLogger @@ -21,6 +22,7 @@ internal class DefaultExpressRepository( private val expressHistoryDao: ExpressHistoryDao, private val appPreferencesStore: AppPreferencesStore, private val dispatchers: CoroutineDispatcherProvider, + private val txHistoryFeatureToggles: TxHistoryFeatureToggles, ) : ExpressRepository { override suspend fun getProviders( @@ -37,7 +39,9 @@ internal class DefaultExpressRepository( ), ).getOrThrow() - expressHistoryDao.upsertProviders(providers.map { it.toEntity() }) + if (txHistoryFeatureToggles.isNewTxHistoryEnabled) { + expressHistoryDao.upsertProviders(providers.map { it.toEntity() }) + } providers.map(ExpressProviderConverter()::convert) .filterIf(filterProviderTypes.isNotEmpty()) { it.type in filterProviderTypes } diff --git a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt index 0814d23c42..4e4f456658 100644 --- a/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt +++ b/data/express/src/main/java/com/tangem/data/express/di/ExpressDataModule.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.express.ExpressErrorResolver import com.tangem.domain.express.ExpressRepository import com.tangem.domain.express.ExpressServiceFetcher +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -40,12 +41,14 @@ internal object ExpressDataModule { expressHistoryDao: ExpressHistoryDao, appPreferencesStore: AppPreferencesStore, dispatchers: CoroutineDispatcherProvider, + txHistoryFeatureToggles: TxHistoryFeatureToggles, ): ExpressRepository { return DefaultExpressRepository( tangemExpressApi = tangemExpressApi, expressHistoryDao = expressHistoryDao, appPreferencesStore = appPreferencesStore, dispatchers = dispatchers, + txHistoryFeatureToggles = txHistoryFeatureToggles, ) } diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index c61541ce6a..7a531ff97e 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(projects.domain.appTheme.models) implementation(projects.domain.models) implementation(projects.domain.express.models) + implementation(projects.domain.txhistory) // region DI diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt index 41c0e75aa1..e783308ca6 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/DefaultOnrampRepository.kt @@ -26,11 +26,11 @@ import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.exchangeservice.swap.ExpressUtils import com.tangem.datasource.local.converter.toEntity import com.tangem.datasource.local.onramp.countries.OnrampCountriesStore +import com.tangem.datasource.local.onramp.country.OnrampCurrentCountryByIPStore import com.tangem.datasource.local.onramp.currencies.OnrampCurrenciesStore import com.tangem.datasource.local.onramp.pairs.OnrampPairsStore import com.tangem.datasource.local.onramp.paymentmethods.OnrampPaymentMethodsStore import com.tangem.datasource.local.onramp.quotes.OnrampQuotesStore -import com.tangem.datasource.local.onramp.country.OnrampCurrentCountryByIPStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObject @@ -48,6 +48,7 @@ import com.tangem.domain.onramp.model.error.OnrampPairsError import com.tangem.domain.onramp.model.error.OnrampRedirectError import com.tangem.domain.onramp.repositories.OnrampRepository import com.tangem.domain.tokens.model.Amount +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.logging.TangemLogger @@ -75,6 +76,7 @@ internal class DefaultOnrampRepository( private val walletManagersFacade: WalletManagersFacade, private val dataSignatureVerifier: DataSignatureVerifier, private val expressHistoryDao: ExpressHistoryDao, + private val txHistoryFeatureToggles: TxHistoryFeatureToggles, moshi: Moshi, ) : OnrampRepository { @@ -165,7 +167,9 @@ internal class DefaultOnrampRepository( ) .getOrThrow() - expressHistoryDao.upsertOnramps(listOf(response.toEntity(ownerAddress = response.payoutAddress))) + if (txHistoryFeatureToggles.isNewTxHistoryEnabled) { + expressHistoryDao.upsertOnramps(listOf(response.toEntity(ownerAddress = response.payoutAddress))) + } statusConverter.convert(response) } diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt index 5b8c718f93..0cab59284f 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/di/OnrampDataModule.kt @@ -30,6 +30,7 @@ import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.common.wallets.UserWalletsListRepository import com.tangem.utils.coroutines.AppCoroutineScope import com.tangem.domain.onramp.repositories.* +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module @@ -58,6 +59,7 @@ internal object OnrampDataModule { dataSignatureVerifier: DataSignatureVerifier, onrampCurrentCountryByIPStore: OnrampCurrentCountryByIPStore, expressHistoryDao: ExpressHistoryDao, + txHistoryFeatureToggles: TxHistoryFeatureToggles, @NetworkMoshi moshi: Moshi, ): OnrampRepository { return DefaultOnrampRepository( @@ -74,6 +76,7 @@ internal object OnrampDataModule { walletManagersFacade = walletManagersFacade, dataSignatureVerifier = dataSignatureVerifier, expressHistoryDao = expressHistoryDao, + txHistoryFeatureToggles = txHistoryFeatureToggles, moshi = moshi, ) } diff --git a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt index 68cc4759a6..0c90bc8ef9 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/pay/flow/DefaultPaymentAccountStatusFetcher.kt @@ -40,6 +40,18 @@ import kotlin.time.Duration.Companion.minutes private const val TAG = "PaymentAccountStatusFetcher" +/** + * Reorders cards to match [previousOrder] (by [TangemPayCard.id]), appending any card absent from it at the + * end while preserving the relative order among the new ones. Keeps the card layout stable when the backend + * reorders `productInstances` (e.g. after a rename bumps `updated_at`). Returns the receiver unchanged when + * [previousOrder] is empty (first load → backend order). + */ +internal fun List.stableOrder(previousOrder: List): List { + if (previousOrder.isEmpty()) return this + val indexById = previousOrder.withIndex().associate { (index, id) -> id to index } + return sortedBy { indexById[it.id] ?: Int.MAX_VALUE } +} + @Suppress("LongParameterList", "LargeClass") internal class DefaultPaymentAccountStatusFetcher @Inject constructor( private val paymentAccountStatusesStore: PaymentAccountStatusesStore, @@ -365,13 +377,18 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( // placeholder for every locally tracked in-flight issuance order alongside the real cards. val issuingCards = buildIssuingCards(userWalletId) + // Keep the card order stable across refetches: the backend orders `productInstances` by a mutable + // field (a rename bumps `updated_at`), which would otherwise make the renamed card jump. Anchor on + // the previously shown order and append newly seen cards at the end. + val orderedCards = tangemPayCards.stableOrder(previousRealCardOrder(userWalletId)) + return PaymentAccountStatusValue.Loaded( source = StatusSource.ACTUAL, customerId = customerId, depositAddress = cryptoBalance.depositAddress, cryptoCurrency = tangemPayCurrencyFactory.create(userWalletId), fiatRate = fiatRate, - cards = tangemPayCards + issuingCards, + cards = orderedCards + issuingCards, balance = PaymentAccountStatusValue.Balance( fiatBalance = fiatBalance, cryptoBalance = cryptoBalance, @@ -381,6 +398,21 @@ internal class DefaultPaymentAccountStatusFetcher @Inject constructor( ) } + /** + * Order of real (product-instance-backed) cards from the previously stored status, used as the stable + * anchor for [stableOrder]. Issuing placeholders are excluded — they carry synthetic order ids and are + * always appended last. Empty on the first load (no prior [PaymentAccountStatusValue.Loaded]), which makes + * [stableOrder] fall back to the backend order. + */ + private suspend fun previousRealCardOrder(userWalletId: UserWalletId): List { + val previousValue = paymentAccountStatusesStore.getSyncOrNull(userWalletId)?.value + return (previousValue as? PaymentAccountStatusValue.Loaded) + ?.cards + ?.filterNot { it.state == TangemPayCardState.Issuing } + ?.map { it.id } + .orEmpty() + } + private suspend fun getCardState(cardId: String, userWalletId: UserWalletId): TangemPayCardState { val closingOrderId = closeCardRepository.getCloseOrderId(userWalletId, cardId).getOrNull() val reissueOrderId = reissueCardRepository.getReissueOrderId(userWalletId, cardId).getOrNull() diff --git a/data/visa/src/test/kotlin/com/tangem/data/pay/flow/StableOrderTest.kt b/data/visa/src/test/kotlin/com/tangem/data/pay/flow/StableOrderTest.kt new file mode 100644 index 0000000000..814491f08f --- /dev/null +++ b/data/visa/src/test/kotlin/com/tangem/data/pay/flow/StableOrderTest.kt @@ -0,0 +1,80 @@ +package com.tangem.data.pay.flow + +import com.google.common.truth.Truth.assertThat +import com.tangem.domain.models.pay.TangemPayCard +import com.tangem.domain.models.pay.TangemPayCardFrozenState +import com.tangem.domain.models.pay.TangemPayCardState +import com.tangem.test.core.ProvideTestModels +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.params.ParameterizedTest + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +internal class StableOrderTest { + + @ParameterizedTest + @ProvideTestModels + fun stableOrder(model: Model) { + // Act + val result = model.cards.stableOrder(model.previousOrder).map { it.id } + + // Assert + assertThat(result).containsExactlyElementsIn(model.expectedIds).inOrder() + } + + private fun provideTestModels() = listOf( + Model( + name = "GIVEN no previous order WHEN stableOrder THEN backend order is kept", + cards = listOf(card("a"), card("b"), card("c")), + previousOrder = emptyList(), + expectedIds = listOf("a", "b", "c"), + ), + Model( + // The reported bug: a rename bumps updated_at, so the backend moves the renamed card. + name = "GIVEN backend reordered existing cards WHEN stableOrder THEN previous order is preserved", + cards = listOf(card("b"), card("c"), card("a")), + previousOrder = listOf("a", "b", "c"), + expectedIds = listOf("a", "b", "c"), + ), + Model( + name = "GIVEN a newly seen card WHEN stableOrder THEN it is appended at the end", + cards = listOf(card("c"), card("a"), card("b")), + previousOrder = listOf("a", "b"), + expectedIds = listOf("a", "b", "c"), + ), + Model( + name = "GIVEN several new cards WHEN stableOrder THEN they keep their backend order at the end", + cards = listOf(card("d"), card("b"), card("a"), card("c")), + previousOrder = listOf("a", "b"), + expectedIds = listOf("a", "b", "d", "c"), + ), + Model( + name = "GIVEN previous order references a gone card WHEN stableOrder THEN the stale id is ignored", + cards = listOf(card("b"), card("a")), + previousOrder = listOf("a", "x", "b"), + expectedIds = listOf("a", "b"), + ), + ) + + internal data class Model( + val name: String, + val cards: List, + val previousOrder: List, + val expectedIds: List, + ) { + override fun toString(): String = name + } + + private companion object { + fun card(id: String): TangemPayCard = TangemPayCard( + id = id, + productInstanceId = id, + cardStatus = TangemPayCard.Status.ACTIVE, + hasPinCode = false, + displayName = null, + limit = null, + frozenState = TangemPayCardFrozenState.Unfrozen, + lastDigits = "0000", + state = TangemPayCardState.Active, + ) + } +} \ No newline at end of file diff --git a/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/TxHistoryFeatureToggles.kt b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/TxHistoryFeatureToggles.kt similarity index 75% rename from features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/TxHistoryFeatureToggles.kt rename to domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/TxHistoryFeatureToggles.kt index 64b805af2b..7cf937781c 100644 --- a/features/txhistory/api/src/main/kotlin/com/tangem/features/txhistory/TxHistoryFeatureToggles.kt +++ b/domain/txhistory/src/main/kotlin/com/tangem/domain/txhistory/TxHistoryFeatureToggles.kt @@ -1,4 +1,4 @@ -package com.tangem.features.txhistory +package com.tangem.domain.txhistory interface TxHistoryFeatureToggles { val isSolanaTxHistoryEnabled: Boolean diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderConflictRules.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderConflictRules.kt index 9e8e69e94a..4c576e6bd3 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderConflictRules.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderConflictRules.kt @@ -66,8 +66,8 @@ object OrderConflictRules { } private fun OrderType.isIssuing(): Boolean { - return this == OrderType.CARD_ISSUE || - this == OrderType.CARD_ISSUE_ADDITIONAL || + return this == OrderType.CARD_ISSUE_ADDITIONAL || + this == OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC || this == OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2 } diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderType.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderType.kt index 979f004e79..006a67088f 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderType.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/model/OrderType.kt @@ -9,9 +9,9 @@ import com.tangem.domain.pay.model.OrderType.Companion.fromString * so the app never crashes on a new server-side type. */ enum class OrderType(val wireValue: String) { - CARD_ISSUE("CARD_ISSUE_VIRTUAL_RAIN_KYC"), CARD_ISSUE_ADDITIONAL("CARD_ISSUE_ADDITIONAL"), CARD_ISSUE_VIRTUAL_RAIN_KYC_V2("CARD_ISSUE_VIRTUAL_RAIN_KYC_V2"), + CARD_ISSUE_VIRTUAL_RAIN_KYC("CARD_ISSUE_VIRTUAL_RAIN_KYC"), CARD_REISSUE("CARD_REISSUE"), CARD_FREEZE("CARD_FREEZE"), CARD_UNFREEZE("CARD_UNFREEZE"), diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt index 5850737efd..e33223eab4 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCase.kt @@ -6,6 +6,7 @@ import arrow.core.raise.catch import arrow.core.raise.either import com.tangem.domain.models.wallet.UserWalletId import com.tangem.domain.pay.model.Offer +import com.tangem.domain.pay.model.OrderType import com.tangem.domain.pay.model.TangemPayOrderInfo import com.tangem.domain.pay.repository.CustomerOffersRepository import com.tangem.domain.pay.repository.CustomerOrderRepository @@ -53,7 +54,14 @@ class IssueAdditionalCardUseCase( val activeOrders = catch( block = { customerOrderRepository - .findOrders(userWalletId = userWalletId, types = setOf(offer.data.orderType)) + .findOrders( + userWalletId = userWalletId, + types = setOf( + offer.data.orderType, + OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, + OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2, + ), + ) .bind() }, catch = { handleError(it) }, diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/model/OrderConflictRulesTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/model/OrderConflictRulesTest.kt index 5df9fc677d..a288112d52 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/model/OrderConflictRulesTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/model/OrderConflictRulesTest.kt @@ -10,7 +10,7 @@ internal class OrderConflictRulesTest { @Test fun `IssueCard is blocked by an active issue order`() { - val active = listOf(order(type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING)) + val active = listOf(order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING)) val resolution = OrderConflictRules.resolve(OrderIntent.IssueCard, active) @@ -104,7 +104,7 @@ internal class OrderConflictRulesTest { val active = listOf( order(type = OrderType.CARD_FREEZE, status = OrderStatus.PROCESSING, productInstanceId = cardA), order(type = OrderType.WITHDRAW, status = OrderStatus.PROCESSING), - order(type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING), + order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING), ) val resolution = OrderConflictRules.resolve(OrderIntent.Rename(cardA), active) @@ -115,8 +115,8 @@ internal class OrderConflictRulesTest { @Test fun `Terminal-status orders never block`() { val terminal = listOf( - order(type = OrderType.CARD_ISSUE, status = OrderStatus.COMPLETED), - order(type = OrderType.CARD_ISSUE, status = OrderStatus.CANCELED), + order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.COMPLETED), + order(type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.CANCELED), ) val resolution = OrderConflictRules.resolve(OrderIntent.IssueCard, terminal) diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCaseTest.kt index 474ac3d59d..8e6b1926a4 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCaseTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/IssueAdditionalCardUseCaseTest.kt @@ -65,7 +65,11 @@ internal class IssueAdditionalCardUseCaseTest { coEvery { orderRepository.findOrders( userWalletId, - types = setOf(OrderType.CARD_ISSUE_ADDITIONAL), + types = setOf( + OrderType.CARD_ISSUE_ADDITIONAL, + OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, + OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2, + ), statuses = emptySet(), ) } returns listOf(existing).right() @@ -83,7 +87,11 @@ internal class IssueAdditionalCardUseCaseTest { coEvery { orderRepository.findOrders( userWalletId, - types = setOf(OrderType.CARD_ISSUE_ADDITIONAL), + types = setOf( + OrderType.CARD_ISSUE_ADDITIONAL, + OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, + OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2, + ), statuses = emptySet(), ) } returns emptyList().right() @@ -107,7 +115,11 @@ internal class IssueAdditionalCardUseCaseTest { coEvery { orderRepository.findOrders( userWalletId, - types = setOf(OrderType.CARD_ISSUE_ADDITIONAL), + types = setOf( + OrderType.CARD_ISSUE_ADDITIONAL, + OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, + OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC_V2, + ), statuses = emptySet(), ) } returns emptyList().right() diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveOrdersUseCaseTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveOrdersUseCaseTest.kt index 824337d9e4..03fa34daf2 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveOrdersUseCaseTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/usecase/RestoreActiveOrdersUseCaseTest.kt @@ -35,7 +35,7 @@ internal class RestoreActiveOrdersUseCaseTest { @Test fun `returns the orders found by the repository`() = runTest { val orders = listOf( - order(id = "issue", type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING), + order(id = "issue", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING), order(id = "withdraw", type = OrderType.WITHDRAW, status = OrderStatus.NEW), ) coEvery { repository.findOrders(userWalletId, types = emptySet(), statuses = any()) } returns orders.right() diff --git a/domain/visa/src/test/kotlin/com/tangem/domain/pay/util/OrderResolverTest.kt b/domain/visa/src/test/kotlin/com/tangem/domain/pay/util/OrderResolverTest.kt index d348afc362..0681644652 100644 --- a/domain/visa/src/test/kotlin/com/tangem/domain/pay/util/OrderResolverTest.kt +++ b/domain/visa/src/test/kotlin/com/tangem/domain/pay/util/OrderResolverTest.kt @@ -12,11 +12,11 @@ internal class OrderResolverTest { fun `selectActive filters by type and active status`() { val orders = listOf( order(id = "1", type = OrderType.WITHDRAW, status = OrderStatus.PROCESSING, updatedAt = "2026-01-01"), - order(id = "2", type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING, updatedAt = "2026-01-02"), - order(id = "3", type = OrderType.CARD_ISSUE, status = OrderStatus.COMPLETED, updatedAt = "2026-01-03"), + order(id = "2", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING, updatedAt = "2026-01-02"), + order(id = "3", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.COMPLETED, updatedAt = "2026-01-03"), ) - val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE) + val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC) assertThat(result?.id).isEqualTo("2") } @@ -24,11 +24,11 @@ internal class OrderResolverTest { @Test fun `selectActive picks the latest by updatedAt`() { val orders = listOf( - order(id = "old", type = OrderType.CARD_ISSUE, status = OrderStatus.NEW, updatedAt = "2026-01-01"), - order(id = "new", type = OrderType.CARD_ISSUE, status = OrderStatus.PROCESSING, updatedAt = "2026-06-05"), + order(id = "old", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.NEW, updatedAt = "2026-01-01"), + order(id = "new", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.PROCESSING, updatedAt = "2026-06-05"), ) - val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE) + val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC) assertThat(result?.id).isEqualTo("new") } @@ -36,11 +36,11 @@ internal class OrderResolverTest { @Test fun `selectActive returns null when no active order of the type exists`() { val orders = listOf( - order(id = "1", type = OrderType.CARD_ISSUE, status = OrderStatus.COMPLETED, updatedAt = "2026-01-01"), + order(id = "1", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.COMPLETED, updatedAt = "2026-01-01"), order(id = "2", type = OrderType.WITHDRAW, status = OrderStatus.PROCESSING, updatedAt = "2026-01-02"), ) - val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE) + val result = OrderResolver.selectActive(orders = orders, type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC) assertThat(result).isNull() } @@ -91,11 +91,11 @@ internal class OrderResolverTest { @Test fun `selectLatest includes terminal orders`() { val orders = listOf( - order(id = "1", type = OrderType.CARD_ISSUE, status = OrderStatus.COMPLETED, updatedAt = "2026-01-05"), - order(id = "2", type = OrderType.CARD_ISSUE, status = OrderStatus.NEW, updatedAt = "2026-01-01"), + order(id = "1", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.COMPLETED, updatedAt = "2026-01-05"), + order(id = "2", type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC, status = OrderStatus.NEW, updatedAt = "2026-01-01"), ) - val result = OrderResolver.selectLatest(orders = orders, type = OrderType.CARD_ISSUE) + val result = OrderResolver.selectLatest(orders = orders, type = OrderType.CARD_ISSUE_VIRTUAL_RAIN_KYC) assertThat(result?.id).isEqualTo("1") } diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/ChooseTokenBridge.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/ChooseTokenBridge.kt index e62479cdbf..0ba2e187cf 100644 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/ChooseTokenBridge.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/choosetoken/ChooseTokenBridge.kt @@ -55,6 +55,13 @@ interface ChooseTokenBridge : ChooseTokenBridgeInternal { isAppBarShown = false, isShowSingleCurrencyWallets = true, ) + val Transfer = Settings( + title = resourceReference(R.string.common_transfer), + isShowMarketBlock = false, + isShowPaymentAccount = false, + isAppBarShown = false, + isShowSingleCurrencyWallets = true, + ) } } diff --git a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addfunds/AddFundsComponent.kt b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/managefunds/ManageFundsComponent.kt similarity index 70% rename from features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addfunds/AddFundsComponent.kt rename to features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/managefunds/ManageFundsComponent.kt index 94533d5fda..855c301e01 100644 --- a/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/addfunds/AddFundsComponent.kt +++ b/features/common-features/api/src/main/java/com/tangem/features/commonfeatures/api/managefunds/ManageFundsComponent.kt @@ -1,17 +1,20 @@ -package com.tangem.features.commonfeatures.api.addfunds +package com.tangem.features.commonfeatures.api.managefunds import com.tangem.core.decompose.factory.ComponentFactory import com.tangem.core.ui.decompose.ComposableBottomSheetComponent import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWalletId -interface AddFundsComponent : ComposableBottomSheetComponent { +interface ManageFundsComponent : ComposableBottomSheetComponent { data class Params( val launchMode: LaunchMode, val onDismiss: () -> Unit, + val flowType: FlowType = FlowType.AddFunds, ) + enum class FlowType { AddFunds, Transfer } + sealed interface LaunchMode { data class ChooseToken(val userWalletId: UserWalletId) : LaunchMode @@ -25,5 +28,5 @@ interface AddFundsComponent : ComposableBottomSheetComponent { ) : LaunchMode } - interface Factory : ComponentFactory + interface Factory : ComponentFactory } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/di/AddFundsComponentModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/di/AddFundsComponentModule.kt deleted file mode 100644 index 7ebd2f3ddf..0000000000 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/di/AddFundsComponentModule.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.tangem.features.commonfeatures.impl.addfunds.di - -import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent -import com.tangem.features.commonfeatures.impl.addfunds.DefaultAddFundsComponent -import dagger.Binds -import dagger.Module -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent - -@Module -@InstallIn(SingletonComponent::class) -internal interface AddFundsComponentModule { - - @Binds - fun bindAddFundsComponentFactory(factory: DefaultAddFundsComponent.Factory): AddFundsComponent.Factory -} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/model/AddFundsRouteUiSpec.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/model/AddFundsRouteUiSpec.kt deleted file mode 100644 index 067e6b76a6..0000000000 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/model/AddFundsRouteUiSpec.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.features.commonfeatures.impl.addfunds.model - -import com.tangem.core.ui.extensions.TextReference -import com.tangem.core.ui.extensions.resourceReference -import com.tangem.features.commonfeatures.impl.R -import com.tangem.core.ui.R as CoreR - -internal data class AddFundsRouteUiSpec( - val title: TextReference, - val shouldApplyHorizontalPadding: Boolean, - val shouldFillHeight: Boolean, -) - -internal fun AddFundsModel.UiRoute.uiSpec(): AddFundsRouteUiSpec = when (this) { - AddFundsModel.UiRoute.Loading -> AddFundsRouteUiSpec( - title = resourceReference(R.string.common_add_funds), - shouldApplyHorizontalPadding = false, - shouldFillHeight = false, - ) - AddFundsModel.UiRoute.ChooseToken -> AddFundsRouteUiSpec( - title = resourceReference(R.string.common_add_funds), - shouldApplyHorizontalPadding = false, - shouldFillHeight = true, - ) - AddFundsModel.UiRoute.UserPortfolio -> AddFundsRouteUiSpec( - title = resourceReference(R.string.common_add_funds), - shouldApplyHorizontalPadding = false, - shouldFillHeight = false, - ) - AddFundsModel.UiRoute.TokenActions -> AddFundsRouteUiSpec( - title = resourceReference(CoreR.string.common_get_token), - shouldApplyHorizontalPadding = true, - shouldFillHeight = true, - ) -} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/DefaultAddFundsComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt similarity index 68% rename from features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/DefaultAddFundsComponent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt index 011dbadec6..acd951b2b2 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/DefaultAddFundsComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/DefaultManageFundsComponent.kt @@ -1,4 +1,4 @@ -package com.tangem.features.commonfeatures.impl.addfunds +package com.tangem.features.commonfeatures.impl.managefunds import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.fillMaxSize @@ -22,51 +22,56 @@ import com.tangem.core.ui.ds2.button.TangemButton import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemeRedesign import com.tangem.core.ui.test.BaseBottomSheetTestTags -import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenComponent -import com.tangem.features.commonfeatures.impl.addfunds.model.AddFundsModel -import com.tangem.features.commonfeatures.impl.addfunds.model.uiSpec +import com.tangem.features.commonfeatures.impl.managefunds.model.ManageFundsModel +import com.tangem.features.commonfeatures.impl.managefunds.model.uiSpec +import com.tangem.common.ui.markets.action.TokenActionsContext import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent import com.tangem.features.commonfeatures.impl.userportfolio.UserPortfolioComponent +import com.tangem.features.commonfeatures.impl.R import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import com.tangem.core.ui.R as CoreR @Suppress("LongParameterList") -internal class DefaultAddFundsComponent @AssistedInject constructor( +internal class DefaultManageFundsComponent @AssistedInject constructor( @Assisted appComponentContext: AppComponentContext, - @Assisted private val params: AddFundsComponent.Params, + @Assisted private val params: ManageFundsComponent.Params, chooseTokenComponentFactory: ChooseTokenComponent.Factory, tokenActionsComponentFactory: TokenActionsComponent.Factory, userPortfolioComponentFactory: UserPortfolioComponent.Factory, walletFeatureToggles: WalletFeatureToggles, -) : AppComponentContext by appComponentContext, AddFundsComponent { +) : AppComponentContext by appComponentContext, ManageFundsComponent { - private val model: AddFundsModel = getOrCreateModel(params) + private val model: ManageFundsModel = getOrCreateModel(params) - private val isCompactTokenActions: Boolean = params.launchMode is AddFundsComponent.LaunchMode.TokenActionsOnly + private val isCompactTokenActions: Boolean = params.launchMode is ManageFundsComponent.LaunchMode.TokenActionsOnly private val isAddFundsStage1Enabled: Boolean = walletFeatureToggles.isAddFundsStage1Enabled private val tokenActionsComponent: TokenActionsComponent by lazy { tokenActionsComponentFactory.create( - context = child(key = "addFundsTokenActions"), + context = child(key = "manageFundsTokenActions"), params = TokenActionsComponent.Params( data = model.tokenActionsData, callbacks = model, bottomAction = model.currentBottomAction, isRedesignForced = true, isCompact = isCompactTokenActions, + context = when (model.flowType) { + ManageFundsComponent.FlowType.AddFunds -> TokenActionsContext.AddFunds + ManageFundsComponent.FlowType.Transfer -> TokenActionsContext.Transfer + }, ), ) } private val chooseTokenComponent: ChooseTokenComponent? by lazy { - (params.launchMode as? AddFundsComponent.LaunchMode.ChooseToken)?.let { + (params.launchMode as? ManageFundsComponent.LaunchMode.ChooseToken)?.let { chooseTokenComponentFactory.create( - context = child(key = "addFundsChooseToken"), + context = child(key = "manageFundsChooseToken"), params = ChooseTokenComponent.Params( bridge = model.chooseTokenBridge, ), @@ -76,7 +81,7 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( private val userPortfolioComponent: UserPortfolioComponent by lazy { userPortfolioComponentFactory.create( - context = child(key = "addFundsUserPortfolio"), + context = child(key = "manageFundsUserPortfolio"), params = UserPortfolioComponent.Params( uiState = model.userPortfolioStateController.uiState, callbacks = object : UserPortfolioComponent.Callbacks { @@ -94,8 +99,8 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( val canGoBack by model.canGoBack.collectAsStateWithLifecycle() LaunchedEffect(route) { - if (route != AddFundsModel.UiRoute.UserPortfolio) return@LaunchedEffect - val mode = params.launchMode as? AddFundsComponent.LaunchMode.FilteredByRawId ?: return@LaunchedEffect + if (route != ManageFundsModel.UiRoute.UserPortfolio) return@LaunchedEffect + val mode = params.launchMode as? ManageFundsComponent.LaunchMode.FilteredByRawId ?: return@LaunchedEffect model.userPortfolioStateController.updateAndWaitNotNullState( allAvailableData = model.buildAvailableToAddDataForChooser(), rawCurrencyId = mode.rawCurrencyId, @@ -111,10 +116,10 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( content = TangemBottomSheetConfigContent.Empty, ), type = when (params.launchMode) { - is AddFundsComponent.LaunchMode.TokenActionsOnly -> TangemBottomSheetType.Modal - is AddFundsComponent.LaunchMode.ChooseToken -> TangemBottomSheetType.Default - is AddFundsComponent.LaunchMode.FilteredByRawId -> - if (route is AddFundsModel.UiRoute.TokenActions) { + is ManageFundsComponent.LaunchMode.TokenActionsOnly -> TangemBottomSheetType.Modal + is ManageFundsComponent.LaunchMode.ChooseToken -> TangemBottomSheetType.Default + is ManageFundsComponent.LaunchMode.FilteredByRawId -> + if (route is ManageFundsModel.UiRoute.TokenActions) { TangemBottomSheetType.Default } else { TangemBottomSheetType.Modal @@ -122,7 +127,7 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( }, containerColor = TangemTheme.colors2.surface.level2, title = { - AddFundsBottomSheetTitle( + ManageFundsBottomSheetTitle( route = route, canGoBack = canGoBack, onBackClick = model::onBack, @@ -131,7 +136,7 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( }, content = { val animatedContentModifier = - if (params.launchMode is AddFundsComponent.LaunchMode.ChooseToken) { + if (params.launchMode is ManageFundsComponent.LaunchMode.ChooseToken) { Modifier.fillMaxSize() } else { Modifier @@ -139,11 +144,12 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( AnimatedContent( targetState = route, modifier = animatedContentModifier, - label = "AddFundsContentAnimation", + label = "ManageFundsContentAnimation", ) { animatedRoute -> - AddFundsRouteContent( + ManageFundsRouteContent( route = animatedRoute, - shouldFillHeight = !isCompactTokenActions && animatedRoute.uiSpec().shouldFillHeight, + shouldFillHeight = !isCompactTokenActions && + animatedRoute.uiSpec(model.flowType).shouldFillHeight, ) } }, @@ -152,8 +158,8 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( } @Composable - private fun AddFundsRouteContent(route: AddFundsModel.UiRoute, shouldFillHeight: Boolean) { - val spec = route.uiSpec() + private fun ManageFundsRouteContent(route: ManageFundsModel.UiRoute, shouldFillHeight: Boolean) { + val spec = route.uiSpec(model.flowType) val horizontalPadding = if (spec.shouldApplyHorizontalPadding) { Modifier.padding(horizontal = TangemTheme.dimens2.x4) } else { @@ -164,33 +170,33 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( } @Composable - private fun RenderRoute(route: AddFundsModel.UiRoute, modifier: Modifier = Modifier) { + private fun RenderRoute(route: ManageFundsModel.UiRoute, modifier: Modifier = Modifier) { when (route) { - AddFundsModel.UiRoute.Loading -> Unit - AddFundsModel.UiRoute.ChooseToken -> chooseTokenComponent?.Content(modifier) - AddFundsModel.UiRoute.UserPortfolio -> CompositionLocalProvider( + ManageFundsModel.UiRoute.Loading -> Unit + ManageFundsModel.UiRoute.ChooseToken -> chooseTokenComponent?.Content(modifier) + ManageFundsModel.UiRoute.UserPortfolio -> CompositionLocalProvider( LocalTangemBottomSheetContentBottomInset provides TangemTheme.dimens2.x4, ) { userPortfolioComponent.Content(modifier) } - AddFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier) + ManageFundsModel.UiRoute.TokenActions -> tokenActionsComponent.Content(modifier) } } @Composable - private fun AddFundsBottomSheetTitle( - route: AddFundsModel.UiRoute, + private fun ManageFundsBottomSheetTitle( + route: ManageFundsModel.UiRoute, canGoBack: Boolean, onBackClick: () -> Unit, onCloseClick: () -> Unit, ) { TangemTopBar( - title = route.uiSpec().title, + title = route.uiSpec(model.flowType).title, type = TangemTopBarType.BottomSheet, startContent = if (canGoBack) { { TangemButton( - iconStart = TangemIconUM.Icon(iconRes = CoreR.drawable.ic_arrow_back_28), + iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_arrow_back_28), onClick = onBackClick, size = TangemButton.Size.X11, variant = TangemButton.Variant.Material, @@ -202,7 +208,7 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( endContent = { TangemButton( modifier = Modifier.testTag(BaseBottomSheetTestTags.CLOSE_BUTTON), - iconStart = TangemIconUM.Icon(iconRes = CoreR.drawable.ic_close_24), + iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24), onClick = onCloseClick, size = TangemButton.Size.X11, variant = TangemButton.Variant.Material, @@ -221,7 +227,10 @@ internal class DefaultAddFundsComponent @AssistedInject constructor( } @AssistedFactory - interface Factory : AddFundsComponent.Factory { - override fun create(context: AppComponentContext, params: AddFundsComponent.Params): DefaultAddFundsComponent + interface Factory : ManageFundsComponent.Factory { + override fun create( + context: AppComponentContext, + params: ManageFundsComponent.Params, + ): DefaultManageFundsComponent } } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/analytics/AddFundsAnalyticsEvent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/analytics/ManageFundsAnalyticsEvent.kt similarity index 53% rename from features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/analytics/AddFundsAnalyticsEvent.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/analytics/ManageFundsAnalyticsEvent.kt index 86cb642022..19c26683b6 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/analytics/AddFundsAnalyticsEvent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/analytics/ManageFundsAnalyticsEvent.kt @@ -1,23 +1,23 @@ -package com.tangem.features.commonfeatures.impl.addfunds.analytics +package com.tangem.features.commonfeatures.impl.managefunds.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.core.analytics.models.AnalyticsParam -internal sealed class AddFundsAnalyticsEvent( +internal sealed class ManageFundsAnalyticsEvent( event: String, params: Map = emptyMap(), ) : AnalyticsEvent(category = CATEGORY, event = event, params = params) { - class MethodScreenOpened(source: String) : AddFundsAnalyticsEvent( + class MethodScreenOpened(source: String) : ManageFundsAnalyticsEvent( event = "Method Screen Opened", params = mapOf(AnalyticsParam.SOURCE to source), ) - class ButtonBuy : AddFundsAnalyticsEvent(event = "Button - Buy") + class ButtonBuy : ManageFundsAnalyticsEvent(event = "Button - Buy") - class ButtonSwap : AddFundsAnalyticsEvent(event = "Button - Swap") + class ButtonSwap : ManageFundsAnalyticsEvent(event = "Button - Swap") - class ButtonReceive : AddFundsAnalyticsEvent(event = "Button - Receive") + class ButtonReceive : ManageFundsAnalyticsEvent(event = "Button - Receive") companion object { private const val CATEGORY = "Add Funds" diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/di/ManageFundsComponentModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/di/ManageFundsComponentModule.kt new file mode 100644 index 0000000000..e44ac1a3e1 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/di/ManageFundsComponentModule.kt @@ -0,0 +1,16 @@ +package com.tangem.features.commonfeatures.impl.managefunds.di + +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent +import com.tangem.features.commonfeatures.impl.managefunds.DefaultManageFundsComponent +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@Module +@InstallIn(SingletonComponent::class) +internal interface ManageFundsComponentModule { + + @Binds + fun bindManageFundsComponentFactory(factory: DefaultManageFundsComponent.Factory): ManageFundsComponent.Factory +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/di/AddFundsModelModule.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/di/ManageFundsModelModule.kt similarity index 52% rename from features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/di/AddFundsModelModule.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/di/ManageFundsModelModule.kt index efc483ebf2..2c8eec95cc 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/di/AddFundsModelModule.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/di/ManageFundsModelModule.kt @@ -1,8 +1,8 @@ -package com.tangem.features.commonfeatures.impl.addfunds.di +package com.tangem.features.commonfeatures.impl.managefunds.di import com.tangem.core.decompose.di.ModelComponent import com.tangem.core.decompose.model.Model -import com.tangem.features.commonfeatures.impl.addfunds.model.AddFundsModel +import com.tangem.features.commonfeatures.impl.managefunds.model.ManageFundsModel import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -11,10 +11,10 @@ import dagger.multibindings.IntoMap @Module @InstallIn(ModelComponent::class) -internal interface AddFundsModelModule { +internal interface ManageFundsModelModule { @Binds @IntoMap - @ClassKey(AddFundsModel::class) - fun addFundsModel(model: AddFundsModel): Model + @ClassKey(ManageFundsModel::class) + fun manageFundsModel(model: ManageFundsModel): Model } \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/model/AddFundsModel.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt similarity index 86% rename from features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/model/AddFundsModel.kt rename to features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt index f3d991be49..71f307963d 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/addfunds/model/AddFundsModel.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsModel.kt @@ -1,4 +1,4 @@ -package com.tangem.features.commonfeatures.impl.addfunds.model +package com.tangem.features.commonfeatures.impl.managefunds.model import androidx.compose.runtime.Immutable import com.tangem.blockchainsdk.compatibility.getTokenIdIfL2Network @@ -19,14 +19,14 @@ import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddData import com.tangem.features.commonfeatures.api.addtoportfolio.AvailableToAddWallet import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenAnalyticsPayload import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenBridge import com.tangem.features.commonfeatures.api.choosetoken.ChooseTokenResult import com.tangem.features.commonfeatures.api.tokenactions.BottomAction -import com.tangem.features.commonfeatures.impl.addfunds.analytics.AddFundsAnalyticsEvent +import com.tangem.features.commonfeatures.impl.managefunds.analytics.ManageFundsAnalyticsEvent import com.tangem.features.commonfeatures.impl.tokenactions.TokenActionsComponent import com.tangem.features.commonfeatures.impl.userportfolio.state.UserPortfolioStateController import com.tangem.utils.coroutines.CoroutineDispatcherProvider @@ -37,7 +37,7 @@ import javax.inject.Inject @ModelScoped @Suppress("LongParameterList") -internal class AddFundsModel @Inject constructor( +internal class ManageFundsModel @Inject constructor( paramsContainer: ParamsContainer, chooseTokenBridgeFactory: ChooseTokenBridge.Factory, userPortfolioStateControllerFactory: UserPortfolioStateController.Factory, @@ -50,8 +50,9 @@ internal class AddFundsModel @Inject constructor( override val dispatchers: CoroutineDispatcherProvider, ) : Model(), TokenActionsComponent.Callbacks { - private val params = paramsContainer.require() - val launchMode: AddFundsComponent.LaunchMode = params.launchMode + private val params = paramsContainer.require() + val launchMode: ManageFundsComponent.LaunchMode = params.launchMode + val flowType: ManageFundsComponent.FlowType = params.flowType private val routeStack = MutableStateFlow(listOf(UiRoute.Loading)) @@ -95,7 +96,10 @@ internal class AddFundsModel @Inject constructor( val chooseTokenBridge: ChooseTokenBridge by lazy { chooseTokenBridgeFactory.create( modelScope = modelScope, - settings = ChooseTokenBridge.Settings.AddFunds, + settings = when (flowType) { + ManageFundsComponent.FlowType.AddFunds -> ChooseTokenBridge.Settings.AddFunds + ManageFundsComponent.FlowType.Transfer -> ChooseTokenBridge.Settings.Transfer + }, analyticsPayload = setOf(ChooseTokenAnalyticsPayload.ScreensSources(SCREEN_SOURCE)), ) } @@ -116,9 +120,9 @@ internal class AddFundsModel @Inject constructor( init { when (val mode = launchMode) { - is AddFundsComponent.LaunchMode.ChooseToken -> initChooseToken(mode) - is AddFundsComponent.LaunchMode.TokenActionsOnly -> initTokenActionsOnly(mode) - is AddFundsComponent.LaunchMode.FilteredByRawId -> initFilteredByRawId(mode) + is ManageFundsComponent.LaunchMode.ChooseToken -> initChooseToken(mode) + is ManageFundsComponent.LaunchMode.TokenActionsOnly -> initTokenActionsOnly(mode) + is ManageFundsComponent.LaunchMode.FilteredByRawId -> initFilteredByRawId(mode) } } @@ -143,9 +147,9 @@ internal class AddFundsModel @Inject constructor( override fun onQuickActionClick(action: TokenActionsBSContentUM.Action, shouldDismiss: Boolean) { val event = when (action) { - TokenActionsBSContentUM.Action.Buy -> AddFundsAnalyticsEvent.ButtonBuy() - TokenActionsBSContentUM.Action.Exchange -> AddFundsAnalyticsEvent.ButtonSwap() - TokenActionsBSContentUM.Action.Receive -> AddFundsAnalyticsEvent.ButtonReceive() + TokenActionsBSContentUM.Action.Buy -> ManageFundsAnalyticsEvent.ButtonBuy() + TokenActionsBSContentUM.Action.Exchange -> ManageFundsAnalyticsEvent.ButtonSwap() + TokenActionsBSContentUM.Action.Receive -> ManageFundsAnalyticsEvent.ButtonReceive() else -> null } event?.let { analyticsEventHandler.send(it) } @@ -168,10 +172,10 @@ internal class AddFundsModel @Inject constructor( ) } - private fun initChooseToken(mode: AddFundsComponent.LaunchMode.ChooseToken) { + private fun initChooseToken(mode: ManageFundsComponent.LaunchMode.ChooseToken) { chooseTokenBridge.selectWalletTab(mode.userWalletId) analyticsEventHandler.send( - AddFundsAnalyticsEvent.MethodScreenOpened(source = AddFundsAnalyticsEvent.SOURCE_MAIN_SCREEN), + ManageFundsAnalyticsEvent.MethodScreenOpened(source = ManageFundsAnalyticsEvent.SOURCE_MAIN_SCREEN), ) replaceRoot(UiRoute.ChooseToken) modelScope.launch { @@ -182,7 +186,7 @@ internal class AddFundsModel @Inject constructor( } } - private fun initTokenActionsOnly(mode: AddFundsComponent.LaunchMode.TokenActionsOnly) { + private fun initTokenActionsOnly(mode: ManageFundsComponent.LaunchMode.TokenActionsOnly) { modelScope.launch { val wallet = getUserWalletUseCase.invokeFlow(mode.userWalletId) .mapNotNull { it.getOrNull() } @@ -206,7 +210,7 @@ internal class AddFundsModel @Inject constructor( } } - private fun initFilteredByRawId(mode: AddFundsComponent.LaunchMode.FilteredByRawId) { + private fun initFilteredByRawId(mode: ManageFundsComponent.LaunchMode.FilteredByRawId) { modelScope.launch { val entries = collectFilteredEntries(mode.rawCurrencyId) when (entries.size) { diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt new file mode 100644 index 0000000000..46258a31b1 --- /dev/null +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/managefunds/model/ManageFundsRouteUiSpec.kt @@ -0,0 +1,38 @@ +package com.tangem.features.commonfeatures.impl.managefunds.model + +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent +import com.tangem.features.commonfeatures.impl.R + +internal data class ManageFundsRouteUiSpec( + val title: TextReference, + val shouldApplyHorizontalPadding: Boolean, + val shouldFillHeight: Boolean, +) + +internal fun ManageFundsModel.UiRoute.uiSpec(flowType: ManageFundsComponent.FlowType): ManageFundsRouteUiSpec { + val isTransfer = flowType == ManageFundsComponent.FlowType.Transfer + return when (this) { + ManageFundsModel.UiRoute.Loading -> ManageFundsRouteUiSpec( + title = resourceReference(if (isTransfer) R.string.common_choose_token else R.string.common_add_funds), + shouldApplyHorizontalPadding = false, + shouldFillHeight = false, + ) + ManageFundsModel.UiRoute.ChooseToken -> ManageFundsRouteUiSpec( + title = resourceReference(R.string.common_choose_token), + shouldApplyHorizontalPadding = false, + shouldFillHeight = true, + ) + ManageFundsModel.UiRoute.UserPortfolio -> ManageFundsRouteUiSpec( + title = resourceReference(R.string.common_add_funds), + shouldApplyHorizontalPadding = false, + shouldFillHeight = false, + ) + ManageFundsModel.UiRoute.TokenActions -> ManageFundsRouteUiSpec( + title = resourceReference(if (isTransfer) R.string.common_transfer else R.string.common_get_token), + shouldApplyHorizontalPadding = true, + shouldFillHeight = true, + ) + } +} \ No newline at end of file diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/TokenActionsComponent.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/TokenActionsComponent.kt index 5ad5e141d6..09f7eb1a18 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/TokenActionsComponent.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/TokenActionsComponent.kt @@ -10,6 +10,7 @@ import com.arkivanov.decompose.router.slot.childSlot import com.arkivanov.decompose.router.slot.dismiss import com.tangem.common.ui.markets.action.CryptoCurrencyData import com.tangem.common.ui.markets.action.TokenActionsBSContentUM +import com.tangem.common.ui.markets.action.TokenActionsContext import com.tangem.core.decompose.context.AppComponentContext import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.factory.ComponentFactory @@ -79,6 +80,7 @@ internal class TokenActionsComponent @AssistedInject constructor( val bottomAction: Flow = flowOf(BottomAction.None), val isRedesignForced: Boolean = false, val isCompact: Boolean = false, + val context: TokenActionsContext = TokenActionsContext.Markets, ) interface Callbacks { diff --git a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt index 91c88bc364..cc1ec83458 100644 --- a/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt +++ b/features/common-features/impl/src/main/java/com/tangem/features/commonfeatures/impl/tokenactions/model/TokenActionsUiBuilder.kt @@ -92,6 +92,7 @@ internal class TokenActionsUiBuilder @Inject constructor( cryptoData = cryptoCurrencyData, tokenActionsHandler = tokenActionsHandler, isRedesignEnabled = false, + context = params.context, ), bottomActionText = bottomActionText(bottomAction), onBottomActionClick = { @@ -124,6 +125,7 @@ internal class TokenActionsUiBuilder @Inject constructor( cryptoData = cryptoCurrencyData, tokenActionsHandler = tokenActionsHandler, isRedesignEnabled = true, + context = params.context, ), bottomActionText = bottomActionText(bottomAction), onBottomActionClick = { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt index 41a971c02d..b5814e9491 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/FeedEntryChildFactory.kt @@ -8,6 +8,7 @@ import com.tangem.core.decompose.navigation.Route import com.tangem.core.ui.DesignFeatureToggles import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.features.feed.components.earn.DefaultEarnComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent import com.tangem.features.feed.components.feed.DefaultFeedComponent.FeedParams @@ -29,7 +30,7 @@ internal class FeedEntryChildFactory @Inject constructor( private val portfolioComponentFactory: MarketsPortfolioComponent.Factory, private val portfolioBlockComponentFactory: PortfolioBlockComponent.Factory, private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, - private val addFundsComponentFactory: com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent.Factory, + private val manageFundsComponentFactory: ManageFundsComponent.Factory, private val promoBannersBlockComponentFactory: PromoBannersBlockComponent.Factory, private val designFeatureToggles: DesignFeatureToggles, ) { @@ -84,7 +85,7 @@ internal class FeedEntryChildFactory @Inject constructor( portfolioBlockComponentFactory = portfolioBlockComponentFactory, designFeatureToggles = designFeatureToggles, addToPortfolioComponentFactory = addToPortfolioComponentFactory, - addFundsComponentFactory = addFundsComponentFactory, + manageFundsComponentFactory = manageFundsComponentFactory, ) } is Child.TokenList -> { diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt index 8d2cc3164c..32e69dc413 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/components/market/details/DefaultMarketsTokenDetailsComponent.kt @@ -42,7 +42,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.markets.PreselectedTokenDetailsSection import com.tangem.domain.markets.TokenMarketParams import com.tangem.domain.models.currency.CryptoCurrency -import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.features.commonfeatures.api.addtoportfolio.AddToPortfolioComponent import com.tangem.features.feed.components.market.details.portfolio.api.MarketsPortfolioComponent import com.tangem.features.feed.components.market.details.portfolioblock.PortfolioBlockComponent @@ -66,7 +66,7 @@ internal class DefaultMarketsTokenDetailsComponent( portfolioBlockComponentFactory: PortfolioBlockComponent.Factory, val params: Params, private val addToPortfolioComponentFactory: AddToPortfolioComponent.Factory, - private val addFundsComponentFactory: AddFundsComponent.Factory, + private val manageFundsComponentFactory: ManageFundsComponent.Factory, ) : ComposableModularBottomSheetContentComponent, AppComponentContext by appComponentContext { // applying l2 compatibility @@ -174,10 +174,10 @@ internal class DefaultMarketsTokenDetailsComponent( config: AddFundsSlotRoute, componentContext: ComponentContext, ): ComposableBottomSheetComponent { - val launchMode = AddFundsComponent.LaunchMode.FilteredByRawId(rawCurrencyId = config.rawCurrencyId) - return addFundsComponentFactory.create( + val launchMode = ManageFundsComponent.LaunchMode.FilteredByRawId(rawCurrencyId = config.rawCurrencyId) + return manageFundsComponentFactory.create( context = childByContext(componentContext), - params = AddFundsComponent.Params( + params = ManageFundsComponent.Params( launchMode = launchMode, onDismiss = { model.addFundsSheetNavigation.dismiss() }, ), diff --git a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt index 5963f2ea34..b3d505c948 100644 --- a/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt +++ b/features/feed/impl/src/main/kotlin/com/tangem/features/feed/ui/utils/EntryContentAnimationTransitions.kt @@ -3,17 +3,18 @@ package com.tangem.features.feed.ui.utils import androidx.compose.animation.* import androidx.compose.animation.core.tween import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.graphicsLayer import com.arkivanov.decompose.Child -import com.arkivanov.decompose.FaultyDecomposeApi -import com.arkivanov.decompose.extensions.compose.stack.animation.StackAnimation -import com.arkivanov.decompose.extensions.compose.stack.animation.fade -import com.arkivanov.decompose.extensions.compose.stack.animation.slide -import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation +import com.arkivanov.decompose.extensions.compose.stack.animation.* import com.arkivanov.decompose.router.stack.ChildStack import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent import com.tangem.features.feed.components.FeedEntryChildFactory +import kotlin.math.abs private const val FEED_ENTRY_SLIDE_DURATION_MS = 300 +private const val FEED_ENTRY_FADE_DURATION_MS = 300 internal typealias FeedEntryActiveChild = Child.Created @@ -21,19 +22,33 @@ internal typealias FeedEntryActiveChild = internal typealias FeedEntryChildStack = ChildStack -@OptIn(FaultyDecomposeApi::class) +// Single-child `stackAnimation` overload (-> SimpleStackAnimation), like the root content. Avoids the +// 3-arg overload, which is @FaultyDecomposeApi and backed by a movableContentOf impl known to misbehave on +// rapid transition interruptions (the "stuck content" bug). The selector sees only one child, so the fade vs +// slide choice is per-screen: Search fades, everything else slides. internal fun contentFeedEntryStackAnimation(): StackAnimation< FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent, > = - stackAnimation { to, from, _ -> - if (to.configuration.usesFadeStackTransition() || from.configuration.usesFadeStackTransition()) { - fade() + stackAnimation { child -> + if (child.configuration.usesFadeStackTransition()) { + feedContentFade() } else { slide() } } +private fun feedContentFade(): StackAnimator = stackAnimator( + animationSpec = tween(FEED_ENTRY_FADE_DURATION_MS), +) { factor, _, content -> + content( + Modifier.graphicsLayer { + alpha = 1f - abs(factor) + compositingStrategy = CompositingStrategy.ModulateAlpha + }, + ) +} + internal fun topBarFeedEntryAnimatedContentTransitionSpec( stackState: State, ): AnimatedContentTransitionScope.() -> ContentTransform = diff --git a/features/send/api/src/main/java/com/tangem/features/send/api/SendEntryPointComponent.kt b/features/send/api/src/main/java/com/tangem/features/send/api/SendEntryPointComponent.kt index 546a470a6e..21b81bf8a4 100644 --- a/features/send/api/src/main/java/com/tangem/features/send/api/SendEntryPointComponent.kt +++ b/features/send/api/src/main/java/com/tangem/features/send/api/SendEntryPointComponent.kt @@ -10,6 +10,7 @@ interface SendEntryPointComponent : ComposableContentComponent { data class Params( val userWalletId: UserWalletId, val cryptoCurrency: CryptoCurrency, + val shouldStartWithSwap: Boolean = false, ) interface Factory : ComponentFactory diff --git a/features/send/impl/src/main/java/com/tangem/features/send/entrypoint/DefaultSendEntryPointComponent.kt b/features/send/impl/src/main/java/com/tangem/features/send/entrypoint/DefaultSendEntryPointComponent.kt index 020cb7a708..5a724b9ef4 100644 --- a/features/send/impl/src/main/java/com/tangem/features/send/entrypoint/DefaultSendEntryPointComponent.kt +++ b/features/send/impl/src/main/java/com/tangem/features/send/entrypoint/DefaultSendEntryPointComponent.kt @@ -98,6 +98,11 @@ internal class DefaultSendEntryPointComponent @AssistedInject constructor( model.currentRoute.emit(stack.active.configuration) } } + if (params.shouldStartWithSwap) { + // Direct Swap&Send entry: replicate the canonical "send with swap" trigger so the receive-token + // selector opens like the regular Send -> Send&Swap flow (this was the less-broken behaviour). + model.onConvertToAnotherToken(lastAmount = "", isEnterInFiatSelected = false) + } } @Composable diff --git a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt index ea54e5c2b3..09064c9ed5 100644 --- a/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt +++ b/features/swap-v2/impl/src/main/java/com/tangem/features/swap/v2/impl/amount/model/SwapAmountModel.kt @@ -565,12 +565,11 @@ internal class SwapAmountModel @Inject constructor( (params as? SwapAmountComponentParams.AmountParams)?.callback?.resetSendWithSwapNavigation( data.shouldResetNavigation, ) + ensurePrimaryReady() + initPairs(data.swapCurrencies, data.cryptoCurrency) uiState.update { amountUM -> if (amountUM is SwapAmountUM.Content) { - initPairs(data.swapCurrencies, data.cryptoCurrency) - amountUM.copy( - isPrimaryButtonEnabled = false, - ) + amountUM.copy(isPrimaryButtonEnabled = false) } else { amountUM } @@ -579,6 +578,32 @@ internal class SwapAmountModel @Inject constructor( .launchIn(modelScope) } + /** + * Forces the amount UI into the primary-ready [SwapAmountUM.Content] state using the current primary + * status. Needed for the direct Send-with-Swap (Swap&Send) entry where the state can be [SwapAmountUM.Empty] and + * the balance-`distinctUntilChanged` primary status flow won't re-emit to rebuild it. No-op when already Content. + */ + private suspend fun ensurePrimaryReady() { + if (uiState.value is SwapAmountUM.Content) return + val primaryCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value + initCurrencies(primaryStatus = primaryCurrencyStatus, secondaryStatus = null) + val isOnlyOneWallet = getWalletsUseCase.invokeSync().size == 1 + uiState.transformerUpdate( + SwapAmountPrimaryReadyStateTransformer( + userWallet = userWallet, + primaryCryptoCurrencyStatus = primaryCurrencyStatus, + appCurrency = appCurrency, + swapDirection = swapDirection, + clickIntents = this, + isBalanceHidden = params.isBalanceHidingFlow.value, + isShowBestRateAnimation = isShowBestRateAnimation, + isSingleWallet = isOnlyOneWallet, + isAccountsMode = params.isAccountModeFlow.value, + account = params.accountFlow.value, + ), + ) + } + private fun initPairs(swapCurrencies: SwapCurrencies, secondaryCryptoCurrency: CryptoCurrency?) { modelScope.launch { val secondaryCurrency = selectInitialPairUseCase( diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 052c18df35..83b9f7209f 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.domain.transaction.models) implementation(projects.domain.express.models) implementation(projects.domain.account.status) + implementation(projects.domain.txhistory) implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 4d35b9c0fe..c72fb74d8c 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -29,6 +29,7 @@ import com.tangem.domain.express.models.ExpressOperationType import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository @@ -55,6 +56,7 @@ internal class DefaultSwapRepository( private val appPreferencesStore: AppPreferencesStore, private val rampStateManager: RampStateManager, private val expressHistoryDao: ExpressHistoryDao, + private val txHistoryFeatureToggles: TxHistoryFeatureToggles, moshi: Moshi, ) : SwapRepository { @@ -253,7 +255,9 @@ internal class DefaultSwapRepository( .getOrThrow() val entity = response.toEntity(ownerAddress = response.fromAddress.orEmpty()) - expressHistoryDao.upsertExchanges(listOf(entity)) + if (txHistoryFeatureToggles.isNewTxHistoryEnabled) { + expressHistoryDao.upsertExchanges(listOf(entity)) + } exchangeStatusConverter.convert(response) }, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index ca24786ea1..df600ee9ad 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -13,6 +13,7 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.txhistory.db.dao.ExpressHistoryDao import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.feature.swap.DefaultSwapFeedbackRepository import com.tangem.feature.swap.DefaultSwapRepository @@ -45,6 +46,7 @@ internal class SwapDataModule { appPreferencesStore: AppPreferencesStore, rampStateManager: RampStateManager, expressHistoryDao: ExpressHistoryDao, + txHistoryFeatureToggles: TxHistoryFeatureToggles, ): SwapRepository { return DefaultSwapRepository( tangemExpressApi = tangemExpressApi, @@ -56,6 +58,7 @@ internal class SwapDataModule { appPreferencesStore = appPreferencesStore, rampStateManager = rampStateManager, expressHistoryDao = expressHistoryDao, + txHistoryFeatureToggles = txHistoryFeatureToggles, ) } diff --git a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelGetSelectApprovalTypeParamsTest.kt b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelGetSelectApprovalTypeParamsTest.kt index 4ccd0da81d..c140ebdd0a 100644 --- a/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelGetSelectApprovalTypeParamsTest.kt +++ b/features/swap/impl/src/test/java/com/tangem/feature/swap/model/SwapModelGetSelectApprovalTypeParamsTest.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.model import com.google.common.truth.Truth.assertThat import com.tangem.common.ui.bottomsheet.permission.state.ApproveType +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.currency.CryptoCurrency import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.PermissionDataState @@ -41,7 +42,7 @@ internal class SwapModelGetSelectApprovalTypeParamsTest : SwapModelTestBase() { @Test fun `GIVEN provider state is not QuotesLoadedState THEN returns null`() { val provider = swapProvider() - val notLoaded: SwapState.EmptyAmountState = mockk(relaxed = true) + val notLoaded = SwapState.EmptyAmountState(zeroAmountEquivalent = TextReference.EMPTY) val model = createModel() model.dataState = model.dataState.copy( fromSwapCurrencyStatus = swapCurrencyStatus(), 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 9efe87d846..9cc41144ee 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 @@ -11,6 +11,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.ic_document_20 import com.tangem.domain.models.account.PaymentAccountStatusValue +import com.tangem.domain.models.pay.TangemPayCard import com.tangem.domain.models.pay.TangemPayCardFrozenState import com.tangem.domain.models.pay.TangemPayCardState import com.tangem.domain.models.pay.isFrozen @@ -59,6 +60,8 @@ internal class TangemPayDetailsStateFactory( fun getLoadedState(status: PaymentAccountStatusValue.Loaded): TangemPayDetailsUM { val hasUnfrozenCard = status.cards.any { it.frozenState == TangemPayCardFrozenState.Unfrozen } + val hasIssuingCard = status.cards.any { it.state == TangemPayCardState.Issuing } + val isAddCardEnabled = status.error == null && !hasIssuingCard return TangemPayDetailsUM( topBarConfig = TangemPayDetailsTopBarConfig( onBackClick = onBack, @@ -79,16 +82,15 @@ internal class TangemPayDetailsStateFactory( TangemPayDetailsBalanceBlockState.Card( lastDigits = cardItem.lastDigits, onClick = { intents.onCardClick(cardItem.id) }, - isReissuingOrClosing = cardItem.state == TangemPayCardState.Reissuing || - cardItem.state == TangemPayCardState.Closing, isEnabled = status.error == null, isFrozen = cardItem.isFrozen, - isIssuing = cardItem.state == TangemPayCardState.Issuing, + state = cardItem.state.toUiState(), ) } .toImmutableList(), onAddCardClick = intents::onAddCardClick, - isAddCardEnabled = status.error == null, + isAddCardEnabled = isAddCardEnabled, + progressBanner = status.cards.resolveProgressBanner(), ), ), isBalanceHidden = false, @@ -102,6 +104,12 @@ internal class TangemPayDetailsStateFactory( ) } + private fun List.resolveProgressBanner(): CardsProgressBannerUM? = when { + any { it.state == TangemPayCardState.Reissuing } -> CardsProgressBannerUM.Reissuing + any { it.state == TangemPayCardState.Issuing } -> CardsProgressBannerUM.Issuing + else -> null + } + fun getDeactivatedState(): TangemPayDetailsUM { return TangemPayDetailsUM( topBarConfig = TangemPayDetailsTopBarConfig( 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 523b1c4650..93713fe0a9 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 @@ -7,6 +7,7 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.models.pay.TangemPayCardFrozenState +import com.tangem.domain.models.pay.TangemPayCardState import kotlinx.collections.immutable.ImmutableList internal enum class CardDataType { @@ -94,13 +95,13 @@ internal sealed class TangemPayDetailsBalanceBlockState { val cards: ImmutableList, val onAddCardClick: () -> Unit, val isAddCardEnabled: Boolean, + val progressBanner: CardsProgressBannerUM? = null, ) data class Card( val lastDigits: String, val onClick: () -> Unit, - val isReissuingOrClosing: Boolean, - val isIssuing: Boolean, + val state: TangemPayCardUiState, val isFrozen: Boolean, val isEnabled: Boolean, ) @@ -109,4 +110,22 @@ internal sealed class TangemPayDetailsBalanceBlockState { internal data class AddToWalletBlockState( val onClick: () -> Unit, val onClickClose: () -> Unit, -) \ No newline at end of file +) + +internal enum class TangemPayCardUiState { + Active, + InProgress, +} + +internal enum class CardsProgressBannerUM { + Issuing, + Reissuing, +} + +internal fun TangemPayCardState.toUiState(): TangemPayCardUiState = when (this) { + TangemPayCardState.Active -> TangemPayCardUiState.Active + TangemPayCardState.Issuing, + TangemPayCardState.Reissuing, + TangemPayCardState.Closing, + -> TangemPayCardUiState.InProgress +} \ 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 56c15f7f88..7cea9f32f3 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 @@ -26,6 +26,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -37,12 +39,14 @@ import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider +import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.constraintlayout.compose.ConstrainedLayoutReference import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayoutScope import androidx.constraintlayout.compose.Dimension +import com.tangem.core.ui.components.SpacerH 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 @@ -60,9 +64,12 @@ import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.entity.CardDataType import com.tangem.features.tangempay.entity.DisplayNameState import com.tangem.features.tangempay.entity.TangemPayCardDetailsUM +import kotlin.math.roundToInt private const val TEXT_WIDTH_PADDING = 2 private const val FREEZE_ANIMATION_DURATION_MS = 600 +private const val CARD_WIDTH_RATIO = 328f +private const val CARD_HEIGHT_RATIO = 212f private val CustomCardBlockColor = Color(0x1F828282) private val CardBackgroundColor = Color(0xFF171A27) @@ -86,9 +93,10 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M CardBgWrapper( rotateCardY = rotateCardY, zAxisDistance = zAxisDistance, + shouldShowDetails = shouldShowDetails, modifier = modifier, - ) { - if (shouldShowDetails) { + front = { TangemPayCardDetailsHiddenBlock(state = state) }, + back = { TangemPayCardDetailsShownBlock( cardNumber = state.number, expiry = state.expiry, @@ -99,10 +107,8 @@ internal fun TangemPayCard(state: TangemPayCardDetailsUM, modifier: Modifier = M onHideDetails = state.onClick, modifier = Modifier.graphicsLayer { rotationY = 180f }, ) - } else { - TangemPayCardDetailsHiddenBlock(state = state) - } - } + }, + ) } @Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") @@ -111,7 +117,7 @@ private fun TangemPayCardDetailsHiddenBlock(state: TangemPayCardDetailsUM, modif Box(modifier = modifier.fillMaxSize()) { TangemPayCardBackground( modifier = Modifier - .fillMaxSize() + .matchParentSize() .zIndex(0f), cardFrozenState = state.cardFrozenState, ) @@ -219,6 +225,7 @@ private fun TangemPayCardBackground(cardFrozenState: TangemPayCardFrozenState, m modifier = Modifier.fillMaxSize(), painter = painterResource(R.drawable.img_tangem_pay_visa), contentDescription = null, + contentScale = ContentScale.FillBounds, ) if (isFrozen || freezeProgress > 0f) { @@ -228,25 +235,27 @@ private fun TangemPayCardBackground(cardFrozenState: TangemPayCardFrozenState, m .graphicsLayer { alpha = freezeProgress }, painter = painterResource(R.drawable.img_tangem_pay_visa_frozen), contentDescription = null, + contentScale = ContentScale.Crop, ) } } } -@Suppress("MagicNumber") +@Suppress("MagicNumber", "LongMethod") @Composable private fun CardBgWrapper( rotateCardY: Float, zAxisDistance: Float, + shouldShowDetails: Boolean, modifier: Modifier = Modifier, - content: @Composable BoxScope.() -> Unit, + back: @Composable () -> Unit, + front: @Composable () -> Unit, ) { val isRedesignEnabled = LocalVisaRedesignEnabled.current - val shouldShowDetailsBg = rotateCardY > 90f && isRedesignEnabled + val shouldShowDetailsBg = shouldShowDetails && isRedesignEnabled Box( modifier = modifier .fillMaxWidth() - .aspectRatio(328f / 212f) // size of img_tangem_pay_visa .graphicsLayer { rotationY = rotateCardY cameraDistance = zAxisDistance @@ -278,24 +287,77 @@ private fun CardBgWrapper( }, ), ) { - Box(modifier = Modifier.fillMaxWidth()) { - if (shouldShowDetailsBg) { - Image( + EqualHeightCardSides( + modifier = Modifier.fillMaxWidth(), + placeBackOnTop = shouldShowDetails, + front = { + Box( modifier = Modifier .fillMaxSize() - .graphicsLayer { - rotationY = rotateCardY - cameraDistance = zAxisDistance - }, - painter = painterResource(R.drawable.img_bg_card_details), - contentDescription = null, - ) + .graphicsLayer { alpha = if (shouldShowDetails) 0f else 1f }, + ) { front() } + }, + back = { + Box( + modifier = Modifier + .fillMaxSize() + .graphicsLayer { alpha = if (shouldShowDetails) 1f else 0f }, + ) { + if (shouldShowDetailsBg) { + Image( + modifier = Modifier + .matchParentSize() + .graphicsLayer { + rotationY = rotateCardY + cameraDistance = zAxisDistance + }, + painter = painterResource(R.drawable.img_bg_card_details), + contentDescription = null, + contentScale = ContentScale.Crop, + ) + } + back() + } + }, + ) + } +} + +@Composable +private fun EqualHeightCardSides( + placeBackOnTop: Boolean, + modifier: Modifier = Modifier, + back: @Composable () -> Unit, + front: @Composable () -> Unit, +) { + SubcomposeLayout(modifier) { constraints -> + val width = constraints.maxWidth + val minHeight = if (width == Constraints.Infinity) { + 0 + } else { + (width * CARD_HEIGHT_RATIO / CARD_WIDTH_RATIO).roundToInt() + } + + val naturalConstraints = constraints.copy(minHeight = 0, maxHeight = Constraints.Infinity) + val backNaturalHeight = subcompose(CardSide.BackMeasure, back) + .maxOfOrNull { it.measure(naturalConstraints).height } ?: 0 + val finalHeight = maxOf(minHeight, backNaturalHeight) + val sizeConstraints = constraints.copy(minHeight = finalHeight, maxHeight = finalHeight) + val frontPlaceables = subcompose(CardSide.Front, front).map { it.measure(sizeConstraints) } + val backPlaceables = subcompose(CardSide.Back, back).map { it.measure(sizeConstraints) } + layout(width, finalHeight) { + val ordered = if (placeBackOnTop) { + frontPlaceables + backPlaceables + } else { + backPlaceables + frontPlaceables } - content() + ordered.forEach { it.place(0, 0) } } } } +private enum class CardSide { BackMeasure, Front, Back } + @Composable private fun CardTopBlock(modifier: Modifier = Modifier) { Row( @@ -488,7 +550,7 @@ private fun EditingCardDisplayName(state: DisplayNameState.Editing, modifier: Mo LaunchedEffect(Unit) { focusRequester.requestFocus() } } -@Suppress("MagicNumber", "LongParameterList") +@Suppress("MagicNumber", "LongParameterList", "LongMethod") @Composable private fun TangemPayCardDetailsShownBlock( cardNumber: String, @@ -538,6 +600,7 @@ private fun TangemPayCardDetailsShownBlock( copyTestTag = TangemPayTestTags.CARD_DETAILS_COPY_CVC, ) } + SpacerH(8.dp) Spacer(modifier = Modifier.weight(1f)) Row { SpacerWMax() 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 8e5b386444..3be9ae9f4d 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 @@ -97,8 +97,6 @@ private fun TangemPayCardPageScreen( .fillMaxSize() .padding(scaffoldPaddings), contentPadding = PaddingValues( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, bottom = TangemTheme.dimens.spacing16 + bottomBarHeight, ), verticalArrangement = Arrangement.spacedBy(if (isRedesignEnabled) 0.dp else TangemTheme.dimens.spacing16), @@ -136,7 +134,10 @@ private fun TangemPayCardSwipePager( ) { when { controllers.isEmpty() -> Unit - controllers.size == 1 -> CardDetailsPage(controller = controllers.first(), modifier = modifier) + controllers.size == 1 -> CardDetailsPage( + controller = controllers.first(), + modifier = modifier.padding(horizontal = 16.dp), + ) else -> { val initialPage = controllers.indexOfFirst { it.cardId == selectedCardId }.coerceAtLeast(0) val pagerState = rememberPagerState(initialPage = initialPage) { controllers.size } @@ -328,7 +329,9 @@ private fun LazyListScope.cardPageItem( enter = fadeIn(animationSpec = tween(CONTENT_FADE_DURATION_MS)), exit = fadeOut(animationSpec = tween(CONTENT_FADE_DURATION_MS)), ) { - content() + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + content() + } } } } @@ -340,7 +343,12 @@ private fun TangemPayCardPageScreenPreviewV1() { TangemThemePreview { TangemPayCardPageScreen( state = TangemPayCardPageUM.stub(), - cardSection = { TangemPayCard(state = previewCardDetailsState()) }, + cardSection = { + TangemPayCard( + state = previewCardDetailsState(), + modifier = Modifier.padding(horizontal = 16.dp), + ) + }, ) } } @@ -371,7 +379,12 @@ private fun TangemPayCardPageScreenPreviewV2() { ) { TangemPayCardPageScreen( state = TangemPayCardPageUM.stub(), - cardSection = { TangemPayCard(state = previewCardDetailsState()) }, + cardSection = { + TangemPayCard( + state = previewCardDetailsState(), + modifier = Modifier.padding(horizontal = 16.dp), + ) + }, ) } } 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 6de094b16c..f4e2a0a1f5 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 @@ -33,7 +33,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.RectangleShimmer @@ -132,7 +131,8 @@ internal fun TangemPayDetailsScreen( }, ) - if (state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuingOrClosing } == true) { + val progressBanner = state.balanceBlockState.cardsBlockState?.progressBanner + if (progressBanner == CardsProgressBannerUM.Reissuing) { item( key = "REISSUE_MESSAGE", content = { @@ -315,6 +315,7 @@ private fun CardsBlockRow( @Composable private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modifier: Modifier = Modifier) { + val isCardInProcess = card.state == TangemPayCardUiState.InProgress Box( modifier = modifier .clip(RoundedCornerShape(4.dp)) @@ -329,7 +330,7 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi .alpha(if (card.isEnabled) 1f else DISABLED_ALPHA) .fillMaxSize(), painter = painterResource( - if (card.isReissuingOrClosing) { + if (isCardInProcess) { R.drawable.img_visa_card_inactive_48_32 } else { R.drawable.img_visa_card_48_32 @@ -337,7 +338,7 @@ private fun TangemPayCardItem(card: TangemPayDetailsBalanceBlockState.Card, modi ), contentDescription = null, ) - if (!card.isReissuingOrClosing) { + if (!isCardInProcess) { Text( modifier = Modifier .align(Alignment.BottomStart) @@ -486,18 +487,16 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider { + + if (state.errorNotificationConfig != null) { + item("errorSessionBannerBlock") { + ErrorMessage( + config = state.errorNotificationConfig, + modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), + ) + } + } + + val progressBanner = state.balanceBlockState.cardsBlockState?.progressBanner + + when (progressBanner) { + CardsProgressBannerUM.Reissuing -> { item("reissuingBannerBlock") { TangemMessage( modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), @@ -168,24 +182,25 @@ private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) { ) } } - state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isIssuing } == true -> { + CardsProgressBannerUM.Issuing -> { item("issuingBannerBlock") { TangemMessage( modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), title = resourceReference(R.string.tangempay_issuing_new_digital_card_title), subtitle = resourceReference(R.string.tangempay_reissue_card_in_progress_description), + contentColor = TangemTheme.colors3.bg.opaque.secondary, + leadingContent = { + Icon( + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_clock_24), + contentDescription = null, + tint = TangemTheme.colors3.icon.primary, + ) + }, ) } } - else -> { - if (state.errorNotificationConfig != null) { - item("errorSessionBannerBlock") { - ErrorMessage( - config = state.errorNotificationConfig, - modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x4), - ) - } - } + null -> { if (state.addToWalletBlockState != null) { item("addToWalletBannerBlock") { TangemPayAddToWalletBlock( @@ -375,7 +390,7 @@ private fun CardsBlock( ) { items(items = cardsBlockState.cards) { item -> TangemPayCardView( - isIssueInProgress = item.isIssuing || item.isReissuingOrClosing, + isIssueInProgress = item.state != TangemPayCardUiState.Active, lastDigits = item.lastDigits, onClick = item.onClick, isEnabled = item.isEnabled, diff --git a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayIssueAdditionalCardContent.kt b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayIssueAdditionalCardContent.kt index 38da0cd2bd..dcf9185741 100644 --- a/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayIssueAdditionalCardContent.kt +++ b/features/tangempay/details/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayIssueAdditionalCardContent.kt @@ -1,9 +1,11 @@ package com.tangem.features.tangempay.ui import android.content.res.Configuration +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -11,7 +13,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -37,7 +39,6 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.res.generated.icons.Icons import com.tangem.core.ui.res.generated.icons.ic_card_plus_32 -import com.tangem.core.ui.res.generated.icons.ic_error_28 import com.tangem.features.tangempay.details.impl.R import com.tangem.features.tangempay.entity.TangemPayIssueAdditionalCardUM @@ -72,47 +73,55 @@ internal fun TangemPayIssueAdditionalCardContent(state: TangemPayIssueAdditional @Composable private fun Content(state: TangemPayIssueAdditionalCardUM) { - val appearance = state.contentAppearance() - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - SpacerH(16.dp) - StatusIcon(appearance = appearance) - SpacerH(32.dp) - CenteredMessageText( - textRes = appearance.titleRes, - style = TangemTheme.typography3.heading.small, - color = TangemTheme.colors3.text.primary, - ) - CenteredMessageText( - textRes = appearance.subtitleRes, - style = TangemTheme.typography3.subheading.medium, - color = TangemTheme.colors3.text.secondary, - ) - SpacerH(32.dp) - FeeBlock(modifier = Modifier.padding(top = 16.dp), state = state) - SpacerH(8.dp) - BottomButtonsBlock(state = state, appearance = appearance) + Column(modifier = Modifier.fillMaxWidth()) { + Header() + FeeBlock(state = state) + if (state.isBalanceInsufficient) { + InsufficientFundsNotification(state = state) + } + IssueButton(state = state) } } @Composable -private fun StatusIcon(appearance: IssueAdditionalCardContentAppearance) { +private fun Header() { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + SpacerH(32.dp) + StatusIcon() + SpacerH(32.dp) + CenteredMessageText( + textRes = R.string.tangempay_issue_additional_card_title, + style = TangemTheme.typography3.heading.small, + color = TangemTheme.colors3.text.primary, + ) + SpacerH(8.dp) + CenteredMessageText( + textRes = R.string.tangempay_issue_additional_card_description, + style = TangemTheme.typography3.subheading.medium, + color = TangemTheme.colors3.text.secondary, + ) + SpacerH(32.dp) + } +} + +@Composable +private fun StatusIcon() { Box( modifier = Modifier .size(80.dp) .clip(CircleShape) - .background(appearance.iconBackgroundColor), + .background(TangemTheme.colors3.bg.status.infoSubtle), contentAlignment = Alignment.Center, ) { Icon( - imageVector = appearance.icon, + imageVector = Icons.ic_card_plus_32, contentDescription = null, - tint = appearance.iconColor, + tint = TangemTheme.colors3.icon.status.info, modifier = Modifier.size(28.dp), ) } @@ -152,68 +161,64 @@ private fun FeeBlock(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = } @Composable -private fun BottomButtonsBlock( - state: TangemPayIssueAdditionalCardUM, - appearance: IssueAdditionalCardContentAppearance, - modifier: Modifier = Modifier, -) { +private fun InsufficientFundsNotification(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) { Column( modifier = modifier .fillMaxWidth() - .padding(top = 16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + .padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp) + .clip(RoundedCornerShape(16.dp)) + .background(TangemTheme.colors3.bg.status.warningSubtle) + .padding(horizontal = 14.dp, vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Top, + ) { + Image( + modifier = Modifier.size(20.dp), + painter = painterResource(R.drawable.img_usdc_16), + contentDescription = null, + ) + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = stringResourceSafe(R.string.tangempay_reissue_card_insufficient_funds_title), + style = TangemTheme.typography3.subheading.medium, + color = TangemTheme.colors3.text.primary, + ) + Text( + text = stringResourceSafe(R.string.tangempay_reissue_card_insufficient_funds_subtitle), + style = TangemTheme.typography3.caption.medium, + color = TangemTheme.colors3.text.secondary, + ) + } + } TangemButton( modifier = Modifier.fillMaxWidth(), variant = TangemButton.Variant.Secondary, - size = TangemButton.Size.X12, - onClick = state.onDismiss, - text = resourceReference(R.string.common_cancel), - ) - TangemButton( - modifier = Modifier.fillMaxWidth(), - size = TangemButton.Size.X12, - onClick = appearance.primaryAction(state), - isEnabled = !state.isLoading, - isLoading = state.isLoading, - text = resourceReference(appearance.primaryButtonTextRes), + size = TangemButton.Size.X8, + onClick = state.onAddFundsClick, + text = resourceReference(R.string.tangempay_card_details_add_funds), ) } } -private data class IssueAdditionalCardContentAppearance( - val titleRes: Int, - val subtitleRes: Int, - val icon: ImageVector, - val iconColor: Color, - val iconBackgroundColor: Color, - val primaryButtonTextRes: Int, - val primaryAction: (TangemPayIssueAdditionalCardUM) -> () -> Unit, -) - @Composable -private fun TangemPayIssueAdditionalCardUM.contentAppearance(): IssueAdditionalCardContentAppearance { - return if (isBalanceInsufficient) { - IssueAdditionalCardContentAppearance( - titleRes = R.string.tangempay_reissue_card_insufficient_funds_title, - subtitleRes = R.string.tangempay_reissue_card_insufficient_funds_subtitle, - icon = Icons.ic_error_28, - iconColor = TangemTheme.colors3.icon.status.warning, - iconBackgroundColor = TangemTheme.colors3.bg.status.warningSubtle, - primaryButtonTextRes = R.string.tangempay_card_details_add_funds, - primaryAction = { it.onAddFundsClick }, - ) - } else { - IssueAdditionalCardContentAppearance( - titleRes = R.string.tangempay_issue_additional_card_title, - subtitleRes = R.string.tangempay_issue_additional_card_description, - icon = Icons.ic_card_plus_32, - iconColor = TangemTheme.colors3.icon.status.info, - iconBackgroundColor = TangemTheme.colors3.bg.status.infoSubtle, - primaryButtonTextRes = R.string.tangempay_issue_card, - primaryAction = { it.onIssueClick }, - ) - } +private fun IssueButton(state: TangemPayIssueAdditionalCardUM, modifier: Modifier = Modifier) { + TangemButton( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + size = TangemButton.Size.X12, + onClick = state.onIssueClick, + isEnabled = !state.isLoading && !state.isBalanceInsufficient, + isLoading = state.isLoading, + text = resourceReference(R.string.tangempay_issue_card), + ) } @Preview(showBackground = true, widthDp = 360) @@ -223,29 +228,7 @@ private fun TangemPayIssueAdditionalCardContentPreview( @PreviewParameter(IssueAdditionalCardPreviewProvider::class) state: TangemPayIssueAdditionalCardUM, ) { TangemThemePreviewRedesign { - IssueAdditionalCardSheetPreview(state = state) - } -} - -@Composable -private fun IssueAdditionalCardSheetPreview(state: TangemPayIssueAdditionalCardUM) { - Column( - modifier = Modifier - .fillMaxWidth() - .background(TangemTheme.colors3.bg.secondary), - ) { - TangemTopBar( - type = TangemTopBarType.BottomSheet, - endContent = { - TangemButton( - iconStart = TangemIconUM.Icon(iconRes = R.drawable.ic_close_24), - onClick = state.onDismiss, - size = TangemButton.Size.X11, - variant = TangemButton.Variant.Material, - ) - }, - ) - Content(state) + TangemPayIssueAdditionalCardContent(state = state) } } diff --git a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt index deecec789f..ec5d9d03c8 100644 --- a/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt +++ b/features/tangempay/main/impl/src/main/kotlin/com/tangem/features/tangempay/ui/TangemPayMainBlockContent.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -30,6 +31,7 @@ import com.tangem.core.ui.components.text.applyBladeBrush import com.tangem.core.ui.ds.row.TangemRowContainer import com.tangem.core.ui.ds.row.TangemRowLayoutId import com.tangem.core.ui.extensions.* +import com.tangem.core.ui.res.LocalRedesignEnabled import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.core.ui.test.TangemPayTestTags @@ -96,7 +98,7 @@ private fun TangemPayMainContent( .testTag(TangemPayTestTags.MAIN_SCREEN_TILE), ) { Image( - painter = painterResource(R.drawable.img_visa_36), + painter = getVisaIconPainter(), contentDescription = null, modifier = Modifier .layoutId(TangemRowLayoutId.HEAD) @@ -145,7 +147,7 @@ private fun TangemPayStateRow( .conditional(onClick != null && isEnabled) { clickableSingle(onClick = requireNotNull(onClick)) }, ) { Image( - painter = painterResource(R.drawable.img_visa_36), + painter = getVisaIconPainter(), contentDescription = null, modifier = Modifier .layoutId(TangemRowLayoutId.HEAD) @@ -274,6 +276,12 @@ private fun TangemPayMainLoading(modifier: Modifier = Modifier) { } } +@Composable +private fun getVisaIconPainter(): Painter { + val resource = if (LocalRedesignEnabled.current) R.drawable.ic_visa_in_banner else R.drawable.img_visa_36 + return painterResource(resource) +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) diff --git a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/TokenDetailsFeatureToggles.kt b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/TokenDetailsFeatureToggles.kt index 71f92a9e18..5cf0e73735 100644 --- a/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/TokenDetailsFeatureToggles.kt +++ b/features/tokendetails/api/src/main/kotlin/com/tangem/features/tokendetails/TokenDetailsFeatureToggles.kt @@ -2,4 +2,5 @@ package com.tangem.features.tokendetails interface TokenDetailsFeatureToggles { val isQuickTopUpEnabled: Boolean + val isManageFundsEnabled: Boolean } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt index 50c9102fa5..aec7dd1d42 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsComponent.kt @@ -22,7 +22,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.route.TokenDeta import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreen import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetailsScreenLegacy import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent -import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent @@ -47,7 +47,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( expressTransactionsComponentFactory: ExpressTransactionsComponent.Factory, private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory, private val yieldSupplyWarningComponentFactory: YieldSupplyDepositedWarningComponent.Factory, - private val addFundsComponentFactory: AddFundsComponent.Factory, + private val manageFundsComponentFactory: ManageFundsComponent.Factory, yieldSupplyComponentFactory: YieldSupplyComponent.Factory, private val ratingComponentFactory: RatingComponent.Factory, ) : TokenDetailsComponent, AppComponentContext by appComponentContext { @@ -178,10 +178,10 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor( dynamicAddressesDelegate = model.dynamicAddressesDelegate, onDismiss = model.bottomSheetNavigation::dismiss, ) - is TokenDetailsBottomSheetConfig.AddFunds -> addFundsComponentFactory.create( + is TokenDetailsBottomSheetConfig.AddFunds -> manageFundsComponentFactory.create( context = childByContext(componentContext), - params = AddFundsComponent.Params( - launchMode = AddFundsComponent.LaunchMode.TokenActionsOnly( + params = ManageFundsComponent.Params( + launchMode = ManageFundsComponent.LaunchMode.TokenActionsOnly( userWalletId = route.userWalletId, currency = route.currency, ), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsFeatureToggles.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsFeatureToggles.kt index c3d81a3a9b..24b973acdd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsFeatureToggles.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/DefaultTokenDetailsFeatureToggles.kt @@ -6,10 +6,13 @@ import com.tangem.features.tokendetails.TokenDetailsFeatureToggles import javax.inject.Inject internal class DefaultTokenDetailsFeatureToggles @Inject constructor( - featureTogglesManager: FeatureTogglesManager, + private val featureTogglesManager: FeatureTogglesManager, ) : TokenDetailsFeatureToggles { override val isQuickTopUpEnabled: Boolean = featureTogglesManager.isFeatureEnabled( toggle = FeatureToggles.AND_15258_QUICK_TOP_UP_ENABLED, ) + + override val isManageFundsEnabled: Boolean + get() = featureTogglesManager.isFeatureEnabled(FeatureToggles.TWI_1377_MANAGE_FUNDS) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt index 6ae77ff99c..319472b5ea 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsClickIntents.kt @@ -22,6 +22,8 @@ interface TokenDetailsClickIntents { fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason) + fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) fun onBuyClick(unavailabilityReason: ScenarioUnavailabilityReason) @@ -151,6 +153,8 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents { override fun onSwapFromClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } + override fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } + override fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) { /* no op */ } override fun onHideClick() { /* no op */ } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index 2e1bbdb28f..8c9f7b835c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -599,6 +599,15 @@ internal class TokenDetailsModel @Inject constructor( } override fun onTransferClick() { + val amount = cryptoCurrencyStatus?.value?.amount + if (amount == null || amount.signum() <= 0) { + uiMessageSender.send( + message = SnackbarMessage( + message = resourceReference(R.string.token_button_unavailability_reason_empty_balance_send), + ), + ) + return + } bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.Transfer) } @@ -832,6 +841,20 @@ internal class TokenDetailsModel @Inject constructor( handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.FROM, checkYieldSupply = true) } + override fun onSwapAndSendClick(unavailabilityReason: ScenarioUnavailabilityReason) { + if (handleUnavailabilityReason(unavailabilityReason = unavailabilityReason)) { + return + } + + appRouter.push( + AppRoute.SendEntryPoint( + userWalletId = userWalletId, + currency = cryptoCurrency, + shouldStartWithSwap = true, + ), + ) + } + override fun onSwapToClick(unavailabilityReason: ScenarioUnavailabilityReason) { handleSwap(unavailabilityReason, AppRoute.Swap.CurrencyPosition.TO, checkYieldSupply = false) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TransferUM.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TransferUM.kt index 7f7d1ee2b9..946c563e64 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TransferUM.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TransferUM.kt @@ -21,6 +21,7 @@ internal sealed interface TransferUM : TangemBottomSheetConfigContent { data class Content( val send: Row?, val swap: Row?, + val swapAndSend: Row?, val sell: Row?, ) : TransferUM diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt index c87c35954e..87a40e29dd 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformer.kt @@ -43,6 +43,16 @@ internal class UpdateTransferTransformer( }, ) } + val swapAndSendRow = swapAction?.let { action -> + TransferUM.Row( + isLoading = action.unavailabilityReason.isLoading, + isEnabled = action.unavailabilityReason == ScenarioUnavailabilityReason.None, + onClick = { + onActionDispatched() + clickIntents.onSwapAndSendClick(action.unavailabilityReason) + }, + ) + } val sellRow = sellAction?.let { action -> TransferUM.Row( isLoading = action.unavailabilityReason.isOutdatedLoading(), @@ -55,7 +65,12 @@ internal class UpdateTransferTransformer( } return prevState.copy( - transferUM = TransferUM.Content(send = sendRow, swap = swapRow, sell = sellRow), + transferUM = TransferUM.Content( + send = sendRow, + swap = swapRow, + swapAndSend = swapAndSendRow, + sell = sellRow, + ), ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetContent.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetContent.kt index c784518a3e..99a2cc8b34 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetContent.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/bottomsheet/TransferBottomSheetContent.kt @@ -36,6 +36,7 @@ internal fun TransferBottomSheetContent(state: TransferUM, onCloseClick: () -> U ) { SendActionRow(state = state) SwapActionRow(state = state) + SwapAndSendActionRow(state = state) SellActionRow(state = state) SpacerH(TangemTheme.dimens2.x2) @@ -80,6 +81,19 @@ private fun SwapActionRow(state: TransferUM) { ) } +@Composable +private fun SwapAndSendActionRow(state: TransferUM) { + val row = (state as? TransferUM.Content)?.swapAndSend + if (state is TransferUM.Content && row == null) return + ActionRow( + iconRes = CoreR.drawable.ic_exchange_mini_24, + title = resourceReference(CoreR.string.common_send_with_swap), + description = resourceReference(CoreR.string.quick_action_send_and_swap_description), + row = row, + isLoading = state is TransferUM.Loading, + ) +} + @Composable private fun SellActionRow(state: TransferUM) { val row = (state as? TransferUM.Content)?.sell @@ -149,16 +163,19 @@ private class TransferPreviewProvider : PreviewParameterProvider { TransferUM.Content( send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), swap = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), + swapAndSend = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), sell = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), ), TransferUM.Content( send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), swap = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}), + swapAndSend = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}), sell = TransferUM.Row(isLoading = false, isEnabled = false, onClick = {}), ), TransferUM.Content( send = TransferUM.Row(isLoading = false, isEnabled = true, onClick = {}), swap = null, + swapAndSend = null, sell = null, ), ) diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt index 6391fb3cef..c4354538ab 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/transformer/UpdateTransferTransformerTest.kt @@ -319,6 +319,57 @@ class UpdateTransferTransformerTest { assertThat(content.swap?.isEnabled).isTrue() } + @Test + fun `GIVEN Swap action available WHEN transform THEN swapAndSend row is not null`() { + // Arrange + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false)), + ) + + // Act + val result = transformer.transform(initialState()) + + // Assert + val content = result.transferUM as TransferUM.Content + assertThat(content.swapAndSend).isNotNull() + } + + @Test + fun `GIVEN Swap action available WHEN swapAndSend onClick invoked THEN onSwapAndSendClick is called`() { + // Arrange + val transformer = createTransformer( + actions = listOf(TokenActionsState.ActionState.Swap(ScenarioUnavailabilityReason.None, false)), + ) + + // Act + val content = transformer.transform(initialState()).transferUM as TransferUM.Content + content.swapAndSend!!.onClick() + + // Assert + verifyOrder { + onActionDispatched.invoke() + clickIntents.onSwapAndSendClick(ScenarioUnavailabilityReason.None) + } + } + + @Test + fun `GIVEN no Swap action WHEN transform THEN swapAndSend row is null`() { + // Arrange + val transformer = createTransformer( + actions = listOf( + TokenActionsState.ActionState.Send(ScenarioUnavailabilityReason.None), + TokenActionsState.ActionState.Sell(ScenarioUnavailabilityReason.None), + ), + ) + + // Act + val result = transformer.transform(initialState()) + + // Assert + val content = result.transferUM as TransferUM.Content + assertThat(content.swapAndSend).isNull() + } + @Test fun `GIVEN row WHEN not clicked THEN no callbacks fire`() { // GIVEN diff --git a/features/txhistory/impl/build.gradle.kts b/features/txhistory/impl/build.gradle.kts index 04d8147aa1..2e1f349d03 100644 --- a/features/txhistory/impl/build.gradle.kts +++ b/features/txhistory/impl/build.gradle.kts @@ -9,6 +9,12 @@ plugins { android { namespace = "com.tangem.features.txhistory.impl" + + packaging { + resources { + merges += "paymentrequest.proto" + } + } } dependencies { /* Project - API */ diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverter.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverter.kt index 30d4fa4243..afd4459fd5 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverter.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverter.kt @@ -83,7 +83,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = stringReference(type.name), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()), + subtitle = tx.extractAddressSubtitle(), ) private fun swapContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content = @@ -92,7 +92,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = tx.statusAwareTitle(R.string.common_swapping, R.string.common_swapped), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()), + subtitle = tx.extractAddressSubtitle(), ) private fun transferContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content { @@ -112,7 +112,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( rawAddress = counterpartyAddress, briefAddress = counterpartyAddress.toBriefAddressFormat(), ) - else -> ContentSubtitle.Plain(tx.extractSubtitleByAddressType()) + else -> tx.extractAddressSubtitle() } return buildContent( @@ -145,7 +145,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.yield_module_transaction_topup), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), ) private fun yieldDeployContractContent( @@ -157,7 +157,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.yield_module_transaction_deploy_contract), iconRes = R.drawable.ic_doc_24, - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), ) private fun yieldInitializeTokenContent( @@ -169,7 +169,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.yield_module_transaction_initialize), iconRes = R.drawable.ic_gear_24, - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), ) private fun yieldReactivateTokenContent( @@ -181,7 +181,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.yield_module_transaction_reactivate), iconRes = R.drawable.ic_refresh_24, - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), ) private fun yieldSendContent( @@ -197,7 +197,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( resourceReference(R.string.common_transfer) }, iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.yieldSupplySubtitle(currency, type)), + subtitle = tx.yieldSupplySubtitle(currency, type), hideAmount = currency is CryptoCurrency.Token && !tx.isOutgoing, ) @@ -209,7 +209,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.transaction_history_operation), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()), + subtitle = tx.extractAddressSubtitle(), ) private fun gaslessFeeContent(tx: TxInfo, uiStatus: TransactionItemUM.Content.Status): TransactionItemUM.Content = @@ -218,7 +218,7 @@ internal class TxHistoryItemToTransactionItemUMConverter( uiStatus = uiStatus, title = resourceReference(R.string.gasless_transaction_fee), iconRes = tx.directionalIcon(), - subtitle = ContentSubtitle.Plain(tx.extractSubtitleByAddressType()), + subtitle = tx.extractAddressSubtitle(), ) private fun buildContent( @@ -283,15 +283,21 @@ private fun resolveOwnSubtitle( } } -private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: TransactionType.YieldSupply): TextReference { +private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: TransactionType.YieldSupply): ContentSubtitle { if (currency is CryptoCurrency.Coin) { return if (type is TransactionType.YieldSupply.Send) { - extractSubtitleByAddressType() + extractAddressSubtitle() } else { - resourceReference( + val briefAddress = type.address?.toBriefAddressFormat() + val text = resourceReference( R.string.transaction_history_transaction_for_address, - wrappedList(type.address?.toBriefAddressFormat().orEmpty()), + wrappedList(briefAddress.orEmpty()), ) + if (briefAddress.isNullOrEmpty()) { + ContentSubtitle.Plain(text) + } else { + ContentSubtitle.PlainAddress(text = text, highlight = briefAddress) + } } } return when (type) { @@ -304,15 +310,34 @@ private fun TxInfo.yieldSupplySubtitle(currency: CryptoCurrency, type: Transacti is TransactionType.YieldSupply.Send -> if (!isOutgoing && type.isYieldSupplyWithdraw) { amountSubtitle(currency, R.string.yield_module_transaction_exit_subtitle) } else { - extractSubtitleByAddressType() + extractAddressSubtitle() } - else -> extractSubtitleByAddressType() + else -> extractAddressSubtitle() } } -private fun TxInfo.amountSubtitle(currency: CryptoCurrency, @StringRes resId: Int): TextReference { +private fun TxInfo.amountSubtitle(currency: CryptoCurrency, @StringRes resId: Int): ContentSubtitle.Plain { val formatted = amount.format { crypto(symbol = currency.symbol, decimals = currency.decimals) } - return resourceReference(resId, wrappedList(formatted)) + return ContentSubtitle.Plain(resourceReference(resId, wrappedList(formatted))) +} + +private fun TxInfo.extractAddressSubtitle(): ContentSubtitle { + val text = extractSubtitleByAddressType() + val highlight = interactionAddressType.highlightableBriefAddress() + return if (highlight == null) { + ContentSubtitle.Plain(text) + } else { + ContentSubtitle.PlainAddress(text = text, highlight = highlight) + } +} + +private fun TxInfo.InteractionAddressType?.highlightableBriefAddress(): String? = when (this) { + is TxInfo.InteractionAddressType.Contract -> address.toBriefAddressFormat() + is TxInfo.InteractionAddressType.User -> address.toBriefAddressFormat() + is TxInfo.InteractionAddressType.Validator -> address.toBriefAddressFormat() + is TxInfo.InteractionAddressType.Multiple, + null, + -> null } private fun TxInfo.extractSubtitleByAddressType(): TextReference = diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/di/DefaultTxHistoryFeatureToggles.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/di/DefaultTxHistoryFeatureToggles.kt index 96448a0f83..6eaf006671 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/di/DefaultTxHistoryFeatureToggles.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/di/DefaultTxHistoryFeatureToggles.kt @@ -2,7 +2,7 @@ package com.tangem.features.txhistory.di import com.tangem.core.configtoggle.FeatureToggles import com.tangem.core.configtoggle.feature.FeatureTogglesManager -import com.tangem.features.txhistory.TxHistoryFeatureToggles +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import javax.inject.Inject internal class DefaultTxHistoryFeatureToggles @Inject constructor( diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/di/TxHistoryFeatureModule.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/di/TxHistoryFeatureModule.kt index bb46e57bfb..fafee1ffa7 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/di/TxHistoryFeatureModule.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/di/TxHistoryFeatureModule.kt @@ -1,6 +1,6 @@ package com.tangem.features.txhistory.di -import com.tangem.features.txhistory.TxHistoryFeatureToggles +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import com.tangem.features.txhistory.component.DefaultTxHistoryComponent import com.tangem.features.txhistory.component.DefaultTxHistoryDetailsComponent import com.tangem.features.txhistory.component.TxHistoryComponent diff --git a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt index 76c93935a5..b64a5d10d9 100644 --- a/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt +++ b/features/txhistory/impl/src/main/kotlin/com/tangem/features/txhistory/model/TxHistoryModel.kt @@ -26,7 +26,7 @@ import com.tangem.domain.txhistory.repository.TxHistoryRepositoryV2 import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.wallets.usecase.GetWalletIconUseCase -import com.tangem.features.txhistory.TxHistoryFeatureToggles +import com.tangem.domain.txhistory.TxHistoryFeatureToggles import com.tangem.features.txhistory.component.TxHistoryComponent import com.tangem.features.txhistory.converter.TxHistoryInfoToTransactionItemUMConverter import com.tangem.features.txhistory.converter.TxHistoryItemToTransactionItemUMConverter diff --git a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt index 1b8e49f8db..ae76d9a485 100644 --- a/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt +++ b/features/txhistory/impl/src/test/kotlin/com/tangem/features/txhistory/converter/TxHistoryItemToTransactionItemUMConverterTest.kt @@ -341,7 +341,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest { } @Test - fun `GIVEN Transfer with non-User interaction WHEN convert THEN Plain subtitle`() { + fun `GIVEN Transfer with non-User interaction WHEN convert THEN PlainAddress subtitle`() { val tx = txInfo( type = TransactionType.Transfer, isOutgoing = true, @@ -350,7 +350,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest { val result = coinConverter.convert(tx) as TransactionItemUM.Content - assertThat(result.subtitle).isInstanceOf(ContentSubtitle.Plain::class.java) + assertThat(result.subtitle).isInstanceOf(ContentSubtitle.PlainAddress::class.java) assertThat(result.title).isEqualTo(resRef(R.string.common_sent)) } @@ -575,7 +575,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest { val result = coinConverter.convert(tx) as TransactionItemUM.Content - val subtitle = result.subtitle as ContentSubtitle.Plain + val subtitle = result.subtitle as ContentSubtitle.PlainAddress val res = subtitle.text as TextReference.Res assertThat(res.id).isEqualTo(R.string.transaction_history_contract_address) } @@ -623,7 +623,7 @@ internal class TxHistoryItemToTransactionItemUMConverterTest { val result = coinConverter.convert(tx) as TransactionItemUM.Content - val subtitle = result.subtitle as ContentSubtitle.Plain + val subtitle = result.subtitle as ContentSubtitle.PlainAddress val res = subtitle.text as TextReference.Res assertThat(res.id).isEqualTo(R.string.transaction_history_transaction_validator) } diff --git a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt index 7c72192ae5..d61d46ab1f 100644 --- a/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt +++ b/features/wallet/api/src/main/kotlin/com/tangem/features/wallet/featuretoggles/WalletFeatureToggles.kt @@ -10,4 +10,6 @@ interface WalletFeatureToggles { val isAddAndManageTokensEnabled: Boolean val isAddFundsStage1Enabled: Boolean + + val isManageFundsEnabled: Boolean } \ 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 b2c1be6913..87135b6a10 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 @@ -25,7 +25,7 @@ import com.tangem.core.ui.utils.parseBigDecimal import com.tangem.domain.tokens.model.details.TokenAction import com.tangem.feature.wallet.child.managetokens.AddAndManageBottomSheetComponent import com.tangem.feature.wallet.child.organizetokens.OrganizeTokensComponent -import com.tangem.features.commonfeatures.api.addfunds.AddFundsComponent +import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent import com.tangem.feature.wallet.child.tokenActions.DefaultTokenActionsComponent import com.tangem.feature.wallet.child.tokenActions.TokenActionsComponent import com.tangem.feature.wallet.child.wallet.model.WalletModel @@ -70,7 +70,7 @@ internal class WalletComponent @AssistedInject constructor( private val networkSelectionComponentFactory: NetworkSelectionComponent.Factory, private val tokenActionsComponentFactory: TokenActionsComponent.Factory, private val portfolioSelectorComponentFactory: PortfolioSelectorComponent.Factory, - private val addFundsComponentFactory: AddFundsComponent.Factory, + private val manageFundsComponentFactory: ManageFundsComponent.Factory, private val designFeatureToggles: DesignFeatureToggles, ) : ComposableContentComponent, AppComponentContext by appComponentContext { @@ -191,10 +191,20 @@ internal class WalletComponent @AssistedInject constructor( ) } is WalletDialogConfig.AddFunds -> { - addFundsComponentFactory.create( + manageFundsComponentFactory.create( context = childByContext(componentContext), - params = AddFundsComponent.Params( - launchMode = AddFundsComponent.LaunchMode.ChooseToken(dialogConfig.userWalletId), + params = ManageFundsComponent.Params( + launchMode = ManageFundsComponent.LaunchMode.ChooseToken(dialogConfig.userWalletId), + onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, + ), + ) + } + is WalletDialogConfig.Transfer -> { + manageFundsComponentFactory.create( + context = childByContext(componentContext), + params = ManageFundsComponent.Params( + launchMode = ManageFundsComponent.LaunchMode.ChooseToken(dialogConfig.userWalletId), + flowType = ManageFundsComponent.FlowType.Transfer, onDismiss = model.innerWalletRouter.dialogNavigation::dismiss, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt index 71d49f3081..644d1602c5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/WalletModel.kt @@ -566,6 +566,7 @@ internal class WalletModel @Inject constructor( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled, + isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled, ), ) @@ -613,6 +614,7 @@ internal class WalletModel @Inject constructor( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled, + isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled, ), ) } @@ -635,6 +637,7 @@ internal class WalletModel @Inject constructor( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled, + isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled, ), ) } @@ -650,6 +653,7 @@ internal class WalletModel @Inject constructor( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled, + isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled, ), ) @@ -712,6 +716,7 @@ internal class WalletModel @Inject constructor( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = walletFeatureToggles.isAddFundsStage1Enabled, + isManageFundsEnabled = walletFeatureToggles.isManageFundsEnabled, ), ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt index 952cf4764d..d983fe385c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/wallet/model/intents/WalletClickIntents.kt @@ -124,6 +124,10 @@ internal class WalletClickIntents @Inject constructor( router.openAddFunds(userWalletId) } + fun onTransferClick(userWalletId: UserWalletId) { + router.openTransfer(userWalletId) + } + fun onAddFundsPromoClick(userWalletId: UserWalletId) { analyticsEventHandler.send(WalletScreenAnalyticsEvent.MainScreen.ButtonAddFundsPromo()) router.openAddFunds(userWalletId) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt index 1695e2fe76..d465ad74b7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/featuretoggles/DefaultWalletFeatureToggles.kt @@ -14,4 +14,7 @@ internal class DefaultWalletFeatureToggles @Inject constructor( override val isAddFundsStage1Enabled: Boolean get() = featureToggles.isFeatureEnabled(FeatureToggles.AND_15310_ADD_FUNDS_STAGE1) + + override val isManageFundsEnabled: Boolean + get() = featureToggles.isFeatureEnabled(FeatureToggles.TWI_1377_MANAGE_FUNDS) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt index 3a66c7a075..10e80ff7c5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/DefaultWalletRouter.kt @@ -125,6 +125,12 @@ internal class DefaultWalletRouter @Inject constructor( ) } + override fun openTransfer(userWalletId: UserWalletId) { + dialogNavigation.activate( + configuration = WalletDialogConfig.Transfer(userWalletId = userWalletId), + ) + } + override fun isWalletLastScreen(): Boolean { return router.stack.lastOrNull() is AppRoute.Wallet } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt index 456ee387ec..eed653fcfe 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/router/InnerWalletRouter.kt @@ -120,4 +120,7 @@ internal interface InnerWalletRouter { /** Open Add Funds screen */ fun openAddFunds(userWalletId: UserWalletId) + + /** Open Transfer screen */ + fun openTransfer(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt index 158d3ad6e5..31daa69aca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletActionButtons.kt @@ -56,7 +56,7 @@ internal sealed class WalletActionButtons( override val isEnabled: Boolean, ) : WalletActionButtons( text = resourceReference(R.string.common_add_funds), - iconRes = R.drawable.ic_plus_default_24, + iconRes = R.drawable.ic_arrow_down_24, ) data class Swap( @@ -74,4 +74,12 @@ internal sealed class WalletActionButtons( text = resourceReference(R.string.common_sell), iconRes = R.drawable.ic_dollar_default_24, ) + + data class Transfer( + override val onClick: () -> Unit, + override val isEnabled: Boolean, + ) : WalletActionButtons( + text = resourceReference(R.string.common_transfer), + iconRes = R.drawable.ic_arrow_up_24, + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt index afa05fb850..a21b1a2fb5 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletDialogConfig.kt @@ -65,6 +65,9 @@ internal sealed interface WalletDialogConfig { @Serializable data class AddFunds(val userWalletId: UserWalletId) : WalletDialogConfig + @Serializable + data class Transfer(val userWalletId: UserWalletId) : WalletDialogConfig + @Serializable data class OrganizeTokens(val userWalletId: UserWalletId) : WalletDialogConfig diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt index f2ab77907e..449087b947 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletManageButton.kt @@ -58,7 +58,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { ) : WalletManageButton( config = ActionButtonConfig( text = TextReference.Res(id = R.string.common_add_funds), - iconResId = R.drawable.ic_plus_24, + iconResId = R.drawable.ic_arrow_down_24, onClick = onClick, isEnabled = enabled, shouldDimContent = dimContent, @@ -145,6 +145,29 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { ), ) + /** + * Transfer + * + * @property enabled button click availability + * @property dimContent determines whether the button content will be dimmed + * @property onClick lambda be invoked when Transfer button is clicked + */ + data class Transfer( + override val enabled: Boolean, + override val dimContent: Boolean, + override val onClick: () -> Unit, + val isInProgress: Boolean = false, + ) : WalletManageButton( + config = ActionButtonConfig( + text = TextReference.Res(id = R.string.common_transfer), + iconResId = R.drawable.ic_arrow_up_24, + onClick = onClick, + isEnabled = enabled, + shouldDimContent = dimContent, + isInProgress = isInProgress, + ), + ) + /** * Swap * diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt index e6c36a1e49..4432079a8c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotificationUM.kt @@ -328,8 +328,7 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t isLoading = shouldShowProgress, ), ), - messageEffect = TangemMessageEffect.Card, - isCentered = true, + messageEffect = TangemMessageEffect.Warning, ), type = WalletNotificationType.Warning, ) @@ -339,9 +338,10 @@ internal sealed class WalletNotificationUM(val messageUM: TangemMessageUM, val t id = "TangemPayUnreachable", title = resourceReference(id = R.string.tangempay_temporarily_unavailable), subtitle = resourceReference(id = R.string.tangempay_service_unreachable_try_later), + messageEffect = TangemMessageEffect.Warning, iconUM = TangemIconUM.Icon( - iconRes = R.drawable.ic_attention_default_24, - tintReference = { TangemTheme.colors2.graphic.status.attention }, + iconRes = R.drawable.ic_alert_circle_24, + tintReference = { TangemTheme.colors2.graphic.neutral.primary }, ), ), type = WalletNotificationType.Warning, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt index f41f1471ae..f33e2908eb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/AddWalletTransformer.kt @@ -14,6 +14,7 @@ internal class AddWalletTransformer( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isAddFundsStage1Enabled: Boolean, + private val isManageFundsEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -22,6 +23,7 @@ internal class AddWalletTransformer( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = isAddFundsStage1Enabled, + isManageFundsEnabled = isManageFundsEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt index d6ac34d9eb..edfe70df42 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/InitializeWalletsTransformer.kt @@ -21,6 +21,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import com.tangem.core.ui.R as CoreUiR +@Suppress("LongParameterList") internal class InitializeWalletsTransformer( private val selectedWalletIndex: Int, private val wallets: List, @@ -28,6 +29,7 @@ internal class InitializeWalletsTransformer( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isAddFundsStage1Enabled: Boolean, + private val isManageFundsEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -36,6 +38,7 @@ internal class InitializeWalletsTransformer( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = isAddFundsStage1Enabled, + isManageFundsEnabled = isManageFundsEnabled, ) } @@ -159,10 +162,16 @@ internal class InitializeWalletsTransformer( WalletManageButton.Buy(enabled = false, dimContent = false, onClick = {}) } + val lastButton = if (isManageFundsEnabled) { + WalletManageButton.Transfer(enabled = false, dimContent = false, onClick = {}) + } else { + WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}) + } + return persistentListOf( firstButton, WalletManageButton.Swap(enabled = false, dimContent = false, onClick = {}), - WalletManageButton.Sell(enabled = false, dimContent = false, onClick = {}), + lastButton, ) } @@ -190,12 +199,21 @@ internal class InitializeWalletsTransformer( onClick = {}, ).buttonUM, ) - add( - WalletActionButtons.Sell( - isEnabled = false, - onClick = {}, - ).buttonUM, - ) + if (isManageFundsEnabled) { + add( + WalletActionButtons.Transfer( + isEnabled = false, + onClick = {}, + ).buttonUM, + ) + } else { + add( + WalletActionButtons.Sell( + isEnabled = false, + onClick = {}, + ).buttonUM, + ) + } }.toPersistentList() } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt index 2a7f70ad83..66fa7bb5b2 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeNewWalletTransformer.kt @@ -18,6 +18,7 @@ import kotlinx.collections.immutable.toImmutableList * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class ReinitializeNewWalletTransformer( private val prevWalletId: UserWalletId, private val newUserWallet: UserWallet, @@ -25,6 +26,7 @@ internal class ReinitializeNewWalletTransformer( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isAddFundsStage1Enabled: Boolean, + private val isManageFundsEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -33,6 +35,7 @@ internal class ReinitializeNewWalletTransformer( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = isAddFundsStage1Enabled, + isManageFundsEnabled = isManageFundsEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt index 1729b92b47..5032c7975a 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/ReinitializeWalletTransformer.kt @@ -21,6 +21,7 @@ internal class ReinitializeWalletTransformer( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isAddFundsStage1Enabled: Boolean, + private val isManageFundsEnabled: Boolean, ) : WalletStateTransformer(userWalletId = userWallet.walletId) { private val walletLoadingStateFactory by lazy { @@ -29,6 +30,7 @@ internal class ReinitializeWalletTransformer( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = isAddFundsStage1Enabled, + isManageFundsEnabled = isManageFundsEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt index 9ad9b59660..c90b41d9bb 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetRefreshStateTransformer.kt @@ -87,6 +87,7 @@ internal class SetRefreshStateTransformer( is WalletManageButton.AddFunds -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Send -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Sell -> button.copy(enabled = isButtonsEnabled) + is WalletManageButton.Transfer -> button.copy(enabled = isButtonsEnabled) is WalletManageButton.Receive -> button is WalletManageButton.Stake -> null is WalletManageButton.Swap -> null diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt index 5b4c0c0404..c64e5fa443 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/UnlockWalletTransformer.kt @@ -19,6 +19,7 @@ internal class UnlockWalletTransformer( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isAddFundsStage1Enabled: Boolean, + private val isManageFundsEnabled: Boolean, ) : WalletScreenStateTransformer { private val walletLoadingStateFactory by lazy { @@ -27,6 +28,7 @@ internal class UnlockWalletTransformer( walletImageResolver = walletImageResolver, getWalletIconUseCase = getWalletIconUseCase, isAddFundsStage1Enabled = isAddFundsStage1Enabled, + isManageFundsEnabled = isManageFundsEnabled, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt index 095f5d5654..2147e17885 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/MultiWalletActionsExt.kt @@ -45,6 +45,7 @@ private fun WalletState.MultiCurrency.Content.changeAvailability(enabled: Boolea is WalletManageButton.Buy -> action.copy(enabled = enabled) is WalletManageButton.AddFunds -> action.copy(enabled = enabled) is WalletManageButton.Sell -> action.copy(enabled = enabled) + is WalletManageButton.Transfer -> action.copy(enabled = enabled) is WalletManageButton.Swap -> action.copy(enabled = enabled) else -> action } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt index 08d096da53..1410822ee8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/utils/WalletLoadingStateFactory.kt @@ -36,6 +36,7 @@ internal class WalletLoadingStateFactory( private val walletImageResolver: WalletImageResolver, private val getWalletIconUseCase: GetWalletIconUseCase, private val isAddFundsStage1Enabled: Boolean, + private val isManageFundsEnabled: Boolean, ) { fun create(userWallet: UserWallet): WalletState { @@ -171,6 +172,20 @@ internal class WalletLoadingStateFactory( ) } + val lastButton = if (isManageFundsEnabled) { + WalletManageButton.Transfer( + enabled = true, + dimContent = false, + onClick = { clickIntents.onTransferClick(userWallet.walletId) }, + ) + } else { + WalletManageButton.Sell( + enabled = true, + dimContent = false, + onClick = { clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) }, + ) + } + return persistentListOf( firstButton, WalletManageButton.Swap( @@ -178,11 +193,7 @@ internal class WalletLoadingStateFactory( dimContent = false, onClick = { clickIntents.onMultiWalletSwapClick(userWalletId = userWallet.walletId) }, ), - WalletManageButton.Sell( - enabled = true, - dimContent = false, - onClick = { clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) }, - ), + lastButton, ) } @@ -205,14 +216,25 @@ internal class WalletLoadingStateFactory( }, ).buttonUM, ) - add( - WalletActionButtons.Sell( - isEnabled = false, - onClick = { - clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) - }, - ).buttonUM, - ) + if (isManageFundsEnabled) { + add( + WalletActionButtons.Transfer( + isEnabled = false, + onClick = { + clickIntents.onTransferClick(userWalletId = userWallet.walletId) + }, + ).buttonUM, + ) + } else { + add( + WalletActionButtons.Sell( + isEnabled = false, + onClick = { + clickIntents.onMultiWalletSellClick(userWalletId = userWallet.walletId) + }, + ).buttonUM, + ) + } }.toPersistentList() } diff --git a/tangem-android-tools b/tangem-android-tools index e472e45a2d..a1fd964e4f 160000 --- a/tangem-android-tools +++ b/tangem-android-tools @@ -1 +1 @@ -Subproject commit e472e45a2d43e663e0ceccef8b9aa0e8a98840da +Subproject commit a1fd964e4f9cc4da32be63f8e7bd0638542166f1