Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-20 17:58:31 +05:00
parent 6b01eacf9d
commit b192949d27
24 changed files with 387 additions and 79 deletions

View file

@ -84,6 +84,7 @@
<string name="common_copy">Копировать</string>
<string name="common_copy_address">Скопировать адрес</string>
<string name="common_create">Создать</string>
<string name="common_custom">Свое</string>
<string name="common_delete">Удалить</string>
<string name="common_disabled">Отключено</string>
<string name="common_done">Готово</string>
@ -95,7 +96,6 @@
<string name="common_explorer">Обозреватель</string>
<string name="common_fee_label">Комиссия</string>
<string name="common_fee_selector_footer">Сетевые комиссии это плата пользователя за обработку и подтверждение транзакций. Размер комиссии зависит от нагрузки на сеть, объема транзакции и приоритета исполнения. %s</string>
<string name="common_fee_selector_option_custom">Свое</string>
<string name="common_fee_selector_option_fast">Быстро</string>
<string name="common_fee_selector_option_market">По рынку</string>
<string name="common_fee_selector_option_slow">Медленно</string>
@ -302,6 +302,7 @@
<item quantity="many">%1$d из %2$d кошельков</item>
<item quantity="other">%1$d из %2$d кошельков</item>
</plurals>
<string name="manage_tokens_remove">Удалить</string>
<string name="manage_tokens_search_placeholder">например Bitcoin</string>
<string name="manage_tokens_toast_portfolio_updated">Ваш портфель был обновлен</string>
<string name="manage_tokens_unavailable_description">Выбранный токен не доступен в кошельке на данный момент. Но не переживайте, вы можете выразить свой интерес проголосовав за его добавление.</string>

View file

@ -67,6 +67,7 @@
<string name="cardano_max_amount_has_token_title">Not enough ADA</string>
<string name="common_accept">Accept</string>
<string name="common_access_denied">Access denied</string>
<string name="common_all">All</string>
<string name="common_allow">Allow</string>
<string name="common_apply">Apply</string>
<string name="common_approval">Approval</string>
@ -84,6 +85,7 @@
<string name="common_copy">Copy</string>
<string name="common_copy_address">Copy address</string>
<string name="common_create">Create</string>
<string name="common_custom">Custom</string>
<string name="common_delete">Delete</string>
<string name="common_disabled">Disabled</string>
<string name="common_done">Done</string>
@ -95,7 +97,6 @@
<string name="common_explorer">Explorer</string>
<string name="common_fee_label">Fee</string>
<string name="common_fee_selector_footer">Network fees are charges users pay to process and confirm transactions. The fee amount can be affected by network congestion, transaction size, and execution priority. %s</string>
<string name="common_fee_selector_option_custom">Custom</string>
<string name="common_fee_selector_option_fast">Fast</string>
<string name="common_fee_selector_option_market">Market</string>
<string name="common_fee_selector_option_slow">Slow</string>
@ -304,6 +305,7 @@
<item quantity="one">%1$d of %2$d wallet</item>
<item quantity="other">%1$d of %2$d wallets</item>
</plurals>
<string name="manage_tokens_remove">Remove</string>
<string name="manage_tokens_search_placeholder">e.g. BTC I trust, hodl I must</string>
<string name="manage_tokens_toast_portfolio_updated">Your portfolio has been updated</string>
<string name="manage_tokens_unavailable_description">The selected token is currently unavailable for actions within the crypto wallet. But worry not, you can express your interest by upvoting it.</string>
@ -562,9 +564,11 @@
<string name="staking_details_on_stake">On stake</string>
<string name="staking_details_reward_claiming">Reward claiming</string>
<string name="staking_details_reward_schedule">Reward schedule</string>
<string name="staking_details_rewards_to_claim">Rewards to claim: %s</string>
<string name="staking_details_title">Staking %s</string>
<string name="staking_details_unbonding_period">Unbonding Period</string>
<string name="staking_details_warmup_period">Warmup period</string>
<string name="staking_native">Native staking</string>
<string name="staking_notification_earn_rewards_text" formatted="false">Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days.</string>
<string name="staking_notification_earn_rewards_title">Earn staking rewards</string>
<string name="staking_rewards">Rewards</string>

View file

@ -8,10 +8,7 @@ import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -21,8 +18,9 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
@ -42,7 +40,7 @@ import kotlinx.collections.immutable.persistentListOf
*/
@Composable
inline fun <reified T> SegmentedButtons(
config: PersistentList<T>,
config: ImmutableList<T>,
crossinline onClick: (T) -> Unit,
modifier: Modifier = Modifier,
color: Color = TangemTheme.colors.background.tertiary,
@ -51,7 +49,7 @@ inline fun <reified T> SegmentedButtons(
showIndication: Boolean = true,
initialSelectedItem: T? = null,
isEnabled: Boolean = true,
crossinline buttonContent: @Composable (T) -> Unit,
crossinline buttonContent: @Composable BoxScope.(T) -> Unit,
) {
if (config.isEmpty() || config.size == 1) return
@ -97,7 +95,7 @@ inline fun <reified T> SegmentedButtons(
onClick(config[index])
},
) {
buttonContent.invoke(config[index])
buttonContent.invoke(this, config[index])
}
}
}

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
import com.tangem.utils.Strings
@Composable
fun SelectorRowItem(
@ -127,7 +128,7 @@ private fun RowScope.SelectorValueContent(
)
if (postDot != null) {
Text(
text = "",
text = Strings.DOT,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,

View file

@ -102,9 +102,11 @@ data class TangemDimens internal constructor(
val spacing2: Dp = 2.dp,
val spacing3: Dp = 3.dp,
val spacing4: Dp = 4.dp,
val spacing5: Dp = 5.dp,
val spacing6: Dp = 6.dp,
val spacing8: Dp = 8.dp,
val spacing10: Dp = 10.dp,
val spacing11: Dp = 11.dp,
val spacing12: Dp = 12.dp,
val spacing14: Dp = 14.dp,
val spacing15: Dp = 15.dp,

View file

@ -3,4 +3,5 @@ package com.tangem.utils
object Strings {
const val STARS = "\u2217\u2217\u2217"
const val DOT = ""
}

View file

@ -63,7 +63,7 @@ internal fun SendSpeedSelector(
onSelect = { clickIntents.onFeeSelectorClick(FeeType.Fast) },
)
SendSpeedSelectorItem(
titleRes = R.string.common_fee_selector_option_custom,
titleRes = R.string.common_custom,
iconRes = R.drawable.ic_edit_24,
feeType = FeeType.Custom,
state = state,

View file

@ -53,7 +53,7 @@ internal fun FeeBlock(feeState: SendStates.FeeState, isClickDisabled: Boolean, o
FeeType.Slow -> R.string.common_fee_selector_option_slow to R.drawable.ic_tortoise_24
FeeType.Market -> R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
FeeType.Fast -> R.string.common_fee_selector_option_fast to R.drawable.ic_hare_24
FeeType.Custom -> R.string.common_fee_selector_option_custom to R.drawable.ic_edit_24
FeeType.Custom -> R.string.common_custom to R.drawable.ic_edit_24
}
} else {
R.string.common_fee_selector_option_market to R.drawable.ic_bird_24

View file

@ -9,7 +9,9 @@ import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.components.transactions.state.TxHistoryState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
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.res.TangemTheme
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
@ -18,6 +20,7 @@ import com.tangem.features.tokendetails.impl.R
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
@Suppress("LargeClass")
internal object TokenDetailsPreviewData {
val tokenDetailsTopAppBarConfig = TokenDetailsTopAppBarConfig(
@ -100,17 +103,47 @@ internal object TokenDetailsPreviewData {
TokenDetailsActionButton.Swap(dimContent = false, onClick = {}),
)
val balanceLoading = TokenDetailsBalanceBlockState.Loading(actionButtons = actionButtons)
private val balanceSegmentedButtonConfig = persistentListOf(
TokenBalanceSegmentedButtonConfig(
title = resourceReference(R.string.common_all),
type = BalanceType.ALL,
),
TokenBalanceSegmentedButtonConfig(
title = resourceReference(R.string.staking_details_available),
type = BalanceType.AVAILABLE,
),
)
val balanceLoading = TokenDetailsBalanceBlockState.Loading(
actionButtons = actionButtons,
balanceSegmentedButtonConfig = balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
)
val balanceContent = TokenDetailsBalanceBlockState.Content(
actionButtons = actionButtons,
fiatBalance = "91,50$",
cryptoBalance = "966,96 XLM",
isStakingEnabled = true,
balanceSegmentedButtonConfig = balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
onBalanceSelect = {},
)
val balanceError = TokenDetailsBalanceBlockState.Error(
actionButtons = actionButtons,
balanceSegmentedButtonConfig = balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
)
val balanceError = TokenDetailsBalanceBlockState.Error(actionButtons = actionButtons)
private val marketPriceLoading = MarketPriceBlockState.Loading(currencySymbol = "USDT")
private val stakingLoading = StakingBlockState.Loading(iconState = iconState)
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 $")),
),
)
private val pullToRefreshConfig = TokenDetailsPullToRefreshConfig(
isRefreshing = false,
@ -246,7 +279,7 @@ internal object TokenDetailsPreviewData {
tokenInfoBlockState = tokenInfoBlockState,
tokenBalanceBlockState = balanceLoading,
marketPriceBlockState = marketPriceLoading,
stakingBlockState = stakingLoading,
stakingBlocksState = stakingLoading,
notifications = persistentListOf(),
txHistoryState = TxHistoryState.Content(
contentItems = MutableStateFlow(
@ -278,12 +311,19 @@ internal object TokenDetailsPreviewData {
type = PriceChangeType.UP,
),
),
stakingBlockState = StakingBlockState.Content(
interestRate = "7.38",
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
onStakeClicked = {},
stakingBlocksState = StakingBlocksState(
stakingAvailable = StakingAvailable.Content(
interestRate = "7.38",
periodInDays = 4,
tokenSymbol = "XLM",
iconState = iconState,
onStakeClicked = {},
),
stakingBalance = StakingBalance.Content(
cryptoAmount = stringReference("5 SOL"),
fiatAmount = stringReference("456.34 $"),
rewardAmount = resourceReference(R.string.staking_details_rewards_to_claim, wrappedList("0.43 $")),
),
),
notifications = persistentListOf(),
txHistoryState = TxHistoryState.NotSupported(

View file

@ -1,15 +1,20 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
internal data class StakingBlocksState(
val stakingAvailable: StakingAvailable,
val stakingBalance: StakingBalance,
)
@Immutable
internal sealed interface StakingBlockState {
internal sealed interface StakingAvailable {
val iconState: IconState
data class Error(override val iconState: IconState) : StakingBlockState
data class Error(override val iconState: IconState) : StakingAvailable
data class Loading(override val iconState: IconState) : StakingBlockState
data class Loading(override val iconState: IconState) : StakingAvailable
data class Content(
override val iconState: IconState,
@ -17,5 +22,16 @@ internal sealed interface StakingBlockState {
val periodInDays: Int,
val tokenSymbol: String,
val onStakeClicked: () -> Unit,
) : StakingBlockState
) : StakingAvailable
}
@Immutable
sealed class StakingBalance {
data object Empty : StakingBalance()
data class Content(
val cryptoAmount: TextReference,
val fiatAmount: TextReference,
val rewardAmount: TextReference,
) : StakingBalance()
}

View file

@ -0,0 +1,13 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
import com.tangem.core.ui.extensions.TextReference
data class TokenBalanceSegmentedButtonConfig(
val title: TextReference,
val type: BalanceType,
)
enum class BalanceType {
ALL,
AVAILABLE,
}

View file

@ -6,19 +6,29 @@ import kotlinx.collections.immutable.ImmutableList
internal sealed class TokenDetailsBalanceBlockState {
abstract val actionButtons: ImmutableList<TokenDetailsActionButton>
abstract val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>
abstract val selectedBalanceType: BalanceType
data class Loading(
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
override val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>,
override val selectedBalanceType: BalanceType,
) : TokenDetailsBalanceBlockState()
data class Content(
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
override val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>,
override val selectedBalanceType: BalanceType,
val fiatBalance: String,
val cryptoBalance: String,
val isStakingEnabled: Boolean,
val onBalanceSelect: (TokenBalanceSegmentedButtonConfig) -> Unit,
) : TokenDetailsBalanceBlockState()
data class Error(
override val actionButtons: ImmutableList<TokenDetailsActionButton>,
override val balanceSegmentedButtonConfig: ImmutableList<TokenBalanceSegmentedButtonConfig>,
override val selectedBalanceType: BalanceType,
) : TokenDetailsBalanceBlockState()
fun copyActionButtons(buttons: ImmutableList<TokenDetailsActionButton>): TokenDetailsBalanceBlockState {

View file

@ -17,7 +17,7 @@ internal data class TokenDetailsState(
val tokenInfoBlockState: TokenInfoBlockState,
val tokenBalanceBlockState: TokenDetailsBalanceBlockState,
val marketPriceBlockState: MarketPriceBlockState,
val stakingBlockState: StakingBlockState,
val stakingBlocksState: StakingBlocksState,
val notifications: ImmutableList<TokenDetailsNotification>,
val pendingTxs: PersistentList<TransactionState>,
val swapTxs: PersistentList<SwapTransactionsState>,

View file

@ -10,6 +10,7 @@ import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
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.components.TokenDetailsNotification
@ -23,6 +24,7 @@ import kotlinx.collections.immutable.toPersistentList
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,
@ -39,7 +41,11 @@ internal class TokenDetailsLoadedBalanceConverter(
private fun convertError(): TokenDetailsState {
val state = currentStateProvider()
return state.copy(
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(state.tokenBalanceBlockState.actionButtons),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Error(
actionButtons = state.tokenBalanceBlockState.actionButtons,
balanceSegmentedButtonConfig = state.tokenBalanceBlockState.balanceSegmentedButtonConfig,
selectedBalanceType = state.tokenBalanceBlockState.selectedBalanceType,
),
marketPriceBlockState = MarketPriceBlockState.Error(state.marketPriceBlockState.currencySymbol),
notifications = persistentListOf(TokenDetailsNotification.NetworksUnreachable),
)
@ -74,12 +80,24 @@ internal class TokenDetailsLoadedBalanceConverter(
actionButtons = currentState.actionButtons,
fiatBalance = formatFiatAmount(status.value, appCurrencyProvider()),
cryptoBalance = formatCryptoAmount(status),
isStakingEnabled = isStakingEnabled,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
onBalanceSelect = clickIntents::onBalanceSelect,
selectedBalanceType = currentState.selectedBalanceType,
)
is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(
actionButtons = currentState.actionButtons,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
)
is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading(currentState.actionButtons)
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> TokenDetailsBalanceBlockState.Error(currentState.actionButtons)
-> TokenDetailsBalanceBlockState.Error(
actionButtons = currentState.actionButtons,
balanceSegmentedButtonConfig = currentState.balanceSegmentedButtonConfig,
selectedBalanceType = BalanceType.ALL,
)
}
}

View file

@ -48,9 +48,16 @@ internal class TokenDetailsSkeletonStateConverter(
)
},
),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(actionButtons = createButtons()),
tokenBalanceBlockState = TokenDetailsBalanceBlockState.Loading(
actionButtons = createButtons(),
balanceSegmentedButtonConfig = createBalanceSegmentedButtonConfig(),
selectedBalanceType = BalanceType.ALL,
),
marketPriceBlockState = MarketPriceBlockState.Loading(value.symbol),
stakingBlockState = StakingBlockState.Loading(iconState = iconState),
stakingBlocksState = StakingBlocksState(
stakingAvailable = StakingAvailable.Loading(iconState),
stakingBalance = StakingBalance.Empty,
),
notifications = persistentListOf(),
pendingTxs = persistentListOf(),
swapTxs = persistentListOf(),
@ -99,6 +106,19 @@ internal class TokenDetailsSkeletonStateConverter(
)
}
private fun createBalanceSegmentedButtonConfig(): ImmutableList<TokenBalanceSegmentedButtonConfig> {
return persistentListOf(
TokenBalanceSegmentedButtonConfig(
title = resourceReference(R.string.common_all),
type = BalanceType.ALL,
),
TokenBalanceSegmentedButtonConfig(
title = resourceReference(R.string.staking_details_available),
type = BalanceType.AVAILABLE,
),
)
}
private fun createPullToRefresh(): TokenDetailsPullToRefreshConfig = TokenDetailsPullToRefreshConfig(
isRefreshing = false,
onRefresh = clickIntents::onRefreshSwipe,

View file

@ -24,9 +24,7 @@ import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning
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.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsAppBarMenuConfig
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.TokenDetailsDialogConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadedTxHistoryConverter
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.txhistory.TokenDetailsLoadingTxHistoryConverter
@ -46,6 +44,7 @@ internal class TokenDetailsStateFactory(
private val appCurrencyProvider: Provider<AppCurrency>,
private val clickIntents: TokenDetailsClickIntents,
private val featureToggles: TokenDetailsFeatureToggles,
private val isStakingEnabled: Boolean,
symbol: String,
decimals: Int,
) {
@ -65,6 +64,7 @@ internal class TokenDetailsStateFactory(
TokenDetailsLoadedBalanceConverter(
currentStateProvider = currentStateProvider,
appCurrencyProvider = appCurrencyProvider,
isStakingEnabled = isStakingEnabled,
symbol = symbol,
decimals = decimals,
clickIntents = clickIntents,
@ -218,7 +218,7 @@ internal class TokenDetailsStateFactory(
fun getStateWithStaking(stakingEither: Either<Throwable, StakingEntryInfo>): TokenDetailsState {
return currentStateProvider().copy(
stakingBlockState = stakingStateConverter.convert(stakingEither),
stakingBlocksState = stakingStateConverter.convert(stakingEither),
)
}
@ -348,6 +348,17 @@ 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)
}
}
private fun TokenDetailsAppBarMenuConfig.updateMenu(
cardTypesResolver: CardTypesResolver,
isBitcoin: Boolean,

View file

@ -3,7 +3,9 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state.factory
import arrow.core.Either
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlocksState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents
import com.tangem.utils.Provider
@ -12,25 +14,31 @@ import com.tangem.utils.converter.Converter
internal class TokenStakingStateConverter(
private val clickIntents: TokenDetailsClickIntents,
private val currentStateProvider: Provider<TokenDetailsState>,
) : Converter<Either<Throwable, StakingEntryInfo>, StakingBlockState> {
) : Converter<Either<Throwable, StakingEntryInfo>, StakingBlocksState> {
override fun convert(value: Either<Throwable, StakingEntryInfo>): StakingBlockState {
override fun convert(value: Either<Throwable, StakingEntryInfo>): StakingBlocksState {
value.fold(
ifLeft = {
return StakingBlockState.Error(
iconState = currentStateProvider().tokenInfoBlockState.iconState,
return StakingBlocksState(
stakingAvailable = StakingAvailable.Error(
iconState = currentStateProvider().tokenInfoBlockState.iconState,
),
stakingBalance = StakingBalance.Empty,
)
},
ifRight = {
return StakingBlockState.Content(
interestRate = BigDecimalFormatter.formatPercent(
percent = it.interestRate,
useAbsoluteValue = true,
return StakingBlocksState(
stakingAvailable = StakingAvailable.Content(
interestRate = BigDecimalFormatter.formatPercent(
percent = it.interestRate,
useAbsoluteValue = true,
),
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = currentStateProvider().tokenInfoBlockState.iconState,
onStakeClicked = clickIntents::onStakeBannerClick,
),
periodInDays = it.periodInDays,
tokenSymbol = it.tokenSymbol,
iconState = currentStateProvider().tokenInfoBlockState.iconState,
onStakeClicked = clickIntents::onStakeBannerClick,
stakingBalance = StakingBalance.Empty,
)
},
)

View file

@ -41,10 +41,10 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsNotification
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.*
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
@ -52,6 +52,8 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.T
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheet
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.swapTransactionsItems
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.StakingBalanceBlock
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking.TokenStakingBlock
// TODO: Split to blocks [REDACTED_JIRA]
@Suppress("LongMethod")
@ -140,10 +142,27 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) {
if (state.isStakingBlockShown) {
item(
key = StakingBlockState::class.java,
contentType = StakingBlockState::class.java,
content = { TokenStakingBlock(modifier = itemModifier, state = state.stakingBlockState) },
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,
contentType = StakingBalance::class.java,
content = {
StakingBalanceBlock(
state = state.stakingBlocksState.stakingBalance,
modifier = itemModifier,
)
},
)
}
}
swapTransactionsItems(

View file

@ -13,6 +13,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
@ -35,20 +37,23 @@ internal fun TokenDetailsBalanceBlock(
color = TangemTheme.colors.background.primary,
) {
Column {
Box(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing12)
.padding(horizontal = TangemTheme.dimens.spacing12)
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.spacing24),
contentAlignment = Alignment.CenterStart,
) {
Text(
text = stringResource(id = R.string.common_balance_title),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.subtitle2,
maxLines = 1,
modifier = Modifier
.weight(1f)
.padding(top = TangemTheme.dimens.spacing12),
)
BalanceButtons(state)
}
FiatBalance(
state = state,
@ -130,6 +135,35 @@ private fun CryptoBalance(
}
}
@Composable
private fun BalanceButtons(state: TokenDetailsBalanceBlockState) {
if (state !is TokenDetailsBalanceBlockState.Content) return
SegmentedButtons(
config = state.balanceSegmentedButtonConfig,
onClick = state.onBalanceSelect,
showIndication = false,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing11)
.width(IntrinsicSize.Min),
) { config ->
Text(
text = config.title.resolveReference(),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.caption1,
maxLines = 1,
modifier = Modifier
.padding(
start = TangemTheme.dimens.spacing5,
end = TangemTheme.dimens.spacing5,
top = TangemTheme.dimens.spacing3,
bottom = TangemTheme.dimens.spacing3,
)
.align(Alignment.Center),
)
}
}
@Preview(widthDp = 328, heightDp = 152)
@Preview(widthDp = 328, heightDp = 152, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -0,0 +1,98 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBalance
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.Strings
@Composable
fun StakingBalanceBlock(state: StakingBalance.Content, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.padding(TangemTheme.dimens.spacing12),
) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4),
modifier = Modifier.weight(1f),
) {
Text(
text = stringResource(R.string.staking_native),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(vertical = TangemTheme.dimens.spacing2),
)
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8)) {
Text(
text = state.fiatAmount.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = Strings.DOT,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.cryptoAmount.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
)
}
Text(
text = state.rewardAmount.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
)
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun StakingBalanceBlock_Preview(
@PreviewParameter(StakingBalanceBlockPreviewProvider::class) data: StakingBalance,
) {
TangemThemePreview {
StakingBalanceBlock(
state = data as StakingBalance.Content,
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
)
}
}
private class StakingBalanceBlockPreviewProvider : PreviewParameterProvider<StakingBalance> {
override val values: Sequence<StakingBalance>
get() = sequenceOf(
TokenDetailsPreviewData.tokenDetailsState_1.stakingBlocksState.stakingBalance,
TokenDetailsPreviewData.tokenDetailsState_2.stakingBlocksState.stakingBalance,
)
}
// endregion

View file

@ -1,11 +1,12 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.staking
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Text
import androidx.compose.runtime.*
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
@ -14,14 +15,17 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.*
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerW8
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.GRAY_SCALE_ALPHA
import com.tangem.core.ui.utils.GrayscaleColorFilter
import com.tangem.core.ui.utils.NORMAL_ALPHA
import com.tangem.feature.tokendetails.presentation.tokendetails.state.IconState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.StakingAvailable
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.CurrencyIcon
import com.tangem.features.tokendetails.impl.R
/**
@ -31,7 +35,7 @@ import com.tangem.features.tokendetails.impl.R
* @param modifier modifier
*/
@Composable
internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Modifier) {
internal fun TokenStakingBlock(state: StakingAvailable, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(
@ -49,7 +53,7 @@ internal fun TokenStakingBlock(state: StakingBlockState, modifier: Modifier = Mo
}
@Composable
private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) {
private fun Content(state: StakingAvailable, modifier: Modifier = Modifier) {
AnimatedContent(
modifier = modifier.heightIn(min = TangemTheme.dimens.size60),
targetState = state,
@ -57,24 +61,24 @@ private fun Content(state: StakingBlockState, modifier: Modifier = Modifier) {
label = "Update the content",
) { stakingBlockState ->
when (stakingBlockState) {
is StakingBlockState.Content -> {
is StakingAvailable.Content -> {
StakingContent(
stakingBlockState = stakingBlockState,
iconState = stakingBlockState.iconState,
)
}
is StakingBlockState.Loading -> {
is StakingAvailable.Loading -> {
StakingLoading(
iconState = stakingBlockState.iconState,
)
}
is StakingBlockState.Error -> Row {} // TODO staking
is StakingAvailable.Error -> Row {} // TODO staking
}
}
}
@Composable
private fun StakingContent(stakingBlockState: StakingBlockState.Content, iconState: IconState) {
private fun StakingContent(stakingBlockState: StakingAvailable.Content, iconState: IconState) {
Column {
Row {
val (alpha, colorFilter) = remember(iconState.isGrayscale) {
@ -177,24 +181,24 @@ private fun StakingLoading(iconState: IconState) {
@Composable
private fun Preview_TokenStakingBlock(
@PreviewParameter(StakingBlockStateProvider::class)
state: StakingBlockState,
state: StakingAvailable,
) {
TangemThemePreview {
TokenStakingBlock(state = state)
}
}
private class StakingBlockStateProvider : CollectionPreviewParameterProvider<StakingBlockState>(
private class StakingBlockStateProvider : CollectionPreviewParameterProvider<StakingAvailable>(
collection = listOf(
StakingBlockState.Content(
StakingAvailable.Content(
iconState = iconState,
interestRate = "10",
periodInDays = 4,
tokenSymbol = "SOL",
onStakeClicked = {},
),
StakingBlockState.Loading(iconState = iconState),
StakingBlockState.Error(iconState = iconState),
StakingAvailable.Loading(iconState = iconState),
StakingAvailable.Error(iconState = iconState),
),
)

View file

@ -4,6 +4,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
@Suppress("TooManyFunctions")
interface TokenDetailsClickIntents {
@ -59,4 +60,6 @@ interface TokenDetailsClickIntents {
fun onAssociateClick()
fun onStakeBannerClick()
fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig)
}

View file

@ -63,6 +63,7 @@ import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRout
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsCurrencyStatusAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.analytics.TokenDetailsNotificationsAnalyticsSender
import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenBalanceSegmentedButtonConfig
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsStateFactory
import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig
@ -125,12 +126,12 @@ internal class TokenDetailsViewModel @Inject constructor(
) : ViewModel(), DefaultLifecycleObserver, TokenDetailsClickIntents {
private val userWalletId: UserWalletId = savedStateHandle.get<Bundle>(AppRoute.CurrencyDetails.USER_WALLET_ID_KEY)
?.let { it.unbundle(UserWalletId.serializer()) }
?.unbundle(UserWalletId.serializer())
?: error("This screen can't open without `UserWalletId`")
private val cryptoCurrency: CryptoCurrency =
savedStateHandle.get<Bundle>(AppRoute.CurrencyDetails.CRYPTO_CURRENCY_KEY)
?.let { it.unbundle(CryptoCurrency.serializer()) }
?.unbundle(CryptoCurrency.serializer())
?: error("This screen can't open without `CryptoCurrency`")
private val userWallet: UserWallet
@ -154,6 +155,7 @@ internal class TokenDetailsViewModel @Inject constructor(
symbol = cryptoCurrency.symbol,
decimals = cryptoCurrency.decimals,
featureToggles = tokenDetailsFeatureToggles,
isStakingEnabled = stakingFeatureToggles.isStakingEnabled,
)
private val exchangeStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {
@ -787,6 +789,10 @@ internal class TokenDetailsViewModel @Inject constructor(
}
}
override fun onBalanceSelect(config: TokenBalanceSegmentedButtonConfig) {
uiState = stateFactory.getStateWithUpdatedBalanceSegmentedButtonConfig(config)
}
private fun handleUnavailabilityReason(unavailabilityReason: ScenarioUnavailabilityReason): Boolean {
if (unavailabilityReason == ScenarioUnavailabilityReason.None) return false

View file

@ -9,6 +9,7 @@ import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.visa.model.VisaTxHistoryItem
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.viewmodels.intents.VisaWalletIntents
import com.tangem.utils.Strings
import com.tangem.utils.converter.Converter
import org.joda.time.DateTimeZone
@ -20,7 +21,7 @@ internal class VisaTxHistoryItemStateConverter(
override fun convert(value: VisaTxHistoryItem): TransactionState {
val localDate = value.date.withZone(DateTimeZone.getDefault())
val time = DateTimeFormatters.formatDate(localDate, DateTimeFormatters.timeFormatter)
val subtitle = "$time ${value.status.capitalize()}"
val subtitle = "$time ${Strings.DOT} ${value.status.capitalize()}"
return TransactionState.Content(
txHash = value.id,