Updated on 2026-08-14
This commit is contained in:
parent
a5488207ed
commit
2a41321e74
20 changed files with 440 additions and 9 deletions
|
|
@ -25,6 +25,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.TokenDetails
|
|||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.ChooseAddressBottomSheetComponent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.CloreMigrationBottomSheetComponent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.DynamicAddressesBottomSheetComponent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.StakingRegionUnavailableBottomSheetComponent
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet.TransferBottomSheetComponent
|
||||
import com.tangem.features.commonfeatures.api.managefunds.ManageFundsComponent
|
||||
import com.tangem.features.marketing.api.MarketingBannerComponent
|
||||
|
|
@ -237,6 +238,9 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
|
|||
stateFlow = model.transferUiState,
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
)
|
||||
is TokenDetailsBottomSheetConfig.RegionUnavailable -> StakingRegionUnavailableBottomSheetComponent(
|
||||
onDismiss = model.bottomSheetNavigation::dismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -78,6 +78,8 @@ interface TokenDetailsClickIntents {
|
|||
|
||||
fun onYieldInfoClick()
|
||||
|
||||
fun onStakingRegionUnavailableClick()
|
||||
|
||||
// region Clore migration
|
||||
// TODO: Remove after 2025-04-01 when Clore migration ends ([REDACTED_TASK_KEY])
|
||||
|
||||
|
|
@ -175,6 +177,8 @@ internal class EmptyTokenDetailsClickIntents : TokenDetailsClickIntents {
|
|||
|
||||
override fun onYieldInfoClick() { /* no op */ }
|
||||
|
||||
override fun onStakingRegionUnavailableClick() { /* no op */ }
|
||||
|
||||
override fun onQuickTopUpClick(amount: BigDecimal, currencyCode: String) { /* no op */ }
|
||||
|
||||
override fun onCopyAddress(): TextReference? {
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ import com.tangem.domain.models.wallet.UserWalletId
|
|||
import com.tangem.domain.offramp.GetOfframpUrlUseCase
|
||||
import com.tangem.domain.onramp.CheckOnrampAvailabilityUseCase
|
||||
import com.tangem.domain.onramp.model.OnrampSource
|
||||
import com.tangem.domain.staking.FetchStakingOptionsUseCase
|
||||
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
|
||||
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
|
|
@ -143,6 +144,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
private val getExtendedPublicKeyForCurrencyUseCase: GetExtendedPublicKeyForCurrencyUseCase,
|
||||
private val getStakingEntryInfoUseCase: GetStakingEntryInfoUseCase,
|
||||
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
|
||||
private val fetchStakingOptionsUseCase: FetchStakingOptionsUseCase,
|
||||
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
|
||||
private val isDemoCardUseCase: IsDemoCardUseCase,
|
||||
private val isWalletBackupProblematicUseCase: IsWalletBackupProblematicUseCase,
|
||||
|
|
@ -1037,6 +1039,7 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
updateTxHistory()
|
||||
expressTransactionsEventListener.send(ExpressTransactionsEvent.Update)
|
||||
},
|
||||
async { fetchStakingOptionsUseCase() },
|
||||
).awaitAll()
|
||||
uiState.value = stateFactory.getRefreshedState()
|
||||
redesignStateController.update { state ->
|
||||
|
|
@ -1261,6 +1264,10 @@ internal class TokenDetailsModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
override fun onStakingRegionUnavailableClick() {
|
||||
bottomSheetNavigation.activate(TokenDetailsBottomSheetConfig.RegionUnavailable)
|
||||
}
|
||||
|
||||
private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean {
|
||||
if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false
|
||||
|
||||
|
|
|
|||
|
|
@ -40,4 +40,7 @@ sealed class TokenDetailsBottomSheetConfig : Route {
|
|||
|
||||
@Serializable
|
||||
data object Transfer : TokenDetailsBottomSheetConfig()
|
||||
|
||||
@Serializable
|
||||
data object RegionUnavailable : TokenDetailsBottomSheetConfig()
|
||||
}
|
||||
|
|
@ -53,6 +53,7 @@ internal class TokenDetailsStakingInfoConverter(
|
|||
)
|
||||
return when (stakingAvailability) {
|
||||
StakingAvailability.TemporaryUnavailable -> StakingBlockUM.TemporaryUnavailable
|
||||
StakingAvailability.RegionUnavailable -> StakingBlockUM.TemporaryUnavailable
|
||||
StakingAvailability.Unavailable -> null
|
||||
is StakingAvailability.Full -> getStakedBlockOrNull(status)
|
||||
is StakingAvailability.Available -> getStakingInfoBlock(
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ internal class UpdateStakingNotificationTransformer(
|
|||
private fun buildEarnBlock(isBalanceHidden: Boolean): EarnBlockUM? {
|
||||
return when (val availability = stakingAvailability) {
|
||||
StakingAvailability.TemporaryUnavailable -> buildTemporaryUnavailable()
|
||||
StakingAvailability.RegionUnavailable -> buildRegionUnavailableOrNull()
|
||||
StakingAvailability.Unavailable -> null
|
||||
is StakingAvailability.Full -> buildActiveBlockOrNull(isBalanceHidden)
|
||||
is StakingAvailability.Available -> getStakingInfoBlock(availability, isBalanceHidden)
|
||||
|
|
@ -69,6 +70,31 @@ internal class UpdateStakingNotificationTransformer(
|
|||
)
|
||||
}
|
||||
|
||||
private fun buildRegionUnavailableOrNull(): EarnBlockUM? {
|
||||
val status = cryptoCurrencyStatus
|
||||
val stakingBalance = status.value.stakingBalance as? StakingBalance.Data
|
||||
val stakingCryptoAmount = stakingBalance?.getTotalStakingBalance(status.currency.network.rawId)
|
||||
val hasStake = !stakingCryptoAmount.isNullOrZero() || stakingBalance.hasPendingBalances()
|
||||
if (!hasStake) return null
|
||||
return EarnBlockUM.Content(
|
||||
type = EarnBlockUM.Type.Staking,
|
||||
backgroundUM = EarnBlockUM.BackgroundUM.Surface,
|
||||
iconUM = EarnBlockUM.IconUM.Plain(iconRes = CoreUiR.drawable.ic_staking_disable_40),
|
||||
titleUM = EarnBlockUM.TitleUM(
|
||||
text = resourceReference(CoreResR.string.common_staking),
|
||||
style = EarnBlockUM.TitleUM.Style.Large,
|
||||
tone = EarnBlockUM.TitleUM.Tone.Primary,
|
||||
),
|
||||
subtitleUM = EarnBlockUM.SubtitleUM.Text(
|
||||
text = resourceReference(CoreResR.string.staking_error_unavailable_region),
|
||||
style = EarnBlockUM.SubtitleUM.Style.Small,
|
||||
tone = EarnBlockUM.SubtitleUM.Tone.Disabled,
|
||||
),
|
||||
trailingUM = null,
|
||||
onClick = clickIntents::onStakingRegionUnavailableClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun getStakingInfoBlock(
|
||||
availability: StakingAvailability.Available,
|
||||
isBalanceHidden: Boolean,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.bottomsheet
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.tangem.core.ui.components.bottomsheets.message.*
|
||||
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.res.generated.icons.Icons
|
||||
import com.tangem.core.ui.res.generated.icons.ic_error_28
|
||||
import com.tangem.core.res.R as CoreResR
|
||||
|
||||
internal class StakingRegionUnavailableBottomSheetComponent(
|
||||
private val onDismiss: () -> Unit,
|
||||
) : ComposableBottomSheetComponent {
|
||||
|
||||
override fun dismiss() {
|
||||
onDismiss()
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun BottomSheet() {
|
||||
val state = remember {
|
||||
messageBottomSheetUM {
|
||||
infoBlock {
|
||||
vector(imageVector = Icons.ic_error_28) {
|
||||
type = MessageBottomSheetUM.Vector.Type.Attention
|
||||
backgroundType = MessageBottomSheetUM.Vector.BackgroundType.Attention
|
||||
}
|
||||
title = resourceReference(CoreResR.string.common_staking)
|
||||
body = resourceReference(CoreResR.string.staking_error_unavailable_region_description)
|
||||
}
|
||||
primaryButton {
|
||||
text = resourceReference(CoreResR.string.common_close)
|
||||
onClick { closeBs() }
|
||||
}
|
||||
onDismiss { dismiss() }
|
||||
}
|
||||
}
|
||||
MessageBottomSheet(state = state, onDismissRequest = ::dismiss)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
package com.tangem.feature.tokendetails.presentation.tokendetails.state.transformer
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import com.tangem.common.ui.earn.EarnBlockUM
|
||||
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig
|
||||
import com.tangem.core.ui.components.marketprice.MarketPriceBlockState
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.staking.StakingBalance
|
||||
import com.tangem.domain.staking.model.StakingAvailability
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDetailsClickIntents
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
|
||||
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsTopAppBarUM.TitleState
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.math.BigDecimal
|
||||
|
||||
internal class UpdateStakingNotificationTransformerRegionTest {
|
||||
|
||||
private val clickIntents: TokenDetailsClickIntents = mockk(relaxed = true)
|
||||
|
||||
@Test
|
||||
fun `GIVEN region unavailable AND staked WHEN transform THEN tappable region block`() {
|
||||
// Arrange
|
||||
val status = buildStatusWithStake(stakedAmount = BigDecimal("5"))
|
||||
val transformer = UpdateStakingNotificationTransformer(
|
||||
cryptoCurrencyStatus = status,
|
||||
stakingAvailability = StakingAvailability.RegionUnavailable,
|
||||
stakingEntryInfo = null,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isBalanceHidden = false,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(initialState()).earnBlockState
|
||||
|
||||
// Assert
|
||||
val content = result as EarnBlockUM.Content
|
||||
assertThat(content.onClick).isNotNull()
|
||||
assertThat(content.trailingUM).isNull()
|
||||
assertThat(content.subtitleUM).isInstanceOf(EarnBlockUM.SubtitleUM.Text::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `GIVEN region unavailable AND not staked WHEN transform THEN no block`() {
|
||||
// Arrange
|
||||
val status = buildStatus()
|
||||
val transformer = UpdateStakingNotificationTransformer(
|
||||
cryptoCurrencyStatus = status,
|
||||
stakingAvailability = StakingAvailability.RegionUnavailable,
|
||||
stakingEntryInfo = null,
|
||||
appCurrency = AppCurrency.Default,
|
||||
isBalanceHidden = false,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
// Act
|
||||
val result = transformer.transform(initialState()).earnBlockState
|
||||
|
||||
// Assert
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
private fun buildStatus(
|
||||
networkRawId: String = "solana",
|
||||
symbol: String = "SOL",
|
||||
isCoin: Boolean = true,
|
||||
stakingBalance: StakingBalance = mockk(relaxed = true),
|
||||
): CryptoCurrencyStatus {
|
||||
val network = mockk<Network>(relaxed = true) {
|
||||
every { rawId } returns networkRawId
|
||||
every { isTestnet } returns false
|
||||
}
|
||||
val currency = mockk<CryptoCurrency.Coin>(relaxed = true) {
|
||||
every { this@mockk.symbol } returns symbol
|
||||
every { decimals } returns 9
|
||||
every { this@mockk.network } returns network
|
||||
every { id.isCoin } returns isCoin
|
||||
}
|
||||
val value = mockk<CryptoCurrencyStatus.Value>(relaxed = true) {
|
||||
every { this@mockk.stakingBalance } returns stakingBalance
|
||||
every { fiatRate } returns BigDecimal.ONE
|
||||
every { yieldSupplyStatus } returns null
|
||||
}
|
||||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
|
||||
private fun buildStatusWithStake(stakedAmount: BigDecimal): CryptoCurrencyStatus {
|
||||
val network = mockk<Network>(relaxed = true) {
|
||||
every { rawId } returns "ethereum"
|
||||
every { isTestnet } returns false
|
||||
}
|
||||
val currency = mockk<CryptoCurrency.Coin>(relaxed = true) {
|
||||
every { symbol } returns "ETH"
|
||||
every { decimals } returns 18
|
||||
every { this@mockk.network } returns network
|
||||
every { id.isCoin } returns true
|
||||
}
|
||||
val stakingBalance = mockk<StakingBalance.Data.P2PEthPool>(relaxed = true) {
|
||||
every { totalStaked } returns stakedAmount
|
||||
every { unstakingAmount } returns BigDecimal.ZERO
|
||||
every { withdrawableAmount } returns BigDecimal.ZERO
|
||||
every { totalRewards } returns BigDecimal.ZERO
|
||||
}
|
||||
val value = mockk<CryptoCurrencyStatus.Value>(relaxed = true) {
|
||||
every { this@mockk.stakingBalance } returns stakingBalance
|
||||
every { fiatRate } returns BigDecimal.ONE
|
||||
every { yieldSupplyStatus } returns null
|
||||
}
|
||||
return CryptoCurrencyStatus(currency = currency, value = value)
|
||||
}
|
||||
|
||||
private fun initialState(): TokenDetailsUM = TokenDetailsUM(
|
||||
topAppBarUM = TokenDetailsTopAppBarUM(
|
||||
titleState = TitleState.Simple(tokenName = "Ethereum"),
|
||||
subtitle = stringReference("Ethereum network"),
|
||||
onBackClick = {},
|
||||
menuItems = persistentListOf(),
|
||||
),
|
||||
balanceBlockUM = mockk<TokenDetailsBalanceBlockUM>(relaxed = true),
|
||||
notifications = persistentListOf(),
|
||||
earnBlockState = null,
|
||||
marketPriceBlockState = mockk<MarketPriceBlockState>(relaxed = true),
|
||||
pullToRefreshConfig = mockk<PullToRefreshConfig>(relaxed = true),
|
||||
isBalanceHidden = false,
|
||||
isMarketPriceAvailable = false,
|
||||
addFundsUM = AddFundsUM.Loading,
|
||||
transferUM = TransferUM.Loading,
|
||||
zeroBalanceActionsUM = ZeroBalanceActionsUM.Loading,
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue