From 537731103fd42180b7c93196f781d20a2fe29492 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 1 Sep 2023 11:39:27 +0500 Subject: [PATCH] Updated on 2026-08-14 --- .../viewmodels/TokensListViewModel.kt | 2 +- .../com/tangem/core/ui/res/TangemDimens.kt | 2 +- .../models/remove/RemoveCurrencyError.kt | 3 - .../domain/tokens/RemoveCurrencyUseCase.kt | 10 +- features/tokendetails/impl/build.gradle.kts | 1 + .../tokendetails/TokenDetailsPreviewData.kt | 17 +- .../state/TokenDetailsAppBarMenuConfig.kt | 14 ++ .../tokendetails/state/TokenDetailsState.kt | 2 + .../state/TokenDetailsTopAppBarConfig.kt | 4 +- .../tokendetails/state/TokenInfoBlockState.kt | 2 +- .../components/TokenDetailsDialogConfig.kt | 81 +++++++ .../TokenDetailsSkeletonStateConverter.kt | 17 +- .../state/factory/TokenDetailsStateFactory.kt | 34 +++ .../tokendetails/ui/TokenDetailsScreen.kt | 3 + .../ui/components/DropdownMenu.kt | 229 ++++++++++++++++++ .../ui/components/TokenDetailsDialogs.kt | 37 +++ .../ui/components/TokenDetailsTopAppBar.kt | 84 ++++++- .../viewmodels/TokenDetailsClickIntents.kt | 8 +- .../viewmodels/TokenDetailsViewModel.kt | 30 ++- .../ui/components/TokenActionsBottomSheet.kt | 2 +- gradle/dependencies.toml | 1 + 21 files changed, 557 insertions(+), 26 deletions(-) create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt create mode 100644 features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt index 1cac67b7b2..78ff6ff6ce 100644 --- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/viewmodels/TokensListViewModel.kt @@ -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( diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt index b4fd692f7e..bacd7c615f 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt @@ -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 diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt index 906f30f1fd..0c6f1a0561 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/remove/RemoveCurrencyError.kt @@ -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() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt index 71dc767ed2..dba5642239 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RemoveCurrencyUseCase.kt @@ -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 { 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 } } } \ No newline at end of file diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 1957acae29..7f3fcd1357 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -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) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt index b4af868ab8..1d46a74430 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/TokenDetailsPreviewData.kt @@ -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, ) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt new file mode 100644 index 0000000000..4926ba0b4a --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsAppBarMenuConfig.kt @@ -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) { + data class MenuItem( + val title: TextReference, + val textColorProvider: @Composable () -> Color, + val onClick: () -> Unit, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt index 7006b6c4ef..c6e26d31c5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsState.kt @@ -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?, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt index 8ed33f724b..88ecf53046 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenDetailsTopAppBarConfig.kt @@ -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, ) \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt index ac4c80561c..f6bec38a44 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/TokenInfoBlockState.kt @@ -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, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt new file mode 100644 index 0000000000..02fc3679f3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsDialogConfig.kt @@ -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, + ) + } + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt index a02f4cc5fe..0bb96c3d93 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSkeletonStateConverter.kt @@ -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 { return persistentListOf( TokenDetailsActionButton.Buy(enabled = false, onClick = {}), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index e0ad501d38..6db391a63d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -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, + ), + ), + ) + } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index a54eef8db7..7ce9bb2274 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -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) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt new file mode 100644 index 0000000000..fa6ff8aca3 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/DropdownMenu.kt @@ -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, + transformOriginState: MutableState, + 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) + } +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt new file mode 100644 index 0000000000..174eb35567 --- /dev/null +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsDialogs.kt @@ -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, + ) + }, + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt index a7901787f0..b5e0839679 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/TokenDetailsTopAppBar.kt @@ -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() { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index fba6130de7..b0917c5d2a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -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() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 76357b097f..f1e17d7e44 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -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( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt index a79d41bb23..31ea416730 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/TokenActionsBottomSheet.kt @@ -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) } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 1694c5da76..96b967b35a 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -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" }