Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-26 19:17:46 +05:00
commit e0262f6d9f
19 changed files with 420 additions and 121 deletions

View file

@ -14,7 +14,7 @@ import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.Flow
@ -110,7 +110,7 @@ internal class DefaultStakingRepository(
override suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
networkStatus: NetworkStatus,
address: String,
integrationId: String,
refresh: Boolean,
) = withContext(dispatchers.io) {
@ -120,7 +120,7 @@ internal class DefaultStakingRepository(
block = {
val result = stakeKitApi.getSingleYieldBalance(
integrationId = integrationId,
body = getBalanceRequestData(networkStatus),
body = getBalanceRequestData(address, integrationId),
).getOrThrow()
stakingBalanceStore.store(
@ -136,20 +136,27 @@ internal class DefaultStakingRepository(
override fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
networkStatus: NetworkStatus,
address: String,
integrationId: String,
): Flow<List<YieldBalance>> = channelFlow {
): Flow<YieldBalance> = channelFlow {
launch(dispatchers.io) {
stakingBalanceStore.get(integrationId)
.collectLatest {
send(yieldBalanceConverter.convertList(it))
send(
yieldBalanceConverter.convert(
YieldBalanceConverter.Data(
balance = it,
integrationId = integrationId,
),
),
)
}
}
withContext(dispatchers.io) {
fetchSingleYieldBalance(
userWalletId,
networkStatus,
address,
integrationId,
)
}
@ -157,7 +164,7 @@ internal class DefaultStakingRepository(
override suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
networks: Set<NetworkStatus>,
addresses: List<CryptoCurrencyAddress>,
integrationId: String,
refresh: Boolean,
) = withContext(dispatchers.io) {
@ -166,7 +173,7 @@ internal class DefaultStakingRepository(
skipCache = refresh,
block = {
val result = stakeKitApi.getMultipleYieldBalances(
networks.map(::getBalanceRequestData),
addresses.map { getBalanceRequestData(it.address, integrationId) },
).getOrThrow()
stakingBalanceStore.store(result)
@ -176,18 +183,18 @@ internal class DefaultStakingRepository(
override fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
networks: Set<NetworkStatus>,
addresses: List<CryptoCurrencyAddress>,
integrationId: String,
): Flow<List<YieldBalanceList>> = channelFlow {
): Flow<YieldBalanceList> = channelFlow {
launch(dispatchers.io) {
stakingBalanceStore.get()
.collectLatest { send(yieldBalanceListConverter.convertList(it)) }
.collectLatest { send(yieldBalanceListConverter.convert(it)) }
}
withContext(dispatchers.io) {
fetchMultiYieldBalance(
userWalletId,
networks,
addresses,
integrationId,
)
}
@ -202,23 +209,17 @@ internal class DefaultStakingRepository(
return yields.map { yieldConverter.convert(it) }
}
private fun getBalanceRequestData(networkStatus: NetworkStatus): YieldBalanceRequestBody {
val networkAddress = when (val network = networkStatus.value) {
NetworkStatus.MissedDerivation -> null
is NetworkStatus.NoAccount -> network.address
is NetworkStatus.Unreachable -> network.address
is NetworkStatus.Verified -> network.address
}
val address = networkAddress?.defaultAddress?.value.orEmpty()
private fun getBalanceRequestData(address: String, integrationId: String): YieldBalanceRequestBody {
return YieldBalanceRequestBody(
addresses = Address(
address = address,
additionalAddresses = null, // todo fill additional addresses metadata if needed
explorerUrl = "", // todo fill exporer url
explorerUrl = "", // todo fill exporer url [REDACTED_JIRA]
),
args = YieldBalanceRequestBody.YieldBalanceRequestArgs(
validatorAddresses = listOf(), // todo add validator addresses
validatorAddresses = listOf(), // todo add validators [REDACTED_JIRA]
),
integrationId = integrationId,
)
}

View file

@ -1,16 +1,36 @@
package com.tangem.data.staking.converters
import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO
import com.tangem.domain.staking.model.BalanceItem
import com.tangem.domain.staking.model.BalanceType
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.staking.model.YieldBalanceItem
import com.tangem.utils.converter.Converter
internal class YieldBalanceConverter : Converter<BalanceDTO, YieldBalance> {
override fun convert(value: BalanceDTO): YieldBalance {
return YieldBalance(
type = BalanceType.valueOf(value.type.name),
amount = value.amount,
pricePerShare = value.pricePerShare,
)
internal class YieldBalanceConverter : Converter<YieldBalanceConverter.Data, YieldBalance> {
override fun convert(value: Data): YieldBalance {
return if (value.balance.isEmpty()) {
YieldBalance.Empty
} else {
YieldBalance.Data(
balance = YieldBalanceItem(
items = value.balance.map { item ->
BalanceItem(
type = BalanceType.valueOf(item.type.name),
amount = item.amount,
pricePerShare = item.pricePerShare,
rawCurrencyId = item.tokenDTO.coinGeckoId,
)
},
integrationId = value.integrationId,
),
)
}
}
data class Data(
val balance: List<BalanceDTO>,
val integrationId: String?,
)
}

View file

@ -4,15 +4,26 @@ import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrap
import com.tangem.domain.staking.model.YieldBalanceList
import com.tangem.utils.converter.Converter
internal class YieldBalanceListConverter : Converter<YieldBalanceWrapperDTO, YieldBalanceList> {
internal class YieldBalanceListConverter : Converter<List<YieldBalanceWrapperDTO>, YieldBalanceList> {
internal val converter by lazy(LazyThreadSafetyMode.NONE) {
YieldBalanceConverter()
}
override fun convert(value: YieldBalanceWrapperDTO): YieldBalanceList {
return YieldBalanceList(
balances = converter.convertList(value.balances),
)
override fun convert(value: List<YieldBalanceWrapperDTO>): YieldBalanceList {
return if (value.isEmpty()) {
YieldBalanceList.Empty
} else {
YieldBalanceList.Data(
balances = value.map {
converter.convert(
YieldBalanceConverter.Data(
balance = it.balances,
integrationId = it.integrationId,
),
)
},
)
}
}
}

View file

@ -2,10 +2,25 @@ package com.tangem.domain.staking.model
import java.math.BigDecimal
data class YieldBalance(
sealed class YieldBalance {
data class Data(
val balance: YieldBalanceItem,
) : YieldBalance()
data object Empty : YieldBalance()
}
data class YieldBalanceItem(
val items: List<BalanceItem>,
val integrationId: String?,
)
data class BalanceItem(
val type: BalanceType,
val amount: BigDecimal,
val pricePerShare: BigDecimal,
val rawCurrencyId: String?,
)
enum class BalanceType {

View file

@ -1,5 +1,17 @@
package com.tangem.domain.staking.model
data class YieldBalanceList(
val balances: List<YieldBalance>,
)
sealed class YieldBalanceList {
data class Data(
val balances: List<YieldBalance>,
) : YieldBalanceList() {
fun getBalance(rawCurrencyId: String?): YieldBalance? {
return balances.firstOrNull { yield ->
(yield as? YieldBalance.Data)?.balance?.items
?.any { it.rawCurrencyId == rawCurrencyId } == true
}
}
}
data object Empty : YieldBalanceList()
}

View file

@ -2,7 +2,6 @@ package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
class FetchStakingYieldBalanceUseCase(
@ -11,13 +10,13 @@ class FetchStakingYieldBalanceUseCase(
suspend operator fun invoke(
userWalletId: UserWalletId,
networkStatus: NetworkStatus,
address: String,
integrationId: String,
refresh: Boolean = false,
): Either<Throwable, Unit> = Either.catch {
stakingRepository.fetchSingleYieldBalance(
userWalletId = userWalletId,
networkStatus = networkStatus,
address = address,
integrationId = integrationId,
refresh = refresh,
)

View file

@ -6,7 +6,6 @@ import arrow.core.right
import com.tangem.domain.core.utils.EitherFlow
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
@ -17,14 +16,14 @@ class GetStakingYieldBalanceUseCase(
operator fun invoke(
userWalletId: UserWalletId,
networkStatus: NetworkStatus,
address: String,
integrationId: String,
): EitherFlow<Throwable, List<YieldBalance>> {
): EitherFlow<Throwable, YieldBalance> {
return stakingRepository.getSingleYieldBalanceFlow(
userWalletId = userWalletId,
networkStatus = networkStatus,
address = address,
integrationId = integrationId,
).map<List<YieldBalance>, Either<Throwable, List<YieldBalance>>> { it.right() }
).map<YieldBalance, Either<Throwable, YieldBalance>> { it.right() }
.catch { emit(it.left()) }
}
}

View file

@ -2,7 +2,7 @@ package com.tangem.domain.staking.repositories
import com.tangem.domain.staking.model.*
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@ -23,27 +23,27 @@ interface StakingRepository {
suspend fun fetchSingleYieldBalance(
userWalletId: UserWalletId,
networkStatus: NetworkStatus,
address: String,
integrationId: String,
refresh: Boolean = false,
)
fun getSingleYieldBalanceFlow(
userWalletId: UserWalletId,
networkStatus: NetworkStatus,
address: String,
integrationId: String,
): Flow<List<YieldBalance>>
): Flow<YieldBalance>
suspend fun fetchMultiYieldBalance(
userWalletId: UserWalletId,
networks: Set<NetworkStatus>,
addresses: List<CryptoCurrencyAddress>,
integrationId: String,
refresh: Boolean = false,
)
fun getMultiYieldBalanceFlow(
userWalletId: UserWalletId,
networks: Set<NetworkStatus>,
addresses: List<CryptoCurrencyAddress>,
integrationId: String,
): Flow<List<YieldBalanceList>>
): Flow<YieldBalanceList>
}

View file

@ -19,6 +19,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component
import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import java.math.BigDecimal
@Suppress("LargeClass")
internal object TokenDetailsPreviewData {
@ -121,12 +122,13 @@ internal object TokenDetailsPreviewData {
)
val balanceContent = TokenDetailsBalanceBlockState.Content(
actionButtons = actionButtons,
fiatBalance = "91,50$",
cryptoBalance = "966,96 XLM",
isStakingEnabled = true,
fiatBalance = BigDecimal.ZERO,
cryptoBalance = BigDecimal.ZERO,
balanceSegmentedButtonConfig = balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
onBalanceSelect = {},
displayCryptoBalance = "966,96 XLM",
displayFiatBalance = "91,50$",
)
val balanceError = TokenDetailsBalanceBlockState.Error(
actionButtons = actionButtons,
@ -139,9 +141,12 @@ internal object TokenDetailsPreviewData {
private val stakingLoading = StakingBlocksState(
stakingAvailable = StakingAvailable.Loading(iconState),
stakingBalance = StakingBalance.Content(
cryptoAmount = stringReference("5 SOL"),
fiatAmount = stringReference("456.34 $"),
rewardAmount = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")),
cryptoValue = stringReference("5 SOL"),
fiatValue = stringReference("456.34 $"),
rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")),
cryptoAmount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
onStakeClicked = {},
),
)
@ -320,9 +325,12 @@ internal object TokenDetailsPreviewData {
onStakeClicked = {},
),
stakingBalance = StakingBalance.Content(
cryptoAmount = stringReference("5 SOL"),
fiatAmount = stringReference("456.34 $"),
rewardAmount = resourceReference(R.string.staking_details_rewards_to_claim, wrappedList("0.43 $")),
cryptoValue = stringReference("5 SOL"),
fiatValue = stringReference("456.34 $"),
rewardValue = resourceReference(R.string.staking_details_no_rewards_to_claim, wrappedList("0.43 $")),
cryptoAmount = BigDecimal.ZERO,
fiatAmount = BigDecimal.ZERO,
onStakeClicked = {},
),
),
notifications = persistentListOf(),

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import java.math.BigDecimal
internal data class StakingBlocksState(
val stakingAvailable: StakingAvailable,
@ -30,8 +31,11 @@ sealed class StakingBalance {
data object Empty : StakingBalance()
data class Content(
val cryptoAmount: TextReference,
val fiatAmount: TextReference,
val rewardAmount: TextReference,
val cryptoValue: TextReference,
val fiatValue: TextReference,
val rewardValue: TextReference,
val cryptoAmount: BigDecimal?,
val fiatAmount: BigDecimal?,
val onStakeClicked: () -> Unit,
) : StakingBalance()
}

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
internal sealed class TokenDetailsBalanceBlockState {
@ -19,10 +20,11 @@ internal sealed class TokenDetailsBalanceBlockState {
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
override val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>,
override val selectedBalanceType: BalanceType,
val fiatBalance: String,
val cryptoBalance: String,
val isStakingEnabled: Boolean,
val fiatBalance: BigDecimal?,
val cryptoBalance: BigDecimal?,
val onBalanceSelect: (TokenBalanceSegmentedButtonConfig) -> Unit,
val displayCryptoBalance: String,
val displayFiatBalance: String,
) : TokenDetailsBalanceBlockState()
data class Error(

View file

@ -0,0 +1,75 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import java.math.BigDecimal
internal class TokenDetailsBalanceSelectStateConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
) : Converter<TokenBalanceSegmentedButtonConfig, TokenDetailsState> {
override fun convert(value: TokenBalanceSegmentedButtonConfig): TokenDetailsState {
return with(currentStateProvider()) {
if (stakingBlocksState.stakingBalance !is StakingBalance.Content) return this
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider() ?: return this
val stakingCryptoAmount = stakingBlocksState.stakingBalance.cryptoAmount
val stakingFiatAmount = stakingBlocksState.stakingBalance.fiatAmount
copy(
tokenBalanceBlockState = if (tokenBalanceBlockState is TokenDetailsBalanceBlockState.Content) {
tokenBalanceBlockState.copy(
selectedBalanceType = value.type,
displayFiatBalance = formatFiatAmount(
status = cryptoCurrencyStatus.value,
stakingFiatAmount = stakingFiatAmount,
selectedBalanceType = value.type,
appCurrency = appCurrencyProvider(),
),
displayCryptoBalance = formatCryptoAmount(
status = cryptoCurrencyStatus,
stakingCryptoAmount = stakingCryptoAmount,
selectedBalanceType = value.type,
),
)
} else {
tokenBalanceBlockState
},
)
}
}
private fun formatFiatAmount(
status: CryptoCurrencyStatus.Value,
stakingFiatAmount: BigDecimal?,
selectedBalanceType: BalanceType,
appCurrency: AppCurrency,
): String {
val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount)
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = totalAmount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
private fun formatCryptoAmount(
status: CryptoCurrencyStatus,
stakingCryptoAmount: BigDecimal?,
selectedBalanceType: BalanceType,
): String {
val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount)
return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals)
}
}

View file

@ -6,41 +6,54 @@ import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.marketprice.utils.PriceChangeConverter
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType
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.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsTxHistoryTransactionStateConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.utils.getBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Provider
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class TokenDetailsLoadedBalanceConverter(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val isStakingEnabled: Boolean,
private val symbol: String,
private val decimals: Int,
private val clickIntents: TokenDetailsClickIntents,
) : Converter<Either<CurrencyStatusError, CryptoCurrencyStatus>, TokenDetailsState> {
) : Converter<TokenDetailsLoadedBalanceConverter.Data, TokenDetailsState> {
private val txHistoryItemConverter by lazy {
TokenDetailsTxHistoryTransactionStateConverter(symbol, decimals, clickIntents)
}
override fun convert(value: Either<CurrencyStatusError, CryptoCurrencyStatus>): TokenDetailsState {
return value.fold(ifLeft = { convertError() }, ifRight = ::convert)
data class Data(
val maybeCryptoCurrencyStatus: Either<CurrencyStatusError, CryptoCurrencyStatus>,
val maybeYieldBalance: Either<Throwable, YieldBalance>?,
)
override fun convert(value: Data): TokenDetailsState {
return value.maybeCryptoCurrencyStatus.fold(
ifLeft = { convertError() },
ifRight = { convert(it, value.maybeYieldBalance?.getOrNull() ?: YieldBalance.Empty) },
)
}
private fun convertError(): TokenDetailsState {
val state = currentStateProvider()
return state.copy(
isStakingBlockShown = false,
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(
actionButtons = state.tokenBalanceBlockState.actionButtons,
balanceSegmentedButtonConfig = state.tokenBalanceBlockState.balanceSegmentedButtonConfig,
@ -51,12 +64,23 @@ internal class TokenDetailsLoadedBalanceConverter(
)
}
private fun convert(status: CryptoCurrencyStatus): TokenDetailsState {
private fun convert(status: CryptoCurrencyStatus, yieldBalance: YieldBalance): TokenDetailsState {
val state = currentStateProvider()
val currencyName = state.marketPriceBlockState.currencySymbol
val pendingTxs = status.value.pendingTransactions.map(txHistoryItemConverter::convert).toPersistentList()
val stakingCryptoAmount = (yieldBalance as? YieldBalance.Data)?.let {
yieldBalance.balance.items.sumOf { it.amount }
}
val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) }
return state.copy(
tokenBalanceBlockState = getBalanceState(state.tokenBalanceBlockState, status),
tokenBalanceBlockState = getBalanceState(
currentState = state.tokenBalanceBlockState,
status = status,
stakingCryptoAmount = stakingCryptoAmount,
stakingFiatAmount = stakingFiatAmount,
),
stakingBlocksState = getYieldBalance(status, yieldBalance, state),
marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName),
pendingTxs = pendingTxs,
txHistoryState = if (state.txHistoryState is TxHistoryState.NotSupported) {
@ -70,6 +94,8 @@ internal class TokenDetailsLoadedBalanceConverter(
private fun getBalanceState(
currentState: TokenDetailsBalanceBlockState,
status: CryptoCurrencyStatus,
stakingCryptoAmount: BigDecimal?,
stakingFiatAmount: BigDecimal?,
): TokenDetailsBalanceBlockState {
return when (status.value) {
is CryptoCurrencyStatus.NoQuote,
@ -78,9 +104,19 @@ internal class TokenDetailsLoadedBalanceConverter(
is CryptoCurrencyStatus.Custom,
-> TokenDetailsBalanceBlockState.Content(
actionButtons = currentState.actionButtons,
fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()),
cryptoBalance = formatCryptoAmount(status),
isStakingEnabled = isStakingEnabled,
cryptoBalance = status.value.amount,
fiatBalance = status.value.fiatAmount,
displayFiatBalance = formatFiatAmount(
status.value,
stakingFiatAmount,
currentState.selectedBalanceType,
appCurrencyProvider(),
),
displayCryptoBalance = formatCryptoAmount(
status,
stakingCryptoAmount,
currentState.selectedBalanceType,
),
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
onBalanceSelect = clickIntents::onBalanceSelect,
selectedBalanceType = currentState.selectedBalanceType,
@ -88,7 +124,7 @@ internal class TokenDetailsLoadedBalanceConverter(
is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(
actionButtons = currentState.actionButtons,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
selectedBalanceType = currentState.selectedBalanceType,
)
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
@ -96,11 +132,58 @@ internal class TokenDetailsLoadedBalanceConverter(
-> TokenDetailsBalanceBlockState.Error(
actionButtons = currentState.actionButtons,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
selectedBalanceType = currentState.selectedBalanceType,
)
}
}
private fun getYieldBalance(
status: CryptoCurrencyStatus,
yieldBalance: YieldBalance,
state: TokenDetailsState,
): StakingBlocksState {
val stakingCryptoAmount = (yieldBalance as? YieldBalance.Data)?.let {
yieldBalance.balance.items.sumOf { it.amount }
}
val stakingFiatAmount = stakingCryptoAmount?.let { status.value.fiatRate?.multiply(it) }
val stakingRewardAmount = (yieldBalance as? YieldBalance.Data)?.let {
yieldBalance.balance.items.sumOf { it.amount.multiply(it.pricePerShare) }
}
val stakingBalance = if (stakingCryptoAmount == null) {
StakingBalance.Empty
} else {
StakingBalance.Content(
cryptoAmount = stakingCryptoAmount,
fiatAmount = stakingFiatAmount,
cryptoValue = stringReference(
BigDecimalFormatter.formatCryptoAmount(stakingCryptoAmount, symbol, decimals),
),
fiatValue = stringReference(
BigDecimalFormatter.formatFiatAmount(
stakingFiatAmount,
appCurrencyProvider().code,
appCurrencyProvider().symbol,
),
),
rewardValue = resourceReference(
R.string.staking_details_rewards_to_claim,
wrappedList(
BigDecimalFormatter.formatFiatAmount(
stakingRewardAmount,
appCurrencyProvider().code,
appCurrencyProvider().symbol,
),
),
),
onStakeClicked = clickIntents::onStakeBannerClick,
)
}
return state.stakingBlocksState.copy(
stakingAvailable = state.stakingBlocksState.stakingAvailable,
stakingBalance = stakingBalance,
)
}
private fun getMarketPriceState(
status: CryptoCurrencyStatus.Value,
currencySymbol: String,
@ -158,19 +241,30 @@ internal class TokenDetailsLoadedBalanceConverter(
)
}
private fun formatFiatAmount(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String {
private fun formatFiatAmount(
status: CryptoCurrencyStatus.Value,
stakingFiatAmount: BigDecimal?,
selectedBalanceType: BalanceType,
appCurrency: AppCurrency,
): String {
val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
val totalAmount = fiatAmount.getBalance(selectedBalanceType, stakingFiatAmount)
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatAmount,
fiatAmount = totalAmount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
}
private fun formatCryptoAmount(status: CryptoCurrencyStatus): String {
private fun formatCryptoAmount(
status: CryptoCurrencyStatus,
stakingCryptoAmount: BigDecimal?,
selectedBalanceType: BalanceType,
): String {
val amount = status.value.amount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
val totalAmount = amount.getBalance(selectedBalanceType, stakingCryptoAmount)
return BigDecimalFormatter.formatCryptoAmount(amount, status.currency.symbol, status.currency.decimals)
return BigDecimalFormatter.formatCryptoAmount(totalAmount, status.currency.symbol, status.currency.decimals)
}
}

View file

@ -18,6 +18,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.tokens.error.CurrencyStatusError
import com.tangem.domain.tokens.model.*
import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
@ -42,9 +43,9 @@ import kotlinx.coroutines.flow.MutableStateFlow
internal class TokenDetailsStateFactory(
private val currentStateProvider: Provider<TokenDetailsState>,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus?>,
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
private val isStakingEnabled: Boolean,
symbol: String,
decimals: Int,
) {
@ -64,7 +65,6 @@ internal class TokenDetailsStateFactory(
TokenDetailsLoadedBalanceConverter(
currentStateProvider = currentStateProvider,
appCurrencyProvider = appCurrencyProvider,
isStakingEnabled = isStakingEnabled,
symbol = symbol,
decimals = decimals,
clickIntents = clickIntents,
@ -107,14 +107,25 @@ internal class TokenDetailsStateFactory(
)
}
private val balanceSelectStateConverter by lazy {
TokenDetailsBalanceSelectStateConverter(
currentStateProvider = currentStateProvider,
appCurrencyProvider = appCurrencyProvider,
cryptoCurrencyStatusProvider = cryptoCurrencyStatusProvider,
)
}
fun getInitialState(screenArgument: CryptoCurrency): TokenDetailsState {
return skeletonStateConverter.convert(value = screenArgument)
}
fun getCurrencyLoadedBalanceState(
cryptoCurrencyEither: Either<CurrencyStatusError, CryptoCurrencyStatus>,
yieldBalanceEither: Either<Throwable, YieldBalance>?,
): TokenDetailsState {
return tokenDetailsLoadedBalanceConverter.convert(cryptoCurrencyEither)
return tokenDetailsLoadedBalanceConverter.convert(
TokenDetailsLoadedBalanceConverter.Data(cryptoCurrencyEither, yieldBalanceEither),
)
}
fun getManageButtonsState(actions: List<TokenActionsState.ActionState>): TokenDetailsState {
@ -352,12 +363,7 @@ internal class TokenDetailsStateFactory(
fun getStateWithUpdatedBalanceSegmentedButtonConfig(
buttonConfig: TokenBalanceSegmentedButtonConfig,
): TokenDetailsState {
return with(currentStateProvider()) {
val updatedState = (tokenBalanceBlockState as? TokenDetailsBalanceBlockState.Content)
?.copy(selectedBalanceType = buttonConfig.type)
?: tokenBalanceBlockState
copy(tokenBalanceBlockState = updatedState)
}
return balanceSelectStateConverter.convert(buttonConfig)
}
private fun TokenDetailsAppBarMenuConfig.updateMenu(

View file

@ -0,0 +1,12 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state.utils
import com.tangem.feature.tokendetails.presentation.tokendetails.state.BalanceType
import java.math.BigDecimal
fun BigDecimal.getBalance(selectedBalanceType: BalanceType, stakingAmount: BigDecimal?): BigDecimal {
return if (selectedBalanceType == BalanceType.ALL && stakingAmount != null) {
this.plus(stakingAmount)
} else {
this
}
}

View file

@ -108,6 +108,7 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
modifier = itemModifier,
isBalanceHidden = state.isBalanceHidden,
state = state.tokenBalanceBlockState,
isStakingAvailable = state.isStakingBlockShown,
)
}
items(
@ -141,16 +142,6 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
}
if (state.isStakingBlockShown) {
item(
key = StakingAvailable::class.java,
contentType = StakingAvailable::class.java,
content = {
TokenStakingBlock(
modifier = itemModifier,
state = state.stakingBlocksState.stakingAvailable,
)
},
)
if (state.stakingBlocksState.stakingBalance is StakingBalance.Content) {
item(
key = StakingBalance::class.java,
@ -163,6 +154,16 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
},
)
}
item(
key = StakingAvailable::class.java,
contentType = StakingAvailable::class.java,
content = {
TokenStakingBlock(
modifier = itemModifier,
state = state.stakingBlocksState.stakingAvailable,
)
},
)
}
swapTransactionsItems(

View file

@ -29,6 +29,7 @@ import kotlinx.collections.immutable.toImmutableList
internal fun TokenDetailsBalanceBlock(
state: TokenDetailsBalanceBlockState,
isBalanceHidden: Boolean,
isStakingAvailable: Boolean,
modifier: Modifier = Modifier,
) {
Surface(
@ -53,7 +54,7 @@ internal fun TokenDetailsBalanceBlock(
.weight(1f)
.padding(top = TangemTheme.dimens.spacing12),
)
BalanceButtons(state)
if (isStakingAvailable) BalanceButtons(state)
}
FiatBalance(
state = state,
@ -94,7 +95,7 @@ private fun FiatBalance(
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
text = if (isBalanceHidden) STARS else state.fiatBalance,
text = if (isBalanceHidden) STARS else state.displayFiatBalance,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
@ -122,7 +123,7 @@ private fun CryptoBalance(
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
text = if (isBalanceHidden) STARS else state.cryptoBalance,
text = if (isBalanceHidden) STARS else state.displayCryptoBalance,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
@ -171,7 +172,7 @@ private fun Preview_TokenDetailsBalanceBlock(
@PreviewParameter(TokenDetailsBalanceBlockStateProvider::class) state: TokenDetailsBalanceBlockState,
) {
TangemThemePreview {
TokenDetailsBalanceBlock(state = state, isBalanceHidden = false)
TokenDetailsBalanceBlock(state = state, isBalanceHidden = false, isStakingAvailable = true)
}
}

View file

@ -2,10 +2,14 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -30,6 +34,11 @@ fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modi
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
onClick = state.onStakeClicked,
)
.padding(TangemTheme.dimens.spacing12),
) {
Column(
@ -44,7 +53,7 @@ fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modi
)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
Text(
text = state.fiatAmount.resolveReference(),
text = state.fiatValue.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
@ -54,13 +63,13 @@ fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modi
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.cryptoAmount.resolveReference(),
text = state.cryptoValue.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
Text(
text = state.rewardAmount.resolveReference(),
text = state.rewardValue.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)

View file

@ -30,6 +30,7 @@ import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase
import com.tangem.domain.staking.GetStakingAvailabilityUseCase
import com.tangem.domain.staking.GetStakingEntryInfoUseCase
import com.tangem.domain.staking.GetStakingYieldBalanceUseCase
import com.tangem.domain.staking.GetYieldUseCase
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.tokens.*
@ -107,6 +108,7 @@ internal class TokenDetailsViewModel @Inject constructor(
private val stakingFeatureToggles: StakingFeatureToggles,
private val getStakingAvailabilityUseCase: GetStakingAvailabilityUseCase,
private val getYieldUseCase: GetYieldUseCase,
private val getStakingYieldBalanceUseCase: GetStakingYieldBalanceUseCase,
private val swapRepository: SwapRepository,
private val swapTransactionRepository: SwapTransactionRepository,
private val quotesRepository: QuotesRepository,
@ -133,6 +135,7 @@ internal class TokenDetailsViewModel @Inject constructor(
?: error("This screen can't open without `CryptoCurrency`")
private val userWallet: UserWallet
private var stakingIntegrationId: String? = null
lateinit var router: InnerTokenDetailsRouter
@ -149,11 +152,11 @@ internal class TokenDetailsViewModel @Inject constructor(
private val stateFactory = TokenDetailsStateFactory(
currentStateProvider = Provider { uiState.value },
appCurrencyProvider = Provider(selectedAppCurrencyFlow::value),
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
clickIntents = this,
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
featureToggles = tokenDetailsFeatureToggles,
isStakingEnabled = stakingFeatureToggles.isStakingEnabled,
)
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -217,7 +220,6 @@ internal class TokenDetailsViewModel @Inject constructor(
}
private fun updateContent() {
subscribeOnCurrencyStatusUpdates()
subscribeOnExchangeTransactionsUpdates()
updateTxHistory(refresh = false, showItemsLoading = true)
@ -279,12 +281,36 @@ internal class TokenDetailsViewModel @Inject constructor(
)
.distinctUntilChanged()
.onEach { maybeCurrencyStatus ->
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus)
maybeCurrencyStatus.onRight { status ->
cryptoCurrencyStatus = status
updateButtons(currencyStatus = status)
updateWarnings(status)
}
maybeCurrencyStatus.fold(
ifLeft = {
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(
maybeCurrencyStatus,
null,
)
},
ifRight = { status ->
cryptoCurrencyStatus = status
if (stakingIntegrationId != null) {
getStakingYieldBalanceUseCase(
userWalletId = userWalletId,
address = status.value.networkAddress?.defaultAddress?.value.orEmpty(),
integrationId = stakingIntegrationId.orEmpty(),
).onEach { maybeYieldBalance ->
internalUiState.value = stateFactory.getCurrencyLoadedBalanceState(
maybeCurrencyStatus,
maybeYieldBalance,
)
}.launchIn(viewModelScope)
} else {
internalUiState.value =
stateFactory.getCurrencyLoadedBalanceState(maybeCurrencyStatus, null)
}
updateButtons(currencyStatus = status)
updateWarnings(status)
},
)
currencyStatusAnalyticsSender.send(maybeCurrencyStatus)
}
.flowOn(dispatchers.main)
@ -385,8 +411,12 @@ internal class TokenDetailsViewModel @Inject constructor(
)
internalUiState.value = stateFactory.getStateWithUpdatedStakingAvailability(stakingAvailability)
if (stakingAvailability is StakingAvailability.Available) {
stakingIntegrationId = stakingAvailability.integrationId
val stakingInfo = getStakingEntryInfoUseCase(stakingAvailability.integrationId)
subscribeOnCurrencyStatusUpdates()
internalUiState.value = stateFactory.getStateWithStaking(stakingInfo)
} else {
subscribeOnCurrencyStatusUpdates()
}
}
}