Updated on 2026-08-14

This commit is contained in:
Tangem 2023-09-01 11:39:27 +05:00
parent 56bf8bf3e9
commit 537731103f
21 changed files with 557 additions and 26 deletions

View file

@ -378,7 +378,7 @@ internal class TokensListViewModel @Inject constructor(
val supportedTokens = scanResponse.card.supportedTokens(cardTypesResolver)
// refactor this later by moving all this logic in card config
if (!supportedTokens.contains(Blockchain.Solana)) {
if (blockchain == Blockchain.Solana && !supportedTokens.contains(Blockchain.Solana)) {
return SupportTokensState.SolanaNetworkUnsupported
}
val canHandleToken = scanResponse.card.canHandleToken(

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Suppress("ConstructorParameterNaming")
@Suppress("ConstructorParameterNaming", "MagicNumber")
@Immutable
data class TangemDimens internal constructor(
// region Elevation

View file

@ -1,8 +1,5 @@
package com.tangem.domain.tokens.models.remove
import com.tangem.domain.tokens.models.CryptoCurrency
sealed class RemoveCurrencyError : Throwable() {
data class HasLinkedTokens(val currency: CryptoCurrency) : RemoveCurrencyError()
data class DataError(override val cause: Throwable) : RemoveCurrencyError()
}

View file

@ -3,7 +3,6 @@ package com.tangem.domain.tokens
import arrow.core.Either
import arrow.core.raise.catch
import arrow.core.raise.either
import arrow.core.raise.ensure
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.domain.tokens.models.remove.RemoveCurrencyError
import com.tangem.domain.tokens.repository.CurrenciesRepository
@ -20,10 +19,6 @@ class RemoveCurrencyUseCase(
currency: CryptoCurrency,
): Either<RemoveCurrencyError, Unit> {
return either {
ensure(
condition = !currency.hasLinkedTokens(userWalletId),
raise = { RemoveCurrencyError.HasLinkedTokens(currency) },
)
catch(
block = { currenciesRepository.removeCurrency(userWalletId, currency) },
catch = { raise(RemoveCurrencyError.DataError(it)) },
@ -31,10 +26,11 @@ class RemoveCurrencyUseCase(
}
}
private suspend fun CryptoCurrency.hasLinkedTokens(userWalletId: UserWalletId): Boolean {
suspend fun hasLinkedTokens(userWalletId: UserWalletId, currency: CryptoCurrency): Boolean {
val walletCurrencies = currenciesRepository
.getMultiCurrencyWalletCurrenciesSync(userWalletId = userWalletId, refresh = false)
return this is CryptoCurrency.Coin && walletCurrencies.any { it != this && it.network.id == this.network.id }
return currency is CryptoCurrency.Coin &&
walletCurrencies.any { it != currency && it.network.id == currency.network.id }
}
}

View file

@ -27,6 +27,7 @@ dependencies {
implementation(deps.compose.paging)
implementation(deps.compose.ui)
implementation(deps.compose.ui.tooling)
implementation(deps.compose.ui.utils)
implementation(deps.arrow.core)
implementation(deps.jodatime)

View file

@ -2,6 +2,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
@ -13,7 +16,18 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal object TokenDetailsPreviewData {
val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(onBackClick = {}, onMoreClick = {})
val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(
onBackClick = {},
tokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig(
persistentListOf(
TokenDetailsAppBarMenuConfig.MenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },
),
),
),
)
val tokenInfoBlockStateWithLongNameInMainCurrency = TokenInfoBlockState(
name = "Stellar (XLM) with long name test",
@ -67,5 +81,6 @@ internal object TokenDetailsPreviewData {
value = TxHistoryState.getDefaultLoadingTransactions {},
),
),
dialogConfig = null,
)
}

View file

@ -0,0 +1,14 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class TokenDetailsAppBarMenuConfig(val items: ImmutableList<MenuItem>) {
data class MenuItem(
val title: TextReference,
val textColorProvider: @Composable () -> Color,
val onClick: () -> Unit,
)
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
internal data class TokenDetailsState(
val topAppBarConfig: TokenDetailsTopAppBarConfig,
@ -9,4 +10,5 @@ internal data class TokenDetailsState(
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,
val marketPriceBlockState: MarketPriceBlockState,
val txHistoryState: TxHistoryState,
val dialogConfig: TokenDetailsDialogConfig?,
)

View file

@ -1,6 +1,6 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
data class TokenDetailsTopAppBarConfig(
internal data class TokenDetailsTopAppBarConfig(
val onBackClick: () -> Unit,
val onMoreClick: () -> Unit,
val tokenDetailsAppBarMenuConfig: TokenDetailsAppBarMenuConfig,
)

View file

@ -2,7 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.annotation.DrawableRes
data class TokenInfoBlockState(
internal data class TokenInfoBlockState(
val name: String,
val iconUrl: String,
val currency: Currency,

View file

@ -0,0 +1,81 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.components
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.tokendetails.impl.R
/**
* Wallet bottom sheet config
*
* @property isShow flag that determine if bottom sheet is shown
* @property onDismissRequest lambda be invoked when bottom sheet is dismissed
* @property content content config
*/
internal data class TokenDetailsDialogConfig(
val isShow: Boolean,
val onDismissRequest: () -> Unit,
val content: DialogContentConfig,
) {
sealed class DialogContentConfig {
abstract val title: TextReference
abstract val message: TextReference
abstract val confirmButtonConfig: ButtonConfig
abstract val cancelButtonConfig: ButtonConfig?
data class ButtonConfig(
val text: TextReference,
val onClick: () -> Unit,
val warning: Boolean = false,
)
data class ConfirmHideConfig(
val currencySymbol: String,
val onConfirmClick: () -> Unit,
val onCancelClick: () -> Unit,
) : DialogContentConfig() {
override val title: TextReference = TextReference.Res(
id = R.string.token_details_hide_alert_title,
formatArgs = wrappedList(currencySymbol),
)
override val message: TextReference = TextReference.Res(R.string.token_details_hide_alert_message)
override val cancelButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.common_cancel),
onClick = onCancelClick,
)
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.token_details_hide_alert_hide),
onClick = onConfirmClick,
warning = true,
)
}
data class HasLinkedTokensConfig(
val currencySymbol: String,
val networkName: String,
val onConfirmClick: () -> Unit,
) : DialogContentConfig() {
override val title: TextReference = TextReference.Res(
id = R.string.token_details_unable_hide_alert_title,
formatArgs = wrappedList(currencySymbol),
)
override val message: TextReference = TextReference.Res(
id = R.string.token_details_unable_hide_alert_message,
formatArgs = wrappedList(currencySymbol, networkName),
)
override val cancelButtonConfig: ButtonConfig?
get() = null
override val confirmButtonConfig: ButtonConfig = ButtonConfig(
text = TextReference.Res(R.string.common_ok),
onClick = onConfirmClick,
)
}
}
}

View file

@ -2,8 +2,11 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.iconResId
import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.tokens.models.CryptoCurrency
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
@ -11,6 +14,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenInfo
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSkeletonStateConverter.SkeletonModel
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -24,7 +28,7 @@ internal class TokenDetailsSkeletonStateConverter(
return TokenDetailsState(
topAppBarConfig = TokenDetailsTopAppBarConfig(
onBackClick = clickIntents::onBackClick,
onMoreClick = clickIntents::onMoreClick,
tokenDetailsAppBarMenuConfig = createMenu(),
),
tokenInfoBlockState = TokenInfoBlockState(
name = value.cryptoCurrency.name,
@ -47,9 +51,20 @@ internal class TokenDetailsSkeletonStateConverter(
value = TxHistoryState.getDefaultLoadingTransactions(clickIntents::onExploreClick),
),
),
dialogConfig = null,
)
}
private fun createMenu(): TokenDetailsAppBarMenuConfig = TokenDetailsAppBarMenuConfig(
items = persistentListOf(
TokenDetailsAppBarMenuConfig.MenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = clickIntents::onHideClick,
),
),
)
private fun createButtons(): ImmutableList<TokenDetailsActionButton> {
return persistentListOf(
TokenDetailsActionButton.Buy(enabled = false, onClick = {}),

View file

@ -12,6 +12,7 @@ import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.txhistory.models.TxHistoryListError
import com.tangem.domain.txhistory.models.TxHistoryStateError
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
@ -81,4 +82,37 @@ internal class TokenDetailsStateFactory(
): TokenDetailsState {
return loadedTxHistoryConverter.convert(txHistoryEither)
}
fun getStateWithClosedDialog(): TokenDetailsState {
val state = currentStateProvider()
return state.copy(dialogConfig = state.dialogConfig?.copy(isShow = false))
}
fun getStateWithConfirmHideTokenDialog(currency: CryptoCurrency): TokenDetailsState {
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.ConfirmHideConfig(
currencySymbol = currency.symbol,
onConfirmClick = clickIntents::onHideConfirmed,
onCancelClick = clickIntents::onDismissDialog,
),
),
)
}
fun getStateWithLinkedTokensDialog(currency: CryptoCurrency): TokenDetailsState {
return currentStateProvider().copy(
dialogConfig = TokenDetailsDialogConfig(
isShow = true,
onDismissRequest = clickIntents::onDismissDialog,
content = TokenDetailsDialogConfig.DialogContentConfig.HasLinkedTokensConfig(
currencySymbol = currency.symbol,
networkName = currency.network.name,
onConfirmClick = clickIntents::onDismissDialog,
),
),
)
}
}

View file

@ -16,6 +16,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsDialogs
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenDetailsTopAppBar
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.TokenInfoBlock
@ -56,6 +57,8 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
)
txHistoryItems(state = state.txHistoryState, txHistoryItems = txHistoryItems)
}
TokenDetailsDialogs(state = state)
}
}

View file

@ -0,0 +1,229 @@
@file:Suppress("TopLevelPropertyNaming")
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.animation.core.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.*
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
/**
* Just copy paste [DropdownMenu] from material3 with deleting vertical paddings.
*/
@Composable
internal fun TangemDropdownMenu(
expanded: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit,
) {
val expandedStates = remember { MutableTransitionState(false) }
expandedStates.targetState = expanded
if (expandedStates.currentState || expandedStates.targetState) {
val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) }
val density = LocalDensity.current
val popupPositionProvider = DropdownMenuPositionProvider(
offset,
density,
) { parentBounds, menuBounds ->
transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds)
}
Popup(
onDismissRequest = onDismissRequest,
popupPositionProvider = popupPositionProvider,
properties = properties,
) {
DropdownMenuContent(
expandedStates = expandedStates,
transformOriginState = transformOriginState,
modifier = modifier,
content = content,
)
}
}
}
private const val InTransitionDuration = 120
private const val OutTransitionDuration = 75
@Suppress("ReusedModifierInstance", "MagicNumber")
@Composable
private fun DropdownMenuContent(
expandedStates: MutableTransitionState<Boolean>,
transformOriginState: MutableState<TransformOrigin>,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
) {
// Menu open/close animation.
val transition = updateTransition(expandedStates, "DropDownMenu")
val scale by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded
tween(
durationMillis = InTransitionDuration,
easing = LinearOutSlowInEasing,
)
} else {
// Expanded to dismissed.
tween(
durationMillis = 1,
delayMillis = OutTransitionDuration - 1,
)
}
},
label = "",
) {
if (it) {
// Menu is expanded.
1f
} else {
// Menu is dismissed.
0.8f
}
}
val alpha by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded
tween(durationMillis = 30)
} else {
// Expanded to dismissed.
tween(durationMillis = OutTransitionDuration)
}
},
label = "",
) {
if (it) {
// Menu is expanded.
1f
} else {
// Menu is dismissed.
0f
}
}
Card(
modifier = Modifier.graphicsLayer {
scaleX = scale
scaleY = scale
this.alpha = alpha
transformOrigin = transformOriginState.value
},
elevation = CardDefaults.cardElevation(),
) {
Column(
modifier = modifier
.width(IntrinsicSize.Max)
.verticalScroll(rememberScrollState()),
content = content,
)
}
}
private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin {
val pivotX = when {
menuBounds.left >= parentBounds.right -> 0f
menuBounds.right <= parentBounds.left -> 1f
menuBounds.width == 0 -> 0f
else -> {
val intersectionCenter =
(
kotlin.math.max(parentBounds.left, menuBounds.left) +
kotlin.math.min(parentBounds.right, menuBounds.right)
) / 2
(intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
}
}
val pivotY = when {
menuBounds.top >= parentBounds.bottom -> 0f
menuBounds.bottom <= parentBounds.top -> 1f
menuBounds.height == 0 -> 0f
else -> {
val intersectionCenter =
(
kotlin.math.max(parentBounds.top, menuBounds.top) +
kotlin.math.min(parentBounds.bottom, menuBounds.bottom)
) / 2
(intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
}
}
return TransformOrigin(pivotX, pivotY)
}
private val MenuVerticalMargin = 48.dp
@Immutable
internal data class DropdownMenuPositionProvider(
val contentOffset: DpOffset,
val density: Density,
val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> },
) : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize,
): IntOffset {
// The min margin above and below the menu, relative to the screen.
val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() }
// The content offset specified using the dropdown offset parameter.
val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
// Compute horizontal position.
val toRight = anchorBounds.left + contentOffsetX
val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
val toDisplayRight = windowSize.width - popupContentSize.width
val toDisplayLeft = 0
val x = if (layoutDirection == LayoutDirection.Ltr) {
sequenceOf(
toRight,
toLeft,
// If the anchor gets outside of the window on the left, we want to position
// toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft,
)
} else {
sequenceOf(
toLeft,
toRight,
// If the anchor gets outside of the window on the right, we want to position
// toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight,
)
}.firstOrNull {
it >= 0 && it + popupContentSize.width <= windowSize.width
} ?: toLeft
// Compute vertical position.
val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
val toCenter = anchorBounds.top - popupContentSize.height / 2
val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull {
it >= verticalMargin &&
it + popupContentSize.height <= windowSize.height - verticalMargin
} ?: toTop
onPositionCalculated(
anchorBounds,
IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height),
)
return IntOffset(x, y)
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.runtime.Composable
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
@Composable
internal fun TokenDetailsDialogs(state: TokenDetailsState) {
val dialogConfig = state.dialogConfig
if (dialogConfig != null && dialogConfig.isShow) {
TokenDetailsDialog(config = dialogConfig)
}
}
@Composable
private fun TokenDetailsDialog(config: TokenDetailsDialogConfig) {
BasicDialog(
message = config.content.message.resolveReference(),
confirmButton = DialogButton(
title = config.content.confirmButtonConfig.text.resolveReference(),
warning = config.content.confirmButtonConfig.warning,
onClick = config.content.confirmButtonConfig.onClick,
),
onDismissDialog = config.onDismissRequest,
title = config.content.title.resolveReference(),
dismissButton = config.content.cancelButtonConfig?.let { cancelButtonConfig ->
DialogButton(
title = cancelButtonConfig.text.resolveReference(),
warning = cancelButtonConfig.warning,
onClick = cancelButtonConfig.onClick,
)
},
)
}

View file

@ -1,17 +1,31 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.features.tokendetails.impl.R
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarConfig
import com.tangem.features.tokendetails.impl.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
var showDropdownMenu by rememberSaveable { mutableStateOf(false) }
TopAppBar(
navigationIcon = {
IconButton(onClick = config.onBackClick) {
@ -24,13 +38,28 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
},
title = {},
actions = {
IconButton(onClick = config.onMoreClick) {
IconButton(onClick = { showDropdownMenu = true }) {
Icon(
painter = painterResource(id = R.drawable.ic_more_vertical_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = "More",
)
}
TangemDropdownMenu(
expanded = showDropdownMenu,
modifier = Modifier.background(TangemTheme.colors.background.primary),
onDismissRequest = { showDropdownMenu = false },
offset = DpOffset(x = TangemTheme.dimens.spacing20, y = TangemTheme.dimens.spacing10.times(-1)),
content = {
config.tokenDetailsAppBarMenuConfig.items.fastForEach {
AppBarDropdownItem(
item = it,
dismissParent = { showDropdownMenu = false },
)
}
},
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = TangemTheme.colors.background.secondary,
@ -41,6 +70,57 @@ internal fun TokenDetailsTopAppBar(config: TokenDetailsTopAppBarConfig) {
)
}
@Suppress("ComposableEventParameterNaming")
@Composable
private fun AppBarDropdownItem(
item: TokenDetailsAppBarMenuConfig.MenuItem,
dismissParent: () -> Unit,
modifier: Modifier = Modifier,
) {
Text(
modifier = modifier
.clickable {
dismissParent()
item.onClick()
}
.padding(vertical = TangemTheme.dimens.spacing8, horizontal = TangemTheme.dimens.spacing16),
text = item.title.resolveReference(),
style = TangemTheme.typography.body1.copy(color = item.textColorProvider()),
)
}
@Preview
@Composable
private fun Preview_TokenDetailsAppBarDropdownItem_LightTheme() {
TangemTheme(isDark = false) {
AppBarDropdownItem(
modifier = Modifier.background(TangemTheme.colors.background.primary),
dismissParent = {},
item = TokenDetailsAppBarMenuConfig.MenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },
),
)
}
}
@Preview
@Composable
private fun Preview_TokenDetailsAppBarDropdownItem_DarkTheme() {
TangemTheme(isDark = true) {
AppBarDropdownItem(
modifier = Modifier.background(TangemTheme.colors.background.primary),
dismissParent = {},
item = TokenDetailsAppBarMenuConfig.MenuItem(
title = TextReference.Res(id = R.string.token_details_hide_token),
textColorProvider = { TangemTheme.colors.text.warning },
onClick = { },
),
)
}
}
@Preview
@Composable
private fun Preview_TokenDetailsTopAppBar_LightTheme() {

View file

@ -6,8 +6,6 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents {
fun onBackClick()
fun onMoreClick()
fun onSendClick()
fun onReceiveClick()
@ -15,4 +13,10 @@ interface TokenDetailsClickIntents : TxHistoryClickIntents {
fun onSellClick()
fun onSwapClick()
fun onDismissDialog()
fun onHideClick()
fun onHideConfirmed()
}

View file

@ -11,6 +11,7 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.tokens.GetCryptoCurrencyActionsUseCase
import com.tangem.domain.tokens.RemoveCurrencyUseCase
import com.tangem.domain.tokens.GetCurrencyStatusUpdatesUseCase
import com.tangem.domain.tokens.legacy.TradeCryptoAction
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -31,6 +32,7 @@ import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
import kotlin.properties.Delegates
@ -45,6 +47,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val txHistoryItemsUseCase: GetTxHistoryItemsUseCase,
private val getExploreUrlUseCase: GetExploreUrlUseCase,
private val getCryptoCurrencyActionsUseCase: GetCryptoCurrencyActionsUseCase,
private val removeCurrencyUseCase: RemoveCurrencyUseCase,
private val reduxStateHolder: ReduxStateHolder,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
@ -149,10 +152,6 @@ internal class TokenDetailsViewModel @Inject constructor(
router.popBackStack()
}
override fun onMoreClick() {
TODO("Not yet implemented")
}
override fun onBuyClick() {
val status = cryptoCurrencyStatus ?: return
@ -191,6 +190,29 @@ internal class TokenDetailsViewModel @Inject constructor(
reduxStateHolder.dispatch(TradeCryptoAction.New.Swap(cryptoCurrency))
}
override fun onDismissDialog() {
uiState = stateFactory.getStateWithClosedDialog()
}
override fun onHideClick() {
viewModelScope.launch {
val hasLinkedTokens = removeCurrencyUseCase.hasLinkedTokens(wallet.walletId, cryptoCurrency)
uiState = if (hasLinkedTokens) {
stateFactory.getStateWithLinkedTokensDialog(cryptoCurrency)
} else {
stateFactory.getStateWithConfirmHideTokenDialog(cryptoCurrency)
}
}
}
override fun onHideConfirmed() {
viewModelScope.launch {
removeCurrencyUseCase.invoke(wallet.walletId, cryptoCurrency)
.onLeft { Timber.e(it) }
.onRight { router.popBackStack() }
}
}
override fun onExploreClick() {
viewModelScope.launch {
router.openUrl(

View file

@ -65,7 +65,7 @@ private fun ActionsBottomSheetContent_Dark(
@PreviewParameter(ActionsBottomSheetContentConfigProvider::class)
config: ActionsBottomSheetConfig,
) {
TangemTheme(isDark = false) {
TangemTheme(isDark = true) {
// Use preview of content because ModalBottomSheet isn't supported in Preview mode
ActionsBottomSheetContent(actions = config.actions)
}

View file

@ -138,6 +138,7 @@ lifecycle-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", v
# region Compose
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-runtime" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-runtime" }
compose-ui-utils = { module = "androidx.compose.ui:ui-util", version.ref = "compose-runtime" }
compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose-runtime" }
compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" }
compose-material = { module = "androidx.compose.material:material", version.ref = "compose-material" }