Updated on 2026-08-14
This commit is contained in:
commit
bf0a8efd7e
97 changed files with 1537 additions and 507 deletions
|
|
@ -620,6 +620,7 @@ internal class ChildFactory @Inject constructor(
|
|||
params = SendEntryPointComponent.Params(
|
||||
userWalletId = route.userWalletId,
|
||||
cryptoCurrency = route.currency,
|
||||
shouldStartWithSwap = route.shouldStartWithSwap,
|
||||
),
|
||||
componentFactory = sendEntryPointComponentFactory,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -444,6 +444,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}?",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -44,4 +44,8 @@ dependencies {
|
|||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Test */
|
||||
testImplementation(projects.test.core)
|
||||
testImplementation(projects.common.test)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokenActionsState.ActionState>, 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<TokenActionsState.ActionState>): ImmutableList<QuickActionUM> {
|
||||
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<TokenActionsState.ActionState>,
|
||||
isRedesignEnabled: Boolean,
|
||||
context: TokenActionsContext = TokenActionsContext.Markets,
|
||||
): ImmutableList<QuickActionUM> {
|
||||
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<TokenActionsState.ActionState>): ImmutableList<QuickActionUM> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Action>?,
|
||||
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,
|
||||
),
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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<AccountStatus.CryptoPortfolio>(relaxed = true),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `GIVEN AddFunds context WHEN handle Exchange THEN swap pushed with TO position`() {
|
||||
// Arrange
|
||||
val slot = slot<Route>()
|
||||
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<Route>()
|
||||
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<Route>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -332,6 +332,7 @@
|
|||
<string name="common_error">Error</string>
|
||||
<string name="common_estimated_fee">Top-up network fee</string>
|
||||
<string name="common_exchange">Swap</string>
|
||||
<string name="common_send_with_swap">Send&Swap</string>
|
||||
<string name="common_explore">Explore</string>
|
||||
<string name="common_explore_transaction_history">Explore transaction history</string>
|
||||
<string name="common_explorer">Explorer</string>
|
||||
|
|
@ -1294,6 +1295,7 @@
|
|||
<string name="quick_action_sell_description">Sell crypto securely</string>
|
||||
<string name="quick_action_send_description">Send to another wallet</string>
|
||||
<string name="quick_action_swap_description">Between your portfolios</string>
|
||||
<string name="quick_action_send_and_swap_description">Send with swap to another token</string>
|
||||
<string name="quick_top_up_chip_other">Other</string>
|
||||
<string name="quick_top_up_title">Quick top up</string>
|
||||
<string name="receive_bottom_sheet_no_memo_required_message">No memo required</string>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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: <address>", 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: <address>".
|
||||
*/
|
||||
data class PlainAddress(val text: TextReference, val highlight: String) : ContentSubtitle
|
||||
|
||||
/**
|
||||
* External counterparty address — renders as "to/from: <identicon> <briefAddress>".
|
||||
* Used for Transfer to/from external addresses.
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<TangemPayCard>.stableOrder(previousOrder: List<String>): List<TangemPayCard> {
|
||||
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<String> {
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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<TangemPayCard>,
|
||||
val previousOrder: List<String>,
|
||||
val expectedIds: List<String>,
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.features.txhistory
|
||||
package com.tangem.domain.txhistory
|
||||
|
||||
interface TxHistoryFeatureToggles {
|
||||
val isSolanaTxHistoryEnabled: Boolean
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Order>().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<Order>().right()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Params, AddFundsComponent>
|
||||
interface Factory : ComponentFactory<Params, ManageFundsComponent>
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String, String> = 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"
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<AddFundsComponent.Params>()
|
||||
val launchMode: AddFundsComponent.LaunchMode = params.launchMode
|
||||
private val params = paramsContainer.require<ManageFundsComponent.Params>()
|
||||
val launchMode: ManageFundsComponent.LaunchMode = params.launchMode
|
||||
val flowType: ManageFundsComponent.FlowType = params.flowType
|
||||
|
||||
private val routeStack = MutableStateFlow(listOf<UiRoute>(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) {
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<BottomAction> = flowOf(BottomAction.None),
|
||||
val isRedesignForced: Boolean = false,
|
||||
val isCompact: Boolean = false,
|
||||
val context: TokenActionsContext = TokenActionsContext.Markets,
|
||||
)
|
||||
|
||||
interface Callbacks {
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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 -> {
|
||||
|
|
|
|||
|
|
@ -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() },
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>
|
||||
|
|
@ -21,19 +22,33 @@ internal typealias FeedEntryActiveChild =
|
|||
internal typealias FeedEntryChildStack =
|
||||
ChildStack<FeedEntryChildFactory.Child, ComposableModularBottomSheetContentComponent>
|
||||
|
||||
@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<FeedEntryChildStack>,
|
||||
): AnimatedContentTransitionScope<FeedEntryActiveChild>.() -> ContentTransform =
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ interface SendEntryPointComponent : ComposableContentComponent {
|
|||
data class Params(
|
||||
val userWalletId: UserWalletId,
|
||||
val cryptoCurrency: CryptoCurrency,
|
||||
val shouldStartWithSwap: Boolean = false,
|
||||
)
|
||||
|
||||
interface Factory : ComponentFactory<Params, SendEntryPointComponent>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,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.feature.swap.converters.*
|
||||
import com.tangem.feature.swap.domain.api.SwapRepository
|
||||
import com.tangem.feature.swap.domain.models.ExpressDataError
|
||||
|
|
@ -50,6 +51,7 @@ internal class DefaultSwapRepository(
|
|||
private val dataSignatureVerifier: DataSignatureVerifier,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val expressHistoryDao: ExpressHistoryDao,
|
||||
private val txHistoryFeatureToggles: TxHistoryFeatureToggles,
|
||||
moshi: Moshi,
|
||||
) : SwapRepository {
|
||||
|
||||
|
|
@ -156,7 +158,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)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.tangem.datasource.local.config.environment.EnvironmentConfig
|
|||
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.txhistory.TxHistoryFeatureToggles
|
||||
import com.tangem.feature.swap.DefaultSwapFeedbackRepository
|
||||
import com.tangem.feature.swap.DefaultSwapRepository
|
||||
import com.tangem.feature.swap.NoOpSwapFeedbackRepository
|
||||
|
|
@ -41,6 +42,7 @@ internal class SwapDataModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
expressHistoryDao: ExpressHistoryDao,
|
||||
txHistoryFeatureToggles: TxHistoryFeatureToggles,
|
||||
): SwapRepository {
|
||||
return DefaultSwapRepository(
|
||||
tangemExpressApi = tangemExpressApi,
|
||||
|
|
@ -50,6 +52,7 @@ internal class SwapDataModule {
|
|||
moshi = moshi,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
expressHistoryDao = expressHistoryDao,
|
||||
txHistoryFeatureToggles = txHistoryFeatureToggles,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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<TangemPayCard>.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(
|
||||
|
|
|
|||
|
|
@ -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<Card>,
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T
|
|||
TangemPayDetailsBalanceBlockState.Card(
|
||||
lastDigits = "1234",
|
||||
onClick = {},
|
||||
isReissuingOrClosing = false,
|
||||
isEnabled = false,
|
||||
isFrozen = false,
|
||||
isIssuing = false,
|
||||
state = TangemPayCardUiState.Active,
|
||||
),
|
||||
TangemPayDetailsBalanceBlockState.Card(
|
||||
lastDigits = "3456",
|
||||
onClick = {},
|
||||
isReissuingOrClosing = false,
|
||||
isEnabled = true,
|
||||
isFrozen = false,
|
||||
isIssuing = false,
|
||||
state = TangemPayCardUiState.Active,
|
||||
),
|
||||
),
|
||||
onAddCardClick = {},
|
||||
|
|
@ -527,14 +526,14 @@ internal class TangemPayDetailsUMProvider : CollectionPreviewParameterProvider<T
|
|||
TangemPayDetailsBalanceBlockState.Card(
|
||||
lastDigits = "1234",
|
||||
onClick = {},
|
||||
isReissuingOrClosing = true,
|
||||
isFrozen = false,
|
||||
isEnabled = true,
|
||||
isIssuing = false,
|
||||
state = TangemPayCardUiState.InProgress,
|
||||
),
|
||||
),
|
||||
onAddCardClick = {},
|
||||
isAddCardEnabled = true,
|
||||
progressBanner = CardsProgressBannerUM.Reissuing,
|
||||
),
|
||||
),
|
||||
isBalanceHidden = false,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.components.SpacerW
|
||||
|
|
@ -56,6 +55,8 @@ import com.tangem.features.tangempay.components.express.PreviewEmptyExpressTrans
|
|||
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
|
||||
import com.tangem.features.tangempay.components.txHistory.TangemPayTxHistoryComponent
|
||||
import com.tangem.features.tangempay.details.impl.R
|
||||
import com.tangem.features.tangempay.entity.CardsProgressBannerUM
|
||||
import com.tangem.features.tangempay.entity.TangemPayCardUiState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsBalanceBlockState
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsTopBarConfig
|
||||
import com.tangem.features.tangempay.entity.TangemPayDetailsUM
|
||||
|
|
@ -141,6 +142,7 @@ internal fun TangemPayDetailsScreenV2(
|
|||
expressTransactionsBottomSheetState?.content(null)
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
|
||||
item("balanceBlock") {
|
||||
BalanceBlock(
|
||||
|
|
@ -158,8 +160,20 @@ private fun LazyListScope.payDetailsBody(state: TangemPayDetailsUM) {
|
|||
ActionBlock(actionButtons = state.balanceBlockState.actionButtons)
|
||||
}
|
||||
}
|
||||
when {
|
||||
state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuingOrClosing } == true -> {
|
||||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ package com.tangem.features.tokendetails
|
|||
|
||||
interface TokenDetailsFeatureToggles {
|
||||
val isQuickTopUpEnabled: Boolean
|
||||
val isManageFundsEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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 */ }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
|||
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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ plugins {
|
|||
|
||||
android {
|
||||
namespace = "com.tangem.features.txhistory.impl"
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
merges += "paymentrequest.proto"
|
||||
}
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
/* Project - API */
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,4 +10,6 @@ interface WalletFeatureToggles {
|
|||
val isAddAndManageTokensEnabled: Boolean
|
||||
|
||||
val isAddFundsStage1Enabled: Boolean
|
||||
|
||||
val isManageFundsEnabled: Boolean
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,4 +120,7 @@ internal interface InnerWalletRouter {
|
|||
|
||||
/** Open Add Funds screen */
|
||||
fun openAddFunds(userWalletId: UserWalletId)
|
||||
|
||||
/** Open Transfer screen */
|
||||
fun openTransfer(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<UserWallet>,
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit e472e45a2d43e663e0ceccef8b9aa0e8a98840da
|
||||
Subproject commit a1fd964e4f9cc4da32be63f8e7bd0638542166f1
|
||||
Loading…
Add table
Add a link
Reference in a new issue