Updated on 2026-08-14
This commit is contained in:
parent
8511623421
commit
7095ceec0f
21 changed files with 717 additions and 183 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -150,6 +150,7 @@ internal class TokenActionsUiBuilder @Inject constructor(
|
|||
} else {
|
||||
TangemBadgeIconPosition.End
|
||||
},
|
||||
shouldRespectIconTint = cryptoCurrencyData.isAccountMode,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue