Updated on 2026-08-14

This commit is contained in:
Tangem 2024-07-01 16:39:42 +05:00
parent 2d8d9453bd
commit f916d7394b
21 changed files with 559 additions and 153 deletions

View file

@ -111,6 +111,7 @@
<string name="common_next">Далее</string>
<string name="common_no">Нет</string>
<string name="common_no_address">Нет адреса</string>
<string name="common_now">Сейчас</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Основная карта</string>
<string name="common_passphrase">Парольная фраза</string>
@ -139,6 +140,7 @@
<string name="common_support">Поддержка</string>
<string name="common_swap">Обмен</string>
<string name="common_terms_and_conditions">условия участия</string>
<string name="common_today">Сегодня</string>
<string name="common_transaction_failed">Ошибка транзакции</string>
<string name="common_transactions">Транзакции</string>
<string name="common_transfer">Перевод</string>
@ -309,6 +311,8 @@
<string name="manage_tokens_unavailable_vote">Голосовать</string>
<string name="manage_tokens_wallet_selector_title">Выберите кошелек</string>
<string name="manage_tokens_wallet_support_only_one_network_title">Кошелёк не поддерживает более одной сети</string>
<string name="markets_token_details_links">Ссылки</string>
<string name="markets_token_details_metrics">Метрики</string>
<string name="onboarding_access_code_feature_1_description">Вам необходимо установить единый код доступа для защиты всех ваших карт</string>
<string name="onboarding_access_code_feature_1_title">Защита</string>
<string name="onboarding_access_code_feature_2_description">Позже вы сможете установить индивидуальный код доступа для каждой карты</string>
@ -681,6 +685,7 @@
<string name="wallet_connect_sessions_title">Сессии WalletConnect</string>
<string name="wallet_connect_subtitle">Подключение к dApps</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_connect_toast_awaiting_session_proposal">Подключение может занять несколько секунд</string>
<string name="wallet_marketplace_block_title">Рыночная цена %s</string>
<string name="wallet_marketprice_block_update_time">за 24 часа</string>
<string name="wallet_network_group_title">Сеть %s</string>

View file

@ -112,6 +112,7 @@
<string name="common_next">Next</string>
<string name="common_no">No</string>
<string name="common_no_address">No address</string>
<string name="common_now">Now</string>
<string name="common_ok">OK</string>
<string name="common_origin_card">Primary Card</string>
<string name="common_passphrase">Passphrase</string>
@ -142,6 +143,7 @@
<string name="common_support">Support</string>
<string name="common_swap">Swap</string>
<string name="common_terms_and_conditions">terms and conditions</string>
<string name="common_today">Today</string>
<string name="common_transaction_failed">Transaction failed</string>
<string name="common_transactions">Transactions</string>
<string name="common_transfer">Transfer</string>
@ -319,6 +321,11 @@
<string name="markets_common_title">Market</string>
<string name="markets_select_wallet">Select wallet</string>
<string name="markets_sort_by_title">Sort By</string>
<string name="markets_token_details_insights">Insights</string>
<string name="markets_token_details_links">Links</string>
<string name="markets_token_details_metrics">Metrics</string>
<string name="markets_token_details_price_performance">Price performance</string>
<string name="markets_token_details_security_score">Security score</string>
<string name="onboarding_access_code_feature_1_description">You have to set up a single access code to protect all your cards</string>
<string name="onboarding_access_code_feature_1_title">Protect</string>
<string name="onboarding_access_code_feature_2_description">You can set up an individual access code on each card later</string>
@ -552,6 +559,8 @@
<string name="settings_forget_wallet">Forget wallet</string>
<string name="settings_forget_wallet_footer">This will remove the wallet from the application. The wallet itself can be added again.</string>
<string name="settings_wallet_name_title">Name</string>
<string name="staking_active">Active</string>
<string name="staking_active_footer">To unstake your assets, click here.</string>
<string name="staking_details_apr">APR</string>
<string name="staking_details_apy">APY</string>
<string name="staking_details_apy_info">The annual percentage return you can earn from participating in staking.</string>
@ -577,6 +586,9 @@
<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>
<string name="staking_stake_more">Stake more</string>
<string name="staking_unstaked">Unstacked</string>
<string name="staking_unstaked_footer">Check unstaked to claim your assets</string>
<string name="staking_validator">Validator</string>
<string name="story_awe_description">Store your crypto assets secure while keeping private keys contained in your card</string>
<string name="story_awe_title">Revolutionary Hardware Wallet</string>
@ -702,6 +714,7 @@
<string name="wallet_connect_sessions_title">WalletConnect Sessions</string>
<string name="wallet_connect_subtitle">Connect to dApps</string>
<string name="wallet_connect_title">WalletConnect</string>
<string name="wallet_connect_toast_awaiting_session_proposal">Connecting may take a few seconds</string>
<string name="wallet_marketplace_block_title">%s Market Price</string>
<string name="wallet_marketprice_block_update_time">last 24h</string>
<string name="wallet_network_group_title">%s network</string>

View file

@ -1,41 +0,0 @@
package com.tangem.core.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
@Composable
fun BoxWithGradient(
modifier: Modifier = Modifier,
gradient: Brush = BottomGradient,
content: @Composable BoxScope.() -> Unit,
) {
val bottomInsetsPx = WindowInsets.navigationBars.getBottom(LocalDensity.current)
Box(modifier = modifier.fillMaxSize()) {
content()
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.height(TangemTheme.dimens.size164 + bottomInsetsPx.dp)
.background(gradient),
)
}
}
private val BottomGradient: Brush = Brush.verticalGradient(
colors = listOf(
TangemColorPalette.Black.copy(alpha = 0f),
TangemColorPalette.Black.copy(alpha = 0.75f),
TangemColorPalette.Black.copy(alpha = 0.95f),
TangemColorPalette.Black,
),
)

View file

@ -1,4 +1,4 @@
package com.tangem.features.send.impl.presentation.ui.common
package com.tangem.core.ui.components.containers
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
@ -18,7 +18,7 @@ import com.tangem.core.ui.res.TangemTheme
* @param content field content
*/
@Composable
internal fun FooterContainer(
fun FooterContainer(
modifier: Modifier = Modifier,
footer: String? = null,
footerTopPadding: Dp = TangemTheme.dimens.spacing8,

View file

@ -4,12 +4,16 @@ 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.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
@ -20,8 +24,8 @@ import com.tangem.core.ui.R
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
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.core.ui.res.TangemThemePreview
/**
* [InputRowDefault](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=86eKp9izWxUvmoCq-4)
@ -64,7 +68,7 @@ fun InputRowDefault(
) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = titleColor,
)
Text(
@ -80,6 +84,7 @@ fun InputRowDefault(
contentDescription = null,
tint = iconTint,
modifier = Modifier
.align(CenterVertically)
.padding(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,

View file

@ -24,6 +24,7 @@ internal fun InputRowImageBase(
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = modifier,
) {
InputRowAsyncImage(
@ -32,11 +33,7 @@ internal fun InputRowImageBase(
.size(TangemTheme.dimens.spacing36)
.padding(vertical = TangemTheme.dimens.size1),
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = TangemTheme.dimens.spacing12),
) {
Column {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,

View file

@ -1,13 +1,20 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.material.Icon
import android.content.res.Configuration
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Input row component with selector
@ -37,10 +44,36 @@ fun InputRowImageChevron(
subtitleColor = subtitleColor,
captionColor = captionColor,
) {
SpacerWMax()
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun InputRowImageChevron_Preview() {
TangemThemePreview {
InputRowImageChevron(
subtitle = stringReference("Binance"),
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
withStyle(SpanStyle(TangemTheme.colors.text.accent)) {
stringReference("3,54%")
}
},
),
),
imageUrl = "",
)
}
}
// endregion

View file

@ -0,0 +1,114 @@
package com.tangem.core.ui.components.inputrow
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.atoms.text.EllipsisText
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Input row component with selector
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2841-1589&t=u6pOF6lsdpvWLELb-4)
*
* @param title title text
* @param subtitle subtitle text
* @param caption caption text
* @param infoTitle info title text
* @param infoSubtitle info subtitle text
* @param modifier modifier
* @param imageUrl icon to load
* @param subtitleColor subtitle text color
* @param captionColor caption text color
*/
@Suppress("LongParameterList")
@Composable
fun InputRowImageInfo(
title: TextReference?,
subtitle: TextReference,
caption: TextReference,
infoTitle: TextReference,
infoSubtitle: TextReference,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing6),
modifier = modifier
.padding(TangemTheme.dimens.spacing12),
) {
if (title != null) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
}
InputRowImageBase(
subtitle = subtitle,
caption = caption,
imageUrl = imageUrl,
subtitleColor = subtitleColor,
captionColor = captionColor,
) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing2),
horizontalAlignment = Alignment.End,
modifier = Modifier.weight(1f),
) {
EllipsisText(
text = infoTitle.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
EllipsisText(
text = infoSubtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun InputRowImageInfo_Preview() {
TangemThemePreview {
InputRowImageInfo(
title = stringReference("Active"),
subtitle = stringReference("Binance"),
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
withStyle(SpanStyle(TangemTheme.colors.text.accent)) {
stringReference("3,54%")
}
},
),
),
infoTitle = stringReference("5431231231231231231231232 USD"),
infoSubtitle = stringReference("5 SOL"),
imageUrl = "",
)
}
}
// endregion

View file

@ -53,4 +53,6 @@ fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode):
}
}
return this
}
}
fun AnnotatedString.Builder.appendSpace() = append(" ")

View file

@ -3,9 +3,8 @@ package com.tangem.data.staking
import arrow.core.raise.catch
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchainsdk.utils.toCoinId
import com.tangem.data.staking.converters.StakingNetworkTypeConverter
import com.tangem.data.staking.converters.TokenConverter
import com.tangem.data.staking.converters.YieldConverter
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.converters.*
import com.tangem.data.staking.converters.action.ActionStatusConverter
import com.tangem.data.staking.converters.action.EnterActionResponseConverter
import com.tangem.data.staking.converters.action.StakingActionTypeConverter
@ -18,21 +17,15 @@ import com.tangem.datasource.api.stakekit.StakeKitApi
import com.tangem.datasource.api.stakekit.models.request.Address
import com.tangem.datasource.api.stakekit.models.request.ConstructTransactionRequestBody
import com.tangem.datasource.api.stakekit.models.request.EnterActionRequestBody
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.Token
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.model.action.EnterAction
import com.tangem.domain.staking.model.transaction.StakingTransaction
import com.tangem.data.common.cache.CacheRegistry
import com.tangem.data.staking.converters.*
import com.tangem.datasource.api.stakekit.models.request.YieldBalanceRequestBody
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.token.StakingBalanceStore
import com.tangem.datasource.local.token.StakingYieldsStore
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.action.EnterAction
import com.tangem.domain.staking.model.transaction.StakingTransaction
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
@ -40,10 +33,6 @@ import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.toFormattedString
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.cancellable
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

View file

@ -21,6 +21,7 @@ internal class YieldBalanceConverter : Converter<YieldBalanceConverter.Data, Yie
amount = item.amount,
pricePerShare = item.pricePerShare,
rawCurrencyId = item.tokenDTO.coinGeckoId,
validatorAddress = item.validatorAddress,
)
},
integrationId = value.integrationId,

View file

@ -35,6 +35,7 @@ data class BalanceItem(
val amount: BigDecimal,
val pricePerShare: BigDecimal,
val rawCurrencyId: String?,
val validatorAddress: String?,
)
enum class BalanceType {

View file

@ -13,7 +13,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.state.fee.FeeType
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import com.tangem.core.ui.components.containers.FooterContainer
import kotlinx.collections.immutable.ImmutableList
@Composable

View file

@ -32,7 +32,7 @@ import com.tangem.features.send.impl.presentation.state.SendStates
import com.tangem.features.send.impl.presentation.state.fields.SendTextField
import com.tangem.features.send.impl.presentation.state.previewdata.RecipientStatePreviewData
import com.tangem.features.send.impl.presentation.state.previewdata.SendClickIntentsStub
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.features.send.impl.presentation.viewmodel.SendClickIntents
import com.tangem.utils.Strings.STARS
import kotlinx.collections.immutable.ImmutableList

View file

@ -17,7 +17,7 @@ import com.tangem.core.ui.components.inputrow.inner.PasteButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.send.impl.presentation.ui.common.FooterContainer
import com.tangem.core.ui.components.containers.FooterContainer
@Composable
internal fun TextFieldWithPaste(

View file

@ -0,0 +1,30 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.BalanceType
import com.tangem.domain.staking.model.Yield
sealed class InnerYieldBalanceState {
data class Data(
val rewardsCrypto: String,
val rewardsFiat: String,
val isRewardsToClaim: Boolean,
val balance: List<BalanceGroupedState>,
) : InnerYieldBalanceState()
data object Empty : InnerYieldBalanceState()
}
data class BalanceGroupedState(
val items: List<BalanceState>,
val type: BalanceType,
val footer: TextReference?,
val title: TextReference,
)
data class BalanceState(
val validator: Yield.Validator,
val cryptoAmount: TextReference,
val fiatAmount: TextReference,
val rawCurrencyId: String?,
)

View file

@ -53,6 +53,7 @@ internal sealed class StakingStates {
val warmupPeriod: String,
val rewardSchedule: String,
val onInfoClick: (InfoType) -> Unit,
val yieldBalance: InnerYieldBalanceState,
) : InitialInfoState()
data class Empty(

View file

@ -0,0 +1,59 @@
package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.staking.model.BalanceType
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
internal object InitialStakingStatePreview {
val defaultState = StakingStates.InitialInfoState.Data(
isPrimaryButtonEnabled = true,
available = "15 SOL",
onStake = "0 SOL",
aprRange = stringReference("2.54-5.12%"),
unbondingPeriod = "3d",
minimumRequirement = "12 SOL",
rewardClaiming = "Auto",
warmupPeriod = "Days",
rewardSchedule = "Block",
onInfoClick = {},
yieldBalance = InnerYieldBalanceState.Empty,
)
val stateWithYield = defaultState.copy(
yieldBalance = InnerYieldBalanceState.Data(
rewardsFiat = "100 $",
rewardsCrypto = "100 SOL",
isRewardsToClaim = false,
balance = listOf(
BalanceGroupedState(
type = BalanceType.STAKED,
title = stringReference("Staked"),
footer = null,
items = listOf(
BalanceState(
cryptoAmount = stringReference("100 SOL"),
fiatAmount = stringReference("100 $"),
rawCurrencyId = null,
validator = Yield.Validator(
address = "address",
status = "status",
name = "Binance",
image = null,
website = null,
apr = "5".toBigDecimal(),
commission = null,
stakedBalance = null,
votingPower = null,
preferred = false,
),
),
),
),
),
),
)
}

View file

@ -9,17 +9,19 @@ 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.BalanceType
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.model.YieldBalance
import com.tangem.domain.staking.model.YieldBalanceItem
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.isNullOrZero
import com.tangem.utils.transformer.Transformer
import java.math.BigDecimal
@ -55,15 +57,20 @@ internal class SetInitialDataStateTransformer(
private fun createInitialInfoState(): StakingStates.InitialInfoState.Data {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
return StakingStates.InitialInfoState.Data(
isPrimaryButtonEnabled = true,
available = BigDecimalFormatter.formatCryptoAmount(
cryptoCurrencyStatus.value.amount,
cryptoAmount = cryptoCurrencyStatus.value.amount,
cryptoCurrency = cryptoCurrencyStatus.currency.symbol,
decimals = cryptoCurrencyStatus.currency.decimals,
),
onStake = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(),
cryptoCurrency = cryptoCurrencyStatus.currency.symbol,
decimals = cryptoCurrencyStatus.currency.decimals,
),
onStake = "0 SOL", // TODO staking add after adding /balances request
aprRange = getAprRange(),
unbondingPeriod = yield.metadata.cooldownPeriod.days.toString(),
minimumRequirement = yield.metadata.minimumStake.toString(),
@ -71,6 +78,7 @@ internal class SetInitialDataStateTransformer(
warmupPeriod = yield.metadata.warmupPeriod.days.toString(),
rewardSchedule = yield.metadata.rewardSchedule,
onInfoClick = clickIntents::onInfoClick,
yieldBalance = getStakedBalances(cryptoCurrencyStatus),
)
}
@ -108,6 +116,96 @@ internal class SetInitialDataStateTransformer(
return resourceReference(R.string.common_range, wrappedList(formattedMinApr, formattedMaxApr))
}
private fun getStakedBalances(cryptoCurrencyStatus: CryptoCurrencyStatus): InnerYieldBalanceState {
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
return if (yieldBalance is YieldBalance.Data) {
val appCurrency = appCurrencyProvider()
val cryptoCurrency = cryptoCurrencyStatus.currency
val cryptoRewardsValue = yieldBalance.getRewardStakingBalance()
val fiatRewardsValue = cryptoCurrencyStatus.value.fiatRate?.times(cryptoRewardsValue)
val groupedBalances = getGroupedBalance(yieldBalance.balance, cryptoCurrencyStatus, appCurrency)
InnerYieldBalanceState.Data(
rewardsCrypto = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoRewardsValue,
cryptoCurrency = cryptoCurrency,
),
rewardsFiat = BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatRewardsValue,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
isRewardsToClaim = !cryptoRewardsValue.isNullOrZero(),
balance = groupedBalances,
)
} else {
InnerYieldBalanceState.Empty
}
}
private fun getGroupedBalance(
balance: YieldBalanceItem,
cryptoCurrencyStatus: CryptoCurrencyStatus,
appCurrency: AppCurrency,
) = balance.items
.sortedByDescending { it.type }
.groupBy { it.type }
.mapNotNull { item ->
val (title, footer) = getValidatorBalanceInto(item.key)
val balances = item.value.mapNotNull { balance ->
val validator = yield.validators.firstOrNull {
balance.validatorAddress?.contains(it.address, ignoreCase = true) == true
}
val cryptoAmount = balance.amount * balance.pricePerShare
val fiatAmount = cryptoCurrencyStatus.value.fiatRate?.times(cryptoAmount)
validator?.let {
BalanceState(
validator = validator,
cryptoAmount = stringReference(
BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = balance.amount,
cryptoCurrency = cryptoCurrencyStatus.currency,
),
),
fiatAmount = stringReference(
BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatAmount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
),
rawCurrencyId = balance.rawCurrencyId,
)
}
}
title?.let {
BalanceGroupedState(
items = balances,
type = item.key,
footer = footer,
title = it,
)
}
}
private fun getValidatorBalanceInto(type: BalanceType) = when (type) {
BalanceType.PREPARING,
BalanceType.STAKED,
-> resourceReference(R.string.staking_active) to resourceReference(R.string.staking_active_footer)
BalanceType.UNSTAKING,
BalanceType.UNLOCKING,
BalanceType.UNSTAKED,
-> resourceReference(R.string.staking_unstaked) to resourceReference(R.string.staking_unstaked_footer)
BalanceType.AVAILABLE,
BalanceType.LOCKED,
BalanceType.UNKNOWN,
BalanceType.REWARDS,
-> null to null
}
companion object {
private val EQUALITY_THRESHOLD = BigDecimal(1E-10)
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
@ -11,36 +13,64 @@ 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.text.buildAnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.containers.FooterContainer
import com.tangem.core.ui.components.inputrow.InputRowDefault
import com.tangem.core.ui.components.inputrow.InputRowImageInfo
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.components.rows.RoundableCornersRow
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceGroupedState
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.utils.Strings.DOT
import com.tangem.utils.extensions.orZero
@Composable
internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState) {
if (state !is StakingStates.InitialInfoState.Data) return
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
modifier = Modifier // Do not put fillMaxSize() in here
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing16)
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
)
.verticalScroll(rememberScrollState()),
) {
MetricsBlock(state)
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(state.yieldBalance == InnerYieldBalanceState.Empty) {
MetricsBlock(state)
}
StakingDetailsRows(state)
Spacer(modifier = Modifier.height(8.dp))
AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") {
if (it is InnerYieldBalanceState.Data) {
StakingRewardBlock(
rewardCrypto = it.rewardsCrypto,
rewardFiat = it.rewardsFiat,
isRewardsToClaim = it.isRewardsToClaim,
)
}
}
AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") {
if (it is InnerYieldBalanceState.Data) {
ActiveStakingBlock(it.balance)
}
}
}
}
@ -52,7 +82,7 @@ private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) {
color = TangemTheme.colors.background.primary,
shape = RoundedCornerShape(TangemTheme.dimens.radius12),
)
.padding(16.dp)
.padding(TangemTheme.dimens.spacing12)
.fillMaxWidth(),
) {
Text(
@ -60,7 +90,7 @@ private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) {
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(TangemTheme.dimens.size8))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@ -108,53 +138,117 @@ private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) {
@Composable
internal fun StakingDetailsRows(state: StakingStates.InitialInfoState.Data) {
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_available),
endText = state.available,
cornersToRound = CornersToRound.TOP_2,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_on_stake),
endText = state.onStake,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_apy),
endText = state.aprRange.resolveReference(),
cornersToRound = CornersToRound.ZERO,
iconClick = { state.onInfoClick(InfoType.APY) },
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_unbonding_period),
endText = state.unbondingPeriod,
cornersToRound = CornersToRound.ZERO,
iconClick = { state.onInfoClick(InfoType.UNBOUNDING_PERIOD) },
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_minimum_requirement),
endText = state.minimumRequirement,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_reward_claiming),
endText = state.rewardClaiming,
cornersToRound = CornersToRound.ZERO,
iconClick = { state.onInfoClick(InfoType.REWARD_CLAIMING) },
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_warmup_period),
endText = state.warmupPeriod,
cornersToRound = CornersToRound.ZERO,
iconClick = { state.onInfoClick(InfoType.WARMUP_PERIOD) },
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_reward_schedule),
endText = state.rewardSchedule,
cornersToRound = CornersToRound.BOTTOM_2,
iconClick = { state.onInfoClick(InfoType.REWARD_SCHEDULE) },
Column {
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_available),
endText = state.available,
cornersToRound = CornersToRound.TOP_2,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_on_stake),
endText = state.onStake,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_apy),
endText = state.aprRange.resolveReference(),
cornersToRound = CornersToRound.ZERO,
iconClick = { state.onInfoClick(InfoType.APY) },
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_unbonding_period),
endText = state.unbondingPeriod,
cornersToRound = CornersToRound.ZERO,
iconClick = { state.onInfoClick(InfoType.UNBOUNDING_PERIOD) },
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_minimum_requirement),
endText = state.minimumRequirement,
cornersToRound = CornersToRound.ZERO,
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_reward_claiming),
endText = state.rewardClaiming,
cornersToRound = CornersToRound.ZERO,
iconClick = { state.onInfoClick(InfoType.REWARD_CLAIMING) },
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_warmup_period),
endText = state.warmupPeriod,
cornersToRound = CornersToRound.ZERO,
iconClick = { state.onInfoClick(InfoType.WARMUP_PERIOD) },
)
InitialInfoContentRow(
startText = stringResource(id = R.string.staking_details_reward_schedule),
endText = state.rewardSchedule,
cornersToRound = CornersToRound.BOTTOM_2,
iconClick = { state.onInfoClick(InfoType.REWARD_SCHEDULE) },
)
}
}
@Composable
private fun StakingRewardBlock(rewardCrypto: String, rewardFiat: String, isRewardsToClaim: Boolean) {
val (text, textColor) = if (isRewardsToClaim) {
annotatedReference(
buildAnnotatedString {
append("+")
appendSpace()
append(rewardFiat)
appendSpace()
append(DOT)
appendSpace()
append(rewardCrypto)
},
) to TangemTheme.colors.text.primary1
} else {
resourceReference(R.string.staking_details_no_rewards_to_claim) to TangemTheme.colors.text.tertiary
}
InputRowDefault(
title = resourceReference(R.string.staking_rewards),
text = text,
iconRes = R.drawable.ic_chevron_right_24,
textColor = textColor,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
)
}
@Composable
private fun ActiveStakingBlock(groupes: List<BalanceGroupedState>) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
groupes.forEach { group ->
FooterContainer(
footer = group.footer?.resolveReference(),
modifier = Modifier,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action),
) {
group.items.forEachIndexed { index, balance ->
InputRowImageInfo(
title = group.title.takeIf { index == 0 },
subtitle = stringReference(balance.validator.name),
caption = stringReference(
BigDecimalFormatter.formatPercent(balance.validator.apr.orZero(), true),
),
infoTitle = balance.fiatAmount,
infoSubtitle = balance.cryptoAmount,
imageUrl = balance.validator.image.orEmpty(),
)
}
}
}
}
}
}
@Composable
private fun InitialInfoContentRow(
startText: String,
@ -191,18 +285,8 @@ private fun StakingInitialInfoContent_Preview(
private class StakingInitialInfoContentPreviewProvider : PreviewParameterProvider<StakingStates.InitialInfoState.Data> {
override val values: Sequence<StakingStates.InitialInfoState.Data>
get() = sequenceOf(
StakingStates.InitialInfoState.Data(
isPrimaryButtonEnabled = true,
available = "15 SOL",
onStake = "0 SOL",
aprRange = stringReference("2.54-5.12%"),
unbondingPeriod = "3d",
minimumRequirement = "12 SOL",
rewardClaiming = "Auto",
warmupPeriod = "Days",
rewardSchedule = "Block",
onInfoClick = {},
),
InitialStakingStatePreview.defaultState,
InitialStakingStatePreview.stateWithYield,
)
}
// endregion

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButton
import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.presentation.state.InnerYieldBalanceState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
@ -49,12 +50,9 @@ internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifie
@Composable
private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier = Modifier) {
val hapticFeedback = LocalHapticFeedback.current
val confirmState = uiState.confirmStakingState
val isSuccess = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isSuccess ?: false
val isStaking = (confirmState as? StakingStates.ConfirmStakingState.Data)?.isStaking ?: false
val isButtonsVisible = uiState.currentStep != StakingStep.Success
val isStakingState = uiState.currentStep == StakingStep.Success && !isSuccess && !isStaking
val isButtonsVisible = isPrevButtonVisible(uiState.currentStep)
val isStakingState = uiState.currentStep == StakingStep.Confirm
val (buttonTextId, buttonClick) = getButtonData(
currentState = uiState,
@ -103,15 +101,32 @@ private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier
private fun getButtonData(currentState: StakingUiState): Pair<Int, () -> Unit> {
return when (currentState.currentStep) {
StakingStep.InitialInfo,
StakingStep.InitialInfo -> {
val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
R.string.staking_stake_more to currentState.clickIntents::onNextClick
} else {
R.string.common_next to currentState.clickIntents::onNextClick
}
}
StakingStep.Amount,
StakingStep.Confirm,
StakingStep.Success,
-> R.string.common_next to currentState.clickIntents::onNextClick
StakingStep.Confirm -> R.string.common_stake to currentState.clickIntents::onNextClick
StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick
StakingStep.Success -> R.string.common_close to currentState.clickIntents::onBackClick
}
}
private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) {
StakingStep.InitialInfo,
StakingStep.Confirm,
StakingStep.Success,
-> false
StakingStep.Amount,
StakingStep.Validators,
-> true
}
private fun isButtonEnabled(uiState: StakingUiState): Boolean {
return when (uiState.currentStep) {
StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled