diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt index 8b11ea19ec..0cefe4c1bd 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/message/TangemMessageEffect.kt @@ -237,11 +237,7 @@ enum class TangemMessageEffect(val isAnimatable: Boolean) { /** Applies a message effect background to the [Modifier] based on the provided [messageEffect] and [radius] */ @Composable -internal fun Modifier.messageEffectBackground( - messageEffect: TangemMessageEffect, - radius: Dp, - contentColor: Color, -): Modifier { +fun Modifier.messageEffectBackground(messageEffect: TangemMessageEffect, radius: Dp, contentColor: Color): Modifier { val isInDarkTheme = LocalIsInDarkTheme.current val borderGradientColors = remember(messageEffect, isInDarkTheme) { messageEffect.getBorderGradient(isInDarkTheme) } val gradientColors = remember(messageEffect, isInDarkTheme) { messageEffect.getColorGradient(isInDarkTheme) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt index d1ecefea55..ac85882198 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/model/TokenDetailsModel.kt @@ -85,7 +85,7 @@ import com.tangem.domain.transaction.error.OpenTrustlineError import com.tangem.domain.transaction.error.SendTransactionError import com.tangem.domain.transaction.usecase.* import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase -import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase +import com.tangem.domain.txhistory.usecase.GetFixedTxHistoryItemsUseCase import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetWalletIconUseCase import com.tangem.domain.wallets.usecase.GetExtendedPublicKeyForCurrencyUseCase @@ -197,7 +197,7 @@ internal class TokenDetailsModel @Inject constructor( private val swapFeedbackUseCase: SwapFeedbackUseCase, private val swapFeatureToggles: SwapFeatureToggles, private val quickTopUpBlockFactory: QuickTopUpBlockFactory, - private val getTxHistoryItemsCountUseCase: GetTxHistoryItemsCountUseCase, + private val getFixedTxHistoryItemsUseCase: GetFixedTxHistoryItemsUseCase, private val checkOnrampAvailabilityUseCase: CheckOnrampAvailabilityUseCase, ) : Model(), TokenDetailsClickIntents, @@ -1424,12 +1424,18 @@ internal class TokenDetailsModel @Inject constructor( emit(null) return@flow } - val txCount = getTxHistoryItemsCountUseCase(userWalletId, cryptoCurrency) + val isHistoryEmpty = getFixedTxHistoryItemsUseCase.getSync( + userWalletId = userWalletId, + currency = cryptoCurrency, + ).fold( + ifLeft = { true }, + ifRight = { it.isEmpty() }, + ) val availability = checkOnrampAvailabilityUseCase(userWallet) emit( quickTopUpBlockFactory.build( currencyStatus = status, - isTxHistoryEmpty = txCount, + isHistoryEmpty = isHistoryEmpty, onrampAvailability = availability, onPresetClick = ::onQuickTopUpClick, onOtherClick = ::onQuickTopUpOtherClick, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/QuickTopUpBlockFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/QuickTopUpBlockFactory.kt index c09bdcf582..8b9fa14111 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/QuickTopUpBlockFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/QuickTopUpBlockFactory.kt @@ -7,7 +7,6 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.onramp.model.OnrampAvailability import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.feature.tokendetails.presentation.tokendetails.state.QuickTopUpBlockUM import com.tangem.features.tokendetails.TokenDetailsFeatureToggles import com.tangem.utils.extensions.isZero @@ -21,7 +20,7 @@ internal class QuickTopUpBlockFactory @Inject constructor( fun build( currencyStatus: CryptoCurrencyStatus, - isTxHistoryEmpty: Either, + isHistoryEmpty: Boolean, onrampAvailability: Either, onPresetClick: (BigDecimal, String) -> Unit, onOtherClick: () -> Unit, @@ -31,10 +30,6 @@ internal class QuickTopUpBlockFactory @Inject constructor( val amount = currencyStatus.value.amount if (amount == null || !amount.isZero()) return null - val isHistoryEmpty = isTxHistoryEmpty.fold( - ifLeft = { it is TxHistoryStateError.EmptyTxHistories }, - ifRight = { it == 0 }, - ) if (!isHistoryEmpty) return null val currency = when (val availability = onrampAvailability.getOrNull()) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/QuickTopUpBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/QuickTopUpBlock.kt index cd2765fa44..18eac82d3c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/QuickTopUpBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/QuickTopUpBlock.kt @@ -1,102 +1,75 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui import android.content.res.Configuration -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.BlendMode -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.util.fastForEach import com.tangem.core.res.R +import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton +import com.tangem.core.ui.ds.button.TangemButtonShape +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.ds.message.TangemMessageEffect +import com.tangem.core.ui.ds.message.messageEffectBackground import com.tangem.core.ui.extensions.combinedReference import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.utils.StringsSigns import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreviewRedesign import com.tangem.feature.tokendetails.presentation.tokendetails.state.QuickTopUpBlockUM +import com.tangem.utils.StringsSigns import kotlinx.collections.immutable.persistentListOf -private val quickTopUpGradientBrush = Brush.linearGradient( - colors = listOf(Color(0xFFEDE5F3), Color(0xFFD7EDD9)), - start = Offset(0f, 0f), - end = Offset(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY), -) - -private val quickTopUpBorderBrush = Brush.sweepGradient( - listOf( - Color(0x0D000000), - Color(0x26000000), - Color(0x0D000000), - Color(0x26000000), - ), -) - @Composable internal fun QuickTopUpBlock(state: QuickTopUpBlockUM, modifier: Modifier = Modifier) { - val shape = RoundedCornerShape(TangemTheme.dimens.radius20) - - Box( - modifier = modifier - .fillMaxWidth() - .clip(shape) - .background(brush = quickTopUpGradientBrush) - .border(width = 1.dp, brush = quickTopUpBorderBrush, shape = shape), - ) { + Box(modifier = modifier.fillMaxWidth()) { + Box( + modifier = Modifier + .matchParentSize() + .messageEffectBackground( + messageEffect = TangemMessageEffect.Magic, + radius = TangemTheme.dimens2.x6, + contentColor = TangemTheme.colors2.surface.level3, + ), + ) Column( - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + modifier = Modifier + .fillMaxWidth() + .padding(TangemTheme.dimens2.x3), + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x4), ) { - val textColor = TangemTheme.colors.text.primary1 + val titleColor = TangemTheme.colors2.text.neutral.primary Text( text = combinedReference( stringReference("${StringsSigns.LIGHTNING} "), resourceReference(R.string.quick_top_up_title), ).resolveReference(), - style = TangemTheme.typography.subtitle1.copy(fontWeight = FontWeight.SemiBold), + style = TangemTheme.typography2.bodySemibold16, modifier = Modifier.graphicsLayer { - colorFilter = ColorFilter.tint(textColor, BlendMode.SrcIn) + colorFilter = ColorFilter.tint(titleColor, BlendMode.SrcIn) }, ) Row( - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6), + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1_5), ) { state.amounts.fastForEach { amountUM -> - Button( + PrimaryInverseTangemButton( + text = amountUM.displayValue, onClick = amountUM.onClick, - shape = CircleShape, - contentPadding = PaddingValues( - horizontal = TangemTheme.dimens.spacing12, - vertical = TangemTheme.dimens.spacing0, - ), - colors = ButtonDefaults.buttonColors( - containerColor = TangemTheme.colors.background.primary, - contentColor = TangemTheme.colors.text.primary1, - ), - modifier = Modifier.heightIn(TangemTheme.dimens.size36), - elevation = null, - ) { - Text( - text = amountUM.displayValue.resolveReference(), - style = TangemTheme.typography.subtitle1.copy(fontWeight = FontWeight.SemiBold), - ) - } + size = TangemButtonSize.X9, + shape = TangemButtonShape.Rounded, + ) } } } @@ -104,6 +77,7 @@ internal fun QuickTopUpBlock(state: QuickTopUpBlockUM, modifier: Modifier = Modi } @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable private fun QuickTopUpBlock_Preview() { TangemThemePreviewRedesign { @@ -129,7 +103,7 @@ private fun QuickTopUpBlock_Preview() { ), ), ), - modifier = Modifier.padding(TangemTheme.dimens.spacing12), + modifier = Modifier.padding(TangemTheme.dimens2.x3), ) } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/QuickTopUpBlockFactoryTest.kt b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/QuickTopUpBlockFactoryTest.kt index aa46e392f0..a9a6c479a3 100644 --- a/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/QuickTopUpBlockFactoryTest.kt +++ b/features/tokendetails/impl/src/test/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/QuickTopUpBlockFactoryTest.kt @@ -11,7 +11,6 @@ import com.tangem.domain.onramp.model.OnrampAvailability import com.tangem.domain.onramp.model.OnrampCountry import com.tangem.domain.onramp.model.OnrampCurrency import com.tangem.domain.onramp.model.error.OnrampError -import com.tangem.domain.txhistory.models.TxHistoryStateError import com.tangem.features.tokendetails.TokenDetailsFeatureToggles import io.mockk.every import io.mockk.mockk @@ -70,10 +69,6 @@ internal class QuickTopUpBlockFactoryTest { private val notSupported: OnrampAvailability = OnrampAvailability.NotSupported(country = countryMock) - private val emptyHistory = TxHistoryStateError.EmptyTxHistories.left() - private val histWithItems = 5.right() - private val histRightZero = 0.right() - @Test fun `returns null when feature toggle is disabled`() { val disabledToggles: TokenDetailsFeatureToggles = mockk { @@ -83,7 +78,7 @@ internal class QuickTopUpBlockFactoryTest { val result = disabledFactory.build( currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, + isHistoryEmpty = true, onrampAvailability = availableUsd.right(), onPresetClick = { _, _ -> }, onOtherClick = {}, @@ -96,7 +91,7 @@ internal class QuickTopUpBlockFactoryTest { fun `returns null when balance is non-zero`() { val result = factory.build( currencyStatus = nonZeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, + isHistoryEmpty = true, onrampAvailability = availableUsd.right(), onPresetClick = { _, _ -> }, onOtherClick = {}, @@ -106,10 +101,27 @@ internal class QuickTopUpBlockFactoryTest { } @Test - fun `returns null when history has transactions`() { + fun `returns null when balance is loading (amount is null)`() { + val loadingStatus: CryptoCurrencyStatus = mockk { + every { value } returns CryptoCurrencyStatus.Loading + } + + val result = factory.build( + currencyStatus = loadingStatus, + isHistoryEmpty = true, + onrampAvailability = availableUsd.right(), + onPresetClick = { _, _ -> }, + onOtherClick = {}, + ) + + assertThat(result).isNull() + } + + @Test + fun `returns null when history is not empty`() { val result = factory.build( currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = histWithItems, + isHistoryEmpty = false, onrampAvailability = availableUsd.right(), onPresetClick = { _, _ -> }, onOtherClick = {}, @@ -122,7 +134,7 @@ internal class QuickTopUpBlockFactoryTest { fun `returns null when onramp is not available`() { val result = factory.build( currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, + isHistoryEmpty = true, onrampAvailability = notSupported.right(), onPresetClick = { _, _ -> }, onOtherClick = {}, @@ -131,11 +143,24 @@ internal class QuickTopUpBlockFactoryTest { assertThat(result).isNull() } + @Test + fun `returns null when onramp availability is error`() { + val result = factory.build( + currencyStatus = zeroBalanceStatus, + isHistoryEmpty = true, + onrampAvailability = OnrampError.DataError(code = "error", description = null).left(), + onPresetClick = { _, _ -> }, + onOtherClick = {}, + ) + + assertThat(result).isNull() + } + @Test fun `returns null when currency is not USD or EUR`() { val result = factory.build( currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, + isHistoryEmpty = true, onrampAvailability = OnrampAvailability.Available( country = countryMock, currency = gbpCurrency, @@ -151,7 +176,7 @@ internal class QuickTopUpBlockFactoryTest { fun `returns block with USD presets when all conditions met`() { val result = factory.build( currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, + isHistoryEmpty = true, onrampAvailability = availableUsd.right(), onPresetClick = { _, _ -> }, onOtherClick = {}, @@ -178,7 +203,7 @@ internal class QuickTopUpBlockFactoryTest { val result = factory.build( currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, + isHistoryEmpty = true, onrampAvailability = availableEur.right(), onPresetClick = { _, _ -> }, onOtherClick = {}, @@ -195,19 +220,6 @@ internal class QuickTopUpBlockFactoryTest { assertThat(amounts.last().isOther).isTrue() } - @Test - fun `returns block when history count is right zero (boundary case)`() { - val result = factory.build( - currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = histRightZero, - onrampAvailability = availableUsd.right(), - onPresetClick = { _, _ -> }, - onOtherClick = {}, - ) - - assertThat(result).isNotNull() - } - @Test fun `returns block when ConfirmResidency and country supports onramp with USD`() { val usdCountry = OnrampCountry( @@ -224,7 +236,7 @@ internal class QuickTopUpBlockFactoryTest { val result = factory.build( currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, + isHistoryEmpty = true, onrampAvailability = confirmResidency.right(), onPresetClick = { _, _ -> }, onOtherClick = {}, @@ -240,86 +252,6 @@ internal class QuickTopUpBlockFactoryTest { ).inOrder() } - @Test - fun `returns null when onramp availability is error`() { - val result = factory.build( - currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, - onrampAvailability = OnrampError.DataError(code = "error", description = null).left(), - onPresetClick = { _, _ -> }, - onOtherClick = {}, - ) - - assertThat(result).isNull() - } - - @Test - fun `returns null when balance is loading (amount is null)`() { - val loadingStatus: CryptoCurrencyStatus = mockk { - every { value } returns CryptoCurrencyStatus.Loading - } - - val result = factory.build( - currencyStatus = loadingStatus, - isTxHistoryEmpty = emptyHistory, - onrampAvailability = availableUsd.right(), - onPresetClick = { _, _ -> }, - onOtherClick = {}, - ) - - assertThat(result).isNull() - } - - @Test - fun `returns null when tx history is not implemented`() { - val result = factory.build( - currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = TxHistoryStateError.TxHistoryNotImplemented.left(), - onrampAvailability = availableUsd.right(), - onPresetClick = { _, _ -> }, - onOtherClick = {}, - ) - - assertThat(result).isNull() - } - - @Test - fun `returns null when tx history fetch fails with data error`() { - val result = factory.build( - currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = TxHistoryStateError.DataError(RuntimeException("network error")).left(), - onrampAvailability = availableUsd.right(), - onPresetClick = { _, _ -> }, - onOtherClick = {}, - ) - - assertThat(result).isNull() - } - - @Test - fun `returns null when ConfirmResidency with non-USD or EUR default currency`() { - val gbpCountry = OnrampCountry( - id = "gb", - name = "United Kingdom", - code = "GB", - image = "", - alpha3 = "GBR", - continent = "Europe", - defaultCurrency = gbpCurrency, - onrampAvailable = true, - ) - - val result = factory.build( - currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, - onrampAvailability = OnrampAvailability.ConfirmResidency(country = gbpCountry).right(), - onPresetClick = { _, _ -> }, - onOtherClick = {}, - ) - - assertThat(result).isNull() - } - @Test fun `returns null when ConfirmResidency but country does not support onramp`() { val restrictedCountry = OnrampCountry( @@ -336,7 +268,7 @@ internal class QuickTopUpBlockFactoryTest { val result = factory.build( currencyStatus = zeroBalanceStatus, - isTxHistoryEmpty = emptyHistory, + isHistoryEmpty = true, onrampAvailability = confirmResidency.right(), onPresetClick = { _, _ -> }, onOtherClick = {},