Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-29 16:01:28 +02:00
parent 8511623421
commit 7095ceec0f
21 changed files with 717 additions and 183 deletions

View file

@ -0,0 +1,110 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioFooterKind
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.uiSpec
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
import dev.chrisbanes.haze.rememberHazeState
@Composable
internal fun AddToPortfolioBottomSheetFooter(
currentRoute: AddToPortfolioRoutes,
userPortfolioState: State<UserPortfolioUM?>?,
onBack: () -> Unit,
onAddFromUserPortfolioClick: (() -> Unit)?,
) {
when (currentRoute.uiSpec().footer) {
AddToPortfolioFooterKind.Cancel -> WithLocalHaze {
CancelFooterButton(onClick = onBack)
}
AddToPortfolioFooterKind.UserPortfolioAdd -> {
val state = userPortfolioState?.value ?: return
val onClick = onAddFromUserPortfolioClick ?: return
WithLocalHaze {
UserPortfolioAddFooter(
isEnabled = state.isAddEnabled,
onClick = onClick,
)
}
}
AddToPortfolioFooterKind.None -> Unit
}
}
@Composable
private fun WithLocalHaze(content: @Composable () -> Unit) {
CompositionLocalProvider(LocalHazeState provides rememberHazeState(), content = content)
}
@Composable
private fun CancelFooterButton(onClick: () -> Unit) {
SecondaryTangemButton(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens2.x4)
.padding(bottom = TangemTheme.dimens2.x4),
onClick = onClick,
text = resourceReference(R.string.common_cancel),
size = TangemButtonSize.X12,
shape = TangemButtonShape.Rounded,
)
}
@Composable
private fun UserPortfolioAddFooter(isEnabled: Boolean, onClick: () -> Unit) {
TangemRowContainer(
contentPadding = PaddingValues(
horizontal = TangemTheme.dimens2.x6,
vertical = TangemTheme.dimens2.x5,
),
) {
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = stringResourceSafe(R.string.common_add_token),
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
text = stringResourceSafe(R.string.markets_token_add_subtitle),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
SecondaryTangemButton(
modifier = Modifier
.layoutId(TangemRowLayoutId.TAIL)
.padding(start = TangemTheme.dimens2.x2),
onClick = onClick,
text = resourceReference(R.string.common_add),
size = TangemButtonSize.X9,
shape = TangemButtonShape.Rounded,
isEnabled = isEnabled,
)
}
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
@Composable
internal fun AddToPortfolioBottomSheetSwitch(
childStack: State<ChildStack<AddToPortfolioRoutes, ComposableContentComponent>>,
onBack: () -> Unit,
onDismiss: () -> Unit,
userPortfolioState: State<UserPortfolioUM?>? = null,
onAddFromUserPortfolioClick: (() -> Unit)? = null,
) {
if (LocalRedesignEnabled.current) {
AddToPortfolioBottomSheetV2(
childStack = childStack,
onBack = onBack,
onDismiss = onDismiss,
userPortfolioState = userPortfolioState,
onAddFromUserPortfolioClick = onAddFromUserPortfolioClick,
)
} else {
AddToPortfolioBottomSheet(
childStack = childStack,
onBack = onBack,
onDismiss = onDismiss,
)
}
}

View file

@ -0,0 +1,126 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.bottomsheets.*
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.AddToPortfolioRoutes
import com.tangem.features.commonfeatures.impl.addtoportfolio.model.uiSpec
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioUM
@Composable
internal fun AddToPortfolioBottomSheetV2(
childStack: State<ChildStack<AddToPortfolioRoutes, ComposableContentComponent>>,
onBack: () -> Unit,
onDismiss: () -> Unit,
userPortfolioState: State<UserPortfolioUM?>? = null,
onAddFromUserPortfolioClick: (() -> Unit)? = null,
) {
val stack by childStack
val contentStack = remember { mutableStateOf(stack) }
val isNotEmpty = stack.active.configuration != AddToPortfolioRoutes.Empty
if (isNotEmpty) {
contentStack.value = stack
}
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
onBack = onBack,
config = TangemBottomSheetConfig(
isShown = isNotEmpty,
onDismissRequest = onDismiss,
content = TangemBottomSheetConfigContent.Empty,
),
type = TangemBottomSheetType.Modal,
containerColor = TangemTheme.colors2.surface.level2,
title = {
AddToPortfolioBottomSheetTitle(
stack = stack,
onCloseClick = onDismiss,
)
},
content = {
AnimatedContent(targetState = contentStack.value, label = "Content Animation") { animatedStack ->
AddToPortfolioRouteContent(animatedStack = animatedStack)
}
},
footer = {
AnimatedContent(
targetState = contentStack.value.active.configuration,
transitionSpec = { fadeIn() togetherWith fadeOut() },
label = "Footer Animation",
) { route ->
AddToPortfolioBottomSheetFooter(
currentRoute = route,
userPortfolioState = userPortfolioState,
onBack = onBack,
onAddFromUserPortfolioClick = onAddFromUserPortfolioClick,
)
}
},
)
}
@Composable
private fun AddToPortfolioRouteContent(animatedStack: ChildStack<AddToPortfolioRoutes, ComposableContentComponent>) {
val spec = animatedStack.active.configuration.uiSpec()
val baseModifier = if (spec.shouldApplyHorizontalPadding) {
Modifier.padding(horizontal = TangemTheme.dimens2.x4)
} else {
Modifier
}
if (spec.isScrollable) {
val bottomInset = LocalTangemBottomSheetContentBottomInset.current
val scrollBottomReserve = if (bottomInset > 0.dp) bottomInset else TangemTheme.dimens2.x4
Column(modifier = baseModifier.verticalScroll(rememberScrollState())) {
animatedStack.active.instance.Content(modifier = Modifier)
Spacer(modifier = Modifier.height(scrollBottomReserve))
}
} else {
animatedStack.active.instance.Content(modifier = baseModifier)
}
}
@Composable
private fun AddToPortfolioBottomSheetTitle(
stack: ChildStack<AddToPortfolioRoutes, ComposableContentComponent>,
onCloseClick: () -> Unit,
) {
TangemTopBar(
title = stack.active.configuration.uiSpec().title,
type = TangemTopBarType.BottomSheet,
endContent = {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier
.size(TangemTheme.dimens2.x11)
.background(
color = TangemTheme.colors2.button.backgroundSecondary,
shape = CircleShape,
)
.clickableSingle(onClick = onCloseClick)
.padding(TangemTheme.dimens2.x2),
)
},
)
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.stack.backStack
@ -83,10 +84,14 @@ internal class DefaultAddToPortfolioComponent @AssistedInject constructor(
@Composable
override fun BottomSheet() {
AddToPortfolioBottomSheet(
val userPortfolioState = model.userPortfolioStateController.uiState
.collectAsStateWithLifecycle()
AddToPortfolioBottomSheetSwitch(
childStack = childStack.subscribeAsState(),
onBack = ::onBack,
onDismiss = ::dismiss,
userPortfolioState = userPortfolioState,
onAddFromUserPortfolioClick = model::onContinueFromUserPortfolio,
)
}

View file

@ -23,6 +23,7 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor(
selectedWallet: UserWallet?,
tokenParams: RawMarketToken,
accountToAdd: AvailableToAddAccount? = null,
preferredNetwork: TokenMarketInfo.Network? = null,
): InitialSelection? {
if (availableToAddData.availableToAddWallets.isEmpty()) return null
val fallbackNetwork = orderedNetworks.firstOrNull() ?: return null
@ -33,13 +34,13 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor(
val ownerWallet = walletOrder.firstOrNull { entry ->
entry.availableToAddAccounts.values.any { it === accountToAdd }
} ?: walletOrder.first()
val network = pickAddableNetwork(
val network = pickNetworkForExplicitAccount(
userWallet = ownerWallet.userWallet,
account = accountToAdd,
orderedNetworks = orderedNetworks,
tokenParams = tokenParams,
preferredNetwork = preferredNetwork,
)
?: fallbackNetwork
return InitialSelection(userWallet = ownerWallet.userWallet, account = accountToAdd, network = network)
}
@ -81,6 +82,35 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor(
?: walletEntry.availableToAddAccounts.values.firstOrNull()
}
private suspend fun pickNetworkForExplicitAccount(
userWallet: UserWallet,
account: AvailableToAddAccount,
orderedNetworks: List<TokenMarketInfo.Network>,
tokenParams: RawMarketToken,
preferredNetwork: TokenMarketInfo.Network?,
): TokenMarketInfo.Network {
if (preferredNetwork != null) {
val candidate = account.availableToAddNetworks
.firstOrNull { it.networkId == preferredNetwork.networkId }
if (
candidate != null && hasDerivationFor(
userWallet = userWallet,
account = account,
network = candidate,
tokenParams = tokenParams,
)
) {
return candidate
}
}
return pickAddableNetwork(
userWallet = userWallet,
account = account,
orderedNetworks = orderedNetworks,
tokenParams = tokenParams,
) ?: orderedNetworks.first()
}
private suspend fun pickAddableNetwork(
userWallet: UserWallet,
account: AvailableToAddAccount,
@ -92,18 +122,26 @@ internal class AddToPortfolioInitialSelectionResolver @Inject constructor(
}
if (availableOrdered.isEmpty()) return null
val derivationIndex = account.account.account.derivationIndex
val withDerivation = availableOrdered.filter { network ->
val currency = getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = tokenParams,
network = network,
accountIndex = derivationIndex,
) ?: return@filter false
networkHasDerivationUseCase(userWallet, currency.network).getOrElse { false }
val withDerivation = availableOrdered.firstOrNull { network ->
hasDerivationFor(userWallet = userWallet, account = account, network = network, tokenParams = tokenParams)
}
return withDerivation ?: availableOrdered.first()
}
return withDerivation.firstOrNull() ?: availableOrdered.first()
private suspend fun hasDerivationFor(
userWallet: UserWallet,
account: AvailableToAddAccount,
network: TokenMarketInfo.Network,
tokenParams: RawMarketToken,
): Boolean {
val derivationIndex = account.account.account.derivationIndex
val currency = getTokenMarketCryptoCurrency(
userWalletId = userWallet.walletId,
tokenMarketParams = tokenParams,
network = network,
accountIndex = derivationIndex,
) ?: return false
return networkHasDerivationUseCase(userWallet, currency.network).getOrElse { false }
}
data class InitialSelection(

View file

@ -378,23 +378,17 @@ internal class AddToPortfolioModel @Inject constructor(
// drop first selected portfolio or any selected before
.drop(1)
.map { newPortfolio ->
val currentNetwork = selectedNetwork.first().selectedNetwork
val availableToAddNetworks = newPortfolio.account.availableToAddNetworks
val isSelectedNetworkAvailableForNewPortfolio = availableToAddNetworks
.any { it.networkId == currentNetwork.networkId }
val rebuiltSelectedNetwork = selectionResolver.resolve(
availableToAddData = data,
orderedNetworks = orderedNetworks,
selectedWallet = globalSelectedWallet,
tokenParams = tokenParams,
accountToAdd = newPortfolio.account,
preferredNetwork = selectedNetwork.first().selectedNetwork,
)?.toSelectedNetwork()
if (!isSelectedNetworkAvailableForNewPortfolio) {
val selection = selectionResolver.resolve(
availableToAddData = data,
orderedNetworks = orderedNetworks,
selectedWallet = globalSelectedWallet,
tokenParams = tokenParams,
accountToAdd = newPortfolio.account,
)
val newNetwork = selection?.toSelectedNetwork()
if (newNetwork != null) {
this.selectedNetwork.tryEmit(newNetwork)
}
if (rebuiltSelectedNetwork != null) {
this.selectedNetwork.tryEmit(rebuiltSelectedNetwork)
}
selectedPortfolio.tryEmit(newPortfolio)
navigation.popToFirst()

View file

@ -0,0 +1,57 @@
package com.tangem.features.commonfeatures.impl.addtoportfolio.model
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.commonfeatures.impl.R
internal data class AddToPortfolioRouteUiSpec(
val title: TextReference,
val isScrollable: Boolean,
val shouldApplyHorizontalPadding: Boolean,
val footer: AddToPortfolioFooterKind,
)
internal enum class AddToPortfolioFooterKind {
None,
Cancel,
UserPortfolioAdd,
}
internal fun AddToPortfolioRoutes.uiSpec(): AddToPortfolioRouteUiSpec = when (this) {
AddToPortfolioRoutes.AddToken -> AddToPortfolioRouteUiSpec(
title = resourceReference(R.string.common_add_token),
isScrollable = true,
shouldApplyHorizontalPadding = true,
footer = AddToPortfolioFooterKind.None,
)
is AddToPortfolioRoutes.NetworkSelector -> AddToPortfolioRouteUiSpec(
title = resourceReference(R.string.common_add_token),
isScrollable = true,
shouldApplyHorizontalPadding = true,
footer = AddToPortfolioFooterKind.Cancel,
)
AddToPortfolioRoutes.PortfolioSelector -> AddToPortfolioRouteUiSpec(
title = resourceReference(R.string.common_add_token),
isScrollable = false,
shouldApplyHorizontalPadding = true,
footer = AddToPortfolioFooterKind.Cancel,
)
AddToPortfolioRoutes.UserPortfolio -> AddToPortfolioRouteUiSpec(
title = resourceReference(R.string.markets_portfolio_block_title),
isScrollable = false,
shouldApplyHorizontalPadding = false,
footer = AddToPortfolioFooterKind.UserPortfolioAdd,
)
AddToPortfolioRoutes.TokenActions -> AddToPortfolioRouteUiSpec(
title = resourceReference(R.string.common_get_token),
isScrollable = true,
shouldApplyHorizontalPadding = true,
footer = AddToPortfolioFooterKind.None,
)
AddToPortfolioRoutes.Empty -> AddToPortfolioRouteUiSpec(
title = TextReference.EMPTY,
isScrollable = false,
shouldApplyHorizontalPadding = false,
footer = AddToPortfolioFooterKind.None,
)
}

View file

@ -150,6 +150,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
} else {
TangemBadgeIconPosition.End
},
shouldRespectIconTint = cryptoCurrencyData.isAccountMode,
)
}

View file

@ -44,6 +44,7 @@ import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.*
import com.tangem.features.commonfeatures.impl.R
import com.tangem.features.commonfeatures.impl.addtoportfolio.ui.state.TokenActionsUM
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import java.util.UUID
@ -79,13 +80,15 @@ internal fun TokenActionsContentV2(state: TokenActionsUM, modifier: Modifier = M
SpacerH(TangemTheme.dimens2.x2)
SecondaryTangemButton(
modifier = Modifier.fillMaxWidth(),
onClick = state.onLaterClick,
text = resourceReference(R.string.common_later),
size = TangemButtonSize.X12,
shape = TangemButtonShape.Rounded,
)
CompositionLocalProvider(LocalHazeState provides rememberHazeState()) {
SecondaryTangemButton(
modifier = Modifier.fillMaxWidth(),
onClick = state.onLaterClick,
text = resourceReference(R.string.common_later),
size = TangemButtonSize.X12,
shape = TangemButtonShape.Rounded,
)
}
}
}

View file

@ -3,12 +3,10 @@ package com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.common.ui.markets.tokenselector.StickyFooter
import com.tangem.common.ui.markets.tokenselector.TokenSelectorEmbeddedContent
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.commonfeatures.impl.R
import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset
import com.tangem.features.commonfeatures.impl.addtoportfolio.userportfolio.model.UserPortfolioModel
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@ -20,7 +18,6 @@ internal class DefaultUserPortfolioComponent @AssistedInject constructor(
) : UserPortfolioComponent, AppComponentContext by context {
private val model: UserPortfolioModel = getOrCreateModel(params)
private val onContinueClick: () -> Unit = params.callbacks::onContinueFromUserPortfolio
@Composable
override fun Content(modifier: Modifier) {
@ -29,11 +26,7 @@ internal class DefaultUserPortfolioComponent @AssistedInject constructor(
TokenSelectorEmbeddedContent(
content = state.content,
modifier = modifier,
stickyFooter = StickyFooter(
buttonText = resourceReference(R.string.common_add),
isEnabled = state.isAddEnabled,
onClick = onContinueClick,
),
scrollBottomInset = LocalTangemBottomSheetContentBottomInset.current,
)
}

View file

@ -1,11 +1,13 @@
package com.tangem.features.commonfeatures.impl.portfolioselector
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.bottomsheets.LocalTangemBottomSheetContentBottomInset
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.LocalRedesignEnabled
import com.tangem.features.commonfeatures.api.portfolioselector.PortfolioSelectorComponent
@ -45,15 +47,18 @@ internal class DefaultPortfolioSelectorComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.state.collectAsStateWithLifecycle()
val listBottomPadding = PaddingValues(bottom = LocalTangemBottomSheetContentBottomInset.current)
if (LocalRedesignEnabled.current) {
PortfolioSelectorContentV2(
state = state,
modifier = modifier,
contentPadding = listBottomPadding,
)
} else {
PortfolioSelectorContent(
state = state,
modifier = modifier,
contentPadding = listBottomPadding,
)
}
}

View file

@ -508,6 +508,109 @@ class AddToPortfolioInitialSelectionResolverTest {
Truth.assertThat(result?.network).isEqualTo(BITCOIN)
}
@Test
fun `GIVEN accountToAdd and preferredNetwork is on account and addable WHEN resolve THEN return preferred`() = runTest {
val walletId = UserWalletId(WALLET_ID_A)
val userWallet = userWallet(walletId)
val explicitAccount = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN))
val ethereumCurrency = cryptoCurrency()
val bitcoinCurrency = cryptoCurrency()
coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency
coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency
every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns true.right()
every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right()
val data = availableData(
wallets = mapOf(
walletId to walletEntry(
userWallet = userWallet,
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()),
),
),
)
val result = resolver.resolve(
availableToAddData = data,
orderedNetworks = listOf(BITCOIN, ETHEREUM),
selectedWallet = userWallet,
tokenParams = tokenParams,
accountToAdd = explicitAccount,
preferredNetwork = ETHEREUM,
)
Truth.assertThat(result?.network).isEqualTo(ETHEREUM)
}
@Test
fun `GIVEN accountToAdd and preferred is not on account WHEN resolve THEN use pickAddableNetwork`() = runTest {
val walletId = UserWalletId(WALLET_ID_A)
val userWallet = userWallet(walletId)
val explicitAccount = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN))
val ethereumCurrency = cryptoCurrency()
val bitcoinCurrency = cryptoCurrency()
coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency
coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency
every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns true.right()
every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right()
val data = availableData(
wallets = mapOf(
walletId to walletEntry(
userWallet = userWallet,
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()),
),
),
)
val result = resolver.resolve(
availableToAddData = data,
orderedNetworks = listOf(ETHEREUM, BITCOIN),
selectedWallet = userWallet,
tokenParams = tokenParams,
accountToAdd = explicitAccount,
preferredNetwork = POLYGON,
)
Truth.assertThat(result?.network).isEqualTo(ETHEREUM)
}
@Test
fun `GIVEN accountToAdd and preferred on account has no derivation WHEN resolve THEN fall back to pickAddableNetwork`() =
runTest {
val walletId = UserWalletId(WALLET_ID_A)
val userWallet = userWallet(walletId)
val explicitAccount = availableAccount(availableToAddNetworks = setOf(ETHEREUM, BITCOIN))
val ethereumCurrency = cryptoCurrency()
val bitcoinCurrency = cryptoCurrency()
coEvery { getTokenMarketCryptoCurrency(any(), any(), ETHEREUM, any()) } returns ethereumCurrency
coEvery { getTokenMarketCryptoCurrency(any(), any(), BITCOIN, any()) } returns bitcoinCurrency
every { networkHasDerivationUseCase(any(), ethereumCurrency.network) } returns false.right()
every { networkHasDerivationUseCase(any(), bitcoinCurrency.network) } returns true.right()
val data = availableData(
wallets = mapOf(
walletId to walletEntry(
userWallet = userWallet,
accounts = mapOf(AccountId.forMainCryptoPortfolio(walletId) to availableAccount()),
),
),
)
val result = resolver.resolve(
availableToAddData = data,
orderedNetworks = listOf(ETHEREUM, BITCOIN),
selectedWallet = userWallet,
tokenParams = tokenParams,
accountToAdd = explicitAccount,
preferredNetwork = ETHEREUM,
)
Truth.assertThat(result?.network).isEqualTo(BITCOIN)
}
@Test
fun `GIVEN get token market crypto currency returns null WHEN resolve THEN fall back to first ordered available network`() = runTest {
val walletId = UserWalletId(WALLET_ID_A)
@ -587,5 +690,12 @@ class AddToPortfolioInitialSelectionResolverTest {
contractAddress = null,
decimalCount = 8,
)
val POLYGON = TokenMarketInfo.Network(
networkId = "polygon",
isExchangeable = true,
contractAddress = null,
decimalCount = 18,
)
}
}

View file

@ -10,7 +10,6 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.BottomSheetDefaults
@ -22,6 +21,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@ -31,6 +31,8 @@ import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.ds.button.*
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
@ -56,6 +58,7 @@ internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifi
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.BottomCenter),
visible = isVisible,
enter = fadeIn(animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing)),
exit = fadeOut(animationSpec = tween(durationMillis = 300)),
@ -91,62 +94,55 @@ internal fun PortfolioBlock(state: PortfolioBlockUM, modifier: Modifier = Modifi
@Composable
private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = Modifier) {
FloatingCard(modifier = modifier) { // TODO will be handle in [REDACTED_TASK_KEY]
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Row(
modifier = Modifier
.weight(1f)
.clickable(onClick = state.onRowClick),
verticalAlignment = Alignment.CenterVertically,
) {
CurrencyIcon(
modifier = Modifier.padding(end = TangemTheme.dimens2.x3),
state = state.tokenIcon,
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = state.tokenName,
style = TangemTheme.typography2.bodyMedium16,
color = TangemTheme.colors2.text.neutral.primary,
maxLines = 1,
)
Text(
text = stringResourceSafe(R.string.markets_portfolio_block_subtitle),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
}
Column(horizontalAlignment = Alignment.End) {
Text(
text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodyMedium16,
)
Text(
text = state.tokenSymbol,
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
)
}
}
FloatingCard(modifier = modifier) {
TangemRowContainer(modifier = Modifier.clickableSingle(onClick = state.onRowClick)) {
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_TOP),
text = state.totalBalance.orMaskWithStars(state.isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = TangemTheme.colors2.text.neutral.primary,
style = TangemTheme.typography2.bodyMedium16,
)
Text(
modifier = Modifier.layoutId(TangemRowLayoutId.START_BOTTOM),
text = stringResourceSafe(R.string.markets_portfolio_block_subtitle),
style = TangemTheme.typography2.captionMedium12,
color = TangemTheme.colors2.text.neutral.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
TangemButton(
modifier = Modifier.padding(start = TangemTheme.dimens2.x3),
modifier = Modifier.layoutId(TangemRowLayoutId.END_TOP),
buttonUM = TangemButtonUM(
text = resourceReference(R.string.common_add_funds),
type = TangemButtonType.Secondary,
tangemIconUM = TangemIconUM.Icon(
iconRes = R.drawable.ic_arrow_down_20,
tintReference = { TangemTheme.colors2.graphic.neutral.primary },
),
iconPosition = TangemButtonIconPosition.Start,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
onClick = state.onAddFundsClick,
),
)
TangemButton(
modifier = Modifier
.layoutId(TangemRowLayoutId.TAIL)
.padding(start = TangemTheme.dimens2.x3),
buttonUM = TangemButtonUM(
type = TangemButtonType.Secondary,
tangemIconUM = TangemIconUM.Icon(
iconRes = R.drawable.ic_chevron_24,
iconRes = R.drawable.ic_arrow_expand_24,
tintReference = { TangemTheme.colors2.graphic.neutral.primary },
),
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X9,
onClick = state.onAddFundsClick,
onClick = state.onRowClick,
),
)
}
@ -156,7 +152,12 @@ private fun ContentBlock(state: PortfolioBlockUM.Content, modifier: Modifier = M
@Composable
private fun AddTokenBlock(state: PortfolioBlockUM.AddToken, modifier: Modifier = Modifier) {
FloatingCard(modifier = modifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
Row(
modifier = Modifier
.padding(TangemTheme.dimens2.x3)
.clickableSingle(onClick = state.onAddClick),
verticalAlignment = Alignment.CenterVertically,
) {
CurrencyIcon(state.tokenIcon)
SpacerW(TangemTheme.dimens2.x3)
@ -204,8 +205,7 @@ private fun FloatingCard(modifier: Modifier = Modifier, content: @Composable ()
.background(
color = TangemTheme.colors2.surface.level3,
shape = RoundedCornerShape(size = TangemTheme.dimens2.x5),
)
.padding(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3),
),
) {
content()
}

View file

@ -17,6 +17,7 @@ import com.arkivanov.decompose.router.stack.ChildStack
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.decompose.ComposableModularBottomSheetContentComponent
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalMainBottomSheetColor
import com.tangem.core.ui.res.LocalRedesignEnabled
@ -130,6 +131,12 @@ private fun EntryContentV2(
child.instance.Content(
modifier = Modifier
.fillMaxSize()
.conditionalCompose(
condition = !isOpenedInBottomSheet,
modifier = {
padding(top = topBarHeight)
},
)
.hazeSourceTangem(zIndex = 0f, state = hazeState),
contentPadding = PaddingValues(top = topBarHeight),
bottomSheetState = bottomSheetState,

View file

@ -10,13 +10,7 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@ -27,11 +21,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerH32
import com.tangem.core.ui.components.SpacerH4
import com.tangem.core.ui.components.SpacerW4
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
@ -171,7 +161,7 @@ private fun Content(
Modifier
.onSizeChanged { size ->
bottomSpacing = if (size.height > 0) {
with(density) { size.height.toDp() + 16.dp }
with(density) { size.height.toDp() }
} else {
0.dp
}