From f916d7394b4e478f878e3d70847346b28b5c41ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 1 Jul 2024 16:39:42 +0500 Subject: [PATCH] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 5 + core/res/src/main/res/values/strings.xml | 13 ++ .../core/ui/components/BoxWithGradient.kt | 41 ---- .../components/containers}/FooterContainer.kt | 4 +- .../ui/components/inputrow/InputRowDefault.kt | 11 +- .../components/inputrow/InputRowImageBase.kt | 7 +- .../inputrow/InputRowImageChevron.kt | 39 +++- .../components/inputrow/InputRowImageInfo.kt | 114 ++++++++++ .../core/ui/extensions/MarkdownExtension.kt | 4 +- .../data/staking/DefaultStakingRepository.kt | 21 +- .../converters/YieldBalanceConverter.kt | 1 + .../domain/staking/model/YieldBalance.kt | 1 + .../impl/presentation/ui/fee/SendCustomFee.kt | 2 +- .../ui/recipient/SendRecipientContent.kt | 2 +- .../ui/recipient/TextFieldWithPaste.kt | 2 +- .../state/InnerYieldBalanceState.kt | 30 +++ .../impl/presentation/state/StakingUiState.kt | 1 + .../previewdata/InitialStakingStatePreview.kt | 59 +++++ .../SetInitialDataStateTransformer.kt | 110 ++++++++- .../ui/StakingInitialInfoContent.kt | 214 ++++++++++++------ .../ui/StakingNavigationButtons.kt | 31 ++- 21 files changed, 559 insertions(+), 153 deletions(-) delete mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/BoxWithGradient.kt rename {features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/ui/common => core/ui/src/main/java/com/tangem/core/ui/components/containers}/FooterContainer.kt (92%) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/inputrow/InputRowImageInfo.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/InnerYieldBalanceState.kt create mode 100644 features/staking/impl/src/main/java/com/tangem/features/staking/impl/presentation/state/previewdata/InitialStakingStatePreview.kt 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