Updated on 2026-08-14
This commit is contained in:
parent
56bf8bf3e9
commit
537731103f
21 changed files with 557 additions and 26 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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?,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {}),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue