diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index a0be0aea2a..053247f58d 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -111,6 +111,7 @@
Далее
Нет
Нет адреса
+ Сейчас
OK
Основная карта
Парольная фраза
@@ -139,6 +140,7 @@
Поддержка
Обмен
условия участия
+ Сегодня
Ошибка транзакции
Транзакции
Перевод
@@ -309,6 +311,8 @@
Голосовать
Выберите кошелек
Кошелёк не поддерживает более одной сети
+ Ссылки
+ Метрики
Вам необходимо установить единый код доступа для защиты всех ваших карт
Защита
Позже вы сможете установить индивидуальный код доступа для каждой карты
@@ -681,6 +685,7 @@
Сессии WalletConnect
Подключение к dApps
WalletConnect
+ Подключение может занять несколько секунд
Рыночная цена %s
за 24 часа
Сеть %s
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index 497372f66e..e7228a19a8 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -112,6 +112,7 @@
Next
No
No address
+ Now
OK
Primary Card
Passphrase
@@ -142,6 +143,7 @@
Support
Swap
terms and conditions
+ Today
Transaction failed
Transactions
Transfer
@@ -319,6 +321,11 @@
Market
Select wallet
Sort By
+ Insights
+ Links
+ Metrics
+ Price performance
+ Security score
You have to set up a single access code to protect all your cards
Protect
You can set up an individual access code on each card later
@@ -552,6 +559,8 @@
Forget wallet
This will remove the wallet from the application. The wallet itself can be added again.
Name
+ Active
+ To unstake your assets, click here.
APR
APY
The annual percentage return you can earn from participating in staking.
@@ -577,6 +586,9 @@
Staking allow you to earn %1s. Your staking rewards arrive every ~%2s days.
Earn staking rewards
Rewards
+ Stake more
+ Unstacked
+ Check unstaked to claim your assets
Validator
Store your crypto assets secure while keeping private keys contained in your card
Revolutionary Hardware Wallet
@@ -702,6 +714,7 @@
WalletConnect Sessions
Connect to dApps
WalletConnect
+ Connecting may take a few seconds
%s Market Price
last 24h
%s network
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt b/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt
deleted file mode 100644
index b51d1c0f08..0000000000
--- a/core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt
+++ /dev/null
@@ -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,
- ),
-)
\ No newline at end of file
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt
similarity index 92%
rename from features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt
rename to core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt
index 26b442e62a..5296313d28 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common/FooterContainer.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/containers/FooterContainer.kt
@@ -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,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt
index 4222d238c0..3b1e657dda 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowDefault.kt
@@ -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,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt
index 51f7b29ac6..871dccfd94 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageBase.kt
@@ -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,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt
index 9c8fcff521..83fd0ccbf0 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageChevron.kt
@@ -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,
)
}
-}
\ No newline at end of file
+}
+
+// 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
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt
new file mode 100644
index 0000000000..81a7e01218
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt
@@ -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
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt
index 408e9130ee..b8b11c058f 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/MarkdownExtension.kt
@@ -53,4 +53,6 @@ fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode):
}
}
return this
-}
\ No newline at end of file
+}
+
+fun AnnotatedString.Builder.appendSpace() = append(" ")
\ No newline at end of file
diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt
index 52f0aef09c..edb32a111b 100644
--- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt
+++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt
@@ -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
diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt
index 0ab969880e..d0abdee20e 100644
--- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt
+++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt
@@ -21,6 +21,7 @@ internal class YieldBalanceConverter : Converter,
+ ) : InnerYieldBalanceState()
+
+ data object Empty : InnerYieldBalanceState()
+}
+
+data class BalanceGroupedState(
+ val items: List,
+ 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?,
+)
\ No newline at end of file
diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt
index e0112121e4..b1758a8d42 100644
--- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt
+++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/StakingUiState.kt
@@ -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(
diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt
new file mode 100644
index 0000000000..f49e7535d0
--- /dev/null
+++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt
@@ -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,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+}
\ No newline at end of file
diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt
index 6282c12227..0438982aff 100644
--- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt
+++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/transformers/SetInitialDataStateTransformer.kt
@@ -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)
}
diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt
index 558dc71308..81730a1913 100644
--- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt
+++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingInitialInfoContent.kt
@@ -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) {
+ 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 {
override val values: Sequence
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
\ No newline at end of file
diff --git a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt
index 025e254aab..e9c29e3cb1 100644
--- a/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt
+++ b/features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/ui/StakingNavigationButtons.kt
@@ -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 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