Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-19 11:12:21 +03:00
commit e2c7c9bd56
23 changed files with 694 additions and 86 deletions

View file

@ -0,0 +1,85 @@
package com.tangem.core.ui.components.atoms.radiobutton
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* [Radio button](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=101-119&t=FH84ljLBk1vmGAei-4)
*
* @param isSelected Whether the radio button is selected
* @param onClick Called when the user clicks the button
* @param modifier Modifier to be applied to the button
* @param isEnabled Whether the button click is enabled
*/
@Composable
fun TangemRadioButton(
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
Box(
modifier = modifier.clickable(
enabled = isEnabled,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false, radius = TangemTheme.dimens.size16),
onClick = onClick,
),
) {
val color = TangemTheme.colors.stroke.secondary
val radius = with(LocalDensity.current) { TangemTheme.dimens.size9.toPx() }
val width = with(LocalDensity.current) { TangemTheme.dimens.size2.toPx() }
Canvas(
modifier = Modifier
.size(TangemTheme.dimens.size24)
.padding(TangemTheme.dimens.spacing2),
) {
drawCircle(
color = color,
radius = radius,
style = Stroke(width),
)
}
AnimatedVisibility(
visible = isSelected,
label = "Radio button animation",
modifier = modifier
.size(TangemTheme.dimens.size24),
) {
Icon(
painter = painterResource(id = R.drawable.ic_check_circle_24),
contentDescription = null,
tint = TangemTheme.colors.control.checked,
)
}
}
}
// region Preview
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun TangemRadioButton_Preview() {
var isSelected by remember { mutableStateOf(false) }
TangemThemePreview {
TangemRadioButton(isSelected, onClick = { isSelected = !isSelected })
}
}
// endregion

View file

@ -5,7 +5,6 @@ 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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
@ -14,18 +13,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerWMax
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
import com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemThemePreview
@ -63,7 +59,7 @@ fun InputRowBestRate(
modifier = Modifier
.padding(TangemTheme.dimens.spacing12),
) {
InnerIcon(imageUrl = imageUrl)
InputRowAsyncImage(imageUrl = imageUrl, modifier = Modifier.size(TangemTheme.dimens.spacing40))
Column(
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing12),
@ -131,29 +127,6 @@ private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag:
}
}
@Composable
private fun InnerIcon(imageUrl: String) {
SubcomposeAsyncImage(
modifier = Modifier.size(TangemTheme.dimens.size40),
model = ImageRequest.Builder(context = LocalContext.current)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = { LoadingIcon() },
error = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,
shape = CircleShape,
),
)
},
contentDescription = null,
)
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)

View file

@ -0,0 +1,54 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.foundation.layout.*
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 com.tangem.core.ui.components.inputrow.inner.InputRowAsyncImage
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun InputRowImageBase(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
extraContent: @Composable RowScope.() -> Unit = {},
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
InputRowAsyncImage(
imageUrl = imageUrl,
modifier = Modifier
.size(TangemTheme.dimens.spacing36)
.padding(vertical = TangemTheme.dimens.size1),
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = TangemTheme.dimens.spacing12),
) {
Text(
text = subtitle.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = subtitleColor,
)
Text(
text = caption.resolveAnnotatedReference(),
style = TangemTheme.typography.caption2,
color = captionColor,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing2),
)
}
extraContent()
}
}

View file

@ -0,0 +1,46 @@
package com.tangem.core.ui.components.inputrow
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
/**
* Input row component with selector
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2100-842&t=hoBXmDX8NeLrp4p6-4)
*
* @param subtitle subtitle text
* @param caption caption text
* @param imageUrl icon to load
* @param modifier modifier
* @param subtitleColor subtitle text color
* @param captionColor caption text color
*/
@Composable
fun InputRowImageChevron(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
) {
InputRowImageBase(
subtitle = subtitle,
caption = caption,
imageUrl = imageUrl,
modifier = modifier,
subtitleColor = subtitleColor,
captionColor = captionColor,
) {
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,
)
}
}

View file

@ -0,0 +1,131 @@
package com.tangem.core.ui.components.inputrow
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.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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 androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.atoms.radiobutton.TangemRadioButton
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.BigDecimalFormatter
import java.math.BigDecimal
/**
* Input row component with selector
* [Input Row Image](https://www.figma.com/design/14ISV23YB1yVW1uNVwqrKv/Android?node-id=2772-905&t=FH84ljLBk1vmGAei-4)
*
* @param subtitle subtitle text
* @param caption caption text
* @param imageUrl icon to load
* @param onSelect callback when selected
* @param modifier modifier
* @param subtitleColor subtitle text color
* @param captionColor caption text color
* @param isSelected true if selected
*/
@Composable
fun InputRowImageSelector(
subtitle: TextReference,
caption: TextReference,
imageUrl: String,
onSelect: () -> Unit,
modifier: Modifier = Modifier,
subtitleColor: Color = TangemTheme.colors.text.primary1,
captionColor: Color = TangemTheme.colors.text.tertiary,
isSelected: Boolean = false,
) {
InputRowImageBase(
subtitle = subtitle,
caption = caption,
imageUrl = imageUrl,
subtitleColor = subtitleColor,
captionColor = captionColor,
modifier = modifier
.clickable(
onClick = onSelect,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
)
.padding(TangemTheme.dimens.spacing12),
) {
TangemRadioButton(isSelected = isSelected, isEnabled = false, onClick = onSelect)
}
}
//region preview
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun InputRowImageSelectorPreview(
@PreviewParameter(InputRowImageSelectorPreviewDataProvider::class) data: InputRowImageSelectorPreviewData,
) {
TangemThemePreview {
InputRowImageSelector(
modifier = Modifier.background(TangemTheme.colors.background.action),
subtitle = data.subtitle,
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) {
append(
BigDecimalFormatter.formatPercent(BigDecimal.ZERO, true),
)
}
},
),
),
imageUrl = "",
isSelected = false,
onSelect = {},
)
}
}
private data class InputRowImageSelectorPreviewData(
val subtitle: TextReference,
val caption: TextReference,
val showDivider: Boolean,
val actionIconRes: Int?,
val isSelected: Boolean,
)
private class InputRowImageSelectorPreviewDataProvider :
PreviewParameterProvider<InputRowImageSelectorPreviewData> {
override val values: Sequence<InputRowImageSelectorPreviewData>
get() = sequenceOf(
InputRowImageSelectorPreviewData(
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
actionIconRes = null,
showDivider = false,
isSelected = false,
),
InputRowImageSelectorPreviewData(
subtitle = TextReference.Str("subtitle"),
caption = TextReference.Str("caption"),
actionIconRes = R.drawable.ic_chevron_right_24,
showDivider = true,
isSelected = true,
),
)
}
//endregion

View file

@ -0,0 +1,41 @@
package com.tangem.core.ui.components.inputrow.inner
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
import com.tangem.core.ui.res.TangemTheme
/**
* Loads image by url for icon in the input row
*
* @param imageUrl url of the image
* @param modifier modifier
*/
@Composable
internal fun InputRowAsyncImage(imageUrl: String, modifier: Modifier = Modifier) {
SubcomposeAsyncImage(
modifier = modifier,
model = ImageRequest.Builder(context = LocalContext.current)
.data(imageUrl)
.crossfade(enable = true)
.allowHardware(enable = false)
.build(),
loading = { LoadingIcon() },
error = {
Box(
modifier = Modifier
.background(
color = TangemTheme.colors.background.tertiary,
shape = CircleShape,
),
)
},
contentDescription = null,
)
}

View file

@ -48,6 +48,13 @@ sealed interface TextReference {
*/
data class Str(val value: String) : TextReference
/**
* Annotated text
*
* @property value annotated string
*/
data class Annotated(val value: AnnotatedString) : TextReference
/**
* Combined reference. It concatenates all [refs].
*
@ -83,6 +90,16 @@ fun stringReference(value: String): TextReference {
return TextReference.Str(value)
}
/**
* Creates a [TextReference] using an annotated string value.
*
* @param value The annotated string value.
* @return A [TextReference] representing the provided annotated string value.
*/
fun annotatedReference(value: AnnotatedString): TextReference {
return TextReference.Annotated(value)
}
/**
* Creates a [TextReference] using a plural string resource ID with count and optional format arguments.
*
@ -133,6 +150,7 @@ fun TextReference.resolveReference(): String {
}
is TextReference.PluralRes -> pluralStringResource(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Annotated -> value.text
is TextReference.Combined -> {
buildString {
refs.forEach {
@ -155,6 +173,7 @@ fun TextReference.resolveReference(resources: Resources): String {
}
is TextReference.PluralRes -> resources.getQuantityString(id, count, *formatArgs.toTypedArray())
is TextReference.Str -> value
is TextReference.Annotated -> value.text
is TextReference.Combined -> {
buildString {
refs.forEach {
@ -180,9 +199,10 @@ fun TextReference.resolveAnnotatedReference(): AnnotatedString {
pluralStringResource(id, count, *formatArgs.toTypedArray()),
)
is TextReference.Str -> formatAnnotated(value)
is TextReference.Annotated -> value
is TextReference.Combined -> buildAnnotatedString {
refs.forEach {
append(formatAnnotated(it.resolveReference()))
append(it.resolveAnnotatedReference())
}
}
}
@ -195,6 +215,7 @@ operator fun TextReference.plus(ref: TextReference): TextReference {
is TextReference.PluralRes,
is TextReference.Res,
is TextReference.Str,
is TextReference.Annotated,
-> TextReference.Combined(refs = wrappedList(this, ref))
}
}

View file

@ -47,6 +47,7 @@ data class TangemDimens internal constructor(
val size5: Dp = 5.dp,
val size7: Dp = 7.dp,
val size8: Dp = 8.dp,
val size9: Dp = 9.dp,
val size10: Dp = 10.dp,
val size11: Dp = 11.dp,
val size12: Dp = 12.dp,

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="#1E1E1E" android:pathData="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2ZM10,17L5,12L6.41,10.59L10,14.17L17.59,6.58L19,8L10,17Z"/>
</vector>

View file

@ -30,7 +30,9 @@ internal class StakingStateRouter(
fun onNextClick() {
when (stateController.uiState.value.currentStep) {
StakingStep.InitialInfo -> showAmount()
StakingStep.Amount -> showConfirm()
StakingStep.Validators,
StakingStep.Amount,
-> showConfirm()
StakingStep.Confirm -> showSuccess()
StakingStep.Success -> onBackClick()
}
@ -51,6 +53,10 @@ internal class StakingStateRouter(
stateController.update { it.copy(currentStep = StakingStep.Amount) }
}
fun showValidators() {
stateController.update { it.copy(currentStep = StakingStep.Validators) }
}
fun showConfirm() {
stateController.update { it.copy(currentStep = StakingStep.Confirm) }
}

View file

@ -6,7 +6,6 @@ import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
@ -85,16 +84,12 @@ internal sealed class StakingStates {
val appCurrency: AppCurrency,
val isFeeApproximate: Boolean,
)
data class ValidatorState(
val validatorState: InnerValidatorState,
val availableValidators: List<Yield.Validator>,
)
}
enum class StakingStep {
InitialInfo,
Amount,
Validators,
Confirm,
Success,
}

View file

@ -4,13 +4,14 @@ import androidx.compose.runtime.Immutable
import com.tangem.domain.staking.model.Yield
@Immutable
internal sealed class InnerValidatorState {
internal sealed class ValidatorState {
data class Content(
val chosenValidator: Yield.Validator,
) : InnerValidatorState()
val availableValidators: List<Yield.Validator>,
) : ValidatorState()
data object Loading : InnerValidatorState()
data object Loading : ValidatorState()
data object Error : InnerValidatorState()
data object Error : ValidatorState()
}

View file

@ -7,9 +7,9 @@ import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.state.InnerFeeState
import com.tangem.features.staking.impl.presentation.state.InnerValidatorState
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
@ -73,10 +73,8 @@ internal object ConfirmStakingStatePreviewData {
isFeeApproximate = false,
isFeeConvertibleToFiat = true,
),
validatorState = StakingStates.ValidatorState(
validatorState = InnerValidatorState.Content(
chosenValidator = validatorList[0],
),
validatorState = ValidatorState.Content(
chosenValidator = validatorList[0],
availableValidators = validatorList,
),
footerText = "You stake \$715.11 and will be receiving ~\$35 monthly",

View file

@ -1,5 +1,6 @@
package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
object StakingClickIntentsStub : StakingClickIntents {
@ -19,4 +20,8 @@ object StakingClickIntentsStub : StakingClickIntents {
override fun onCurrencyChangeClick(isFiat: Boolean) {}
override fun onAmountNext() {}
override fun openValidators() {}
override fun onValidatorSelect(validator: Yield.Validator) {}
}

View file

@ -14,8 +14,10 @@ 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.StakingUiState
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.previewdata.ConfirmStakingStatePreviewData
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
@ -47,6 +49,7 @@ internal class SetInitialDataStateTransformer(
currentStep = StakingStep.InitialInfo,
initialInfoState = createInitialInfoState(),
amountState = createInitialAmountState(),
confirmStakingState = createInitialConfirmationState(),
)
}
@ -75,6 +78,15 @@ internal class SetInitialDataStateTransformer(
return amountStateConverter.convert("")
}
private fun createInitialConfirmationState(): StakingStates.ConfirmStakingState {
return ConfirmStakingStatePreviewData.confirmStakingState.copy(
validatorState = ValidatorState.Content(
chosenValidator = yield.validators.first(),
availableValidators = yield.validators,
),
)
}
private fun getAprRange(): TextReference {
val aprValues = yield.validators.mapNotNull { it.apr }

View file

@ -0,0 +1,24 @@
package com.tangem.features.staking.impl.presentation.state.transformers.validator
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.utils.transformer.Transformer
internal class ValidatorSelectChangeTransformer(
private val selectedValidator: Yield.Validator,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data ?: return prevState
val validatorState = confirmState.validatorState as? ValidatorState.Content ?: return prevState
return prevState.copy(
confirmStakingState = confirmState.copy(
validatorState = validatorState.copy(
chosenValidator = selectedValidator,
),
),
)
}
}

View file

@ -18,11 +18,18 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.ui.block.NotificationsBlock
import com.tangem.features.staking.impl.presentation.ui.block.StakingFeeBlock
import com.tangem.features.staking.impl.presentation.ui.block.ValidatorBlock
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
@Composable
internal fun StakingConfirmContent(amountState: AmountState, state: StakingStates.ConfirmStakingState) {
internal fun StakingConfirmContent(
amountState: AmountState,
state: StakingStates.ConfirmStakingState,
clickIntents: StakingClickIntents,
) {
if (state !is StakingStates.ConfirmStakingState.Data) return
Column(
@ -39,6 +46,7 @@ internal fun StakingConfirmContent(amountState: AmountState, state: StakingState
isEditingDisabled = true,
onClick = {},
)
ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators)
StakingFeeBlock(feeState = state.feeState)
NotificationsBlock(notifications = state.notifications)
SpacerHMax()
@ -66,6 +74,7 @@ private fun Preview_StakingConfirmContent() {
StakingConfirmContent(
amountState = AmountStatePreviewData.amountState,
state = ConfirmStakingStatePreviewData.confirmStakingState,
clickIntents = StakingClickIntentsStub,
)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.animation.*
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.shrinkHorizontally
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -9,7 +11,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -24,8 +26,8 @@ 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.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.features.staking.impl.presentation.state.StakingStep
import com.tangem.features.staking.impl.presentation.state.StakingUiState
@Composable
internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) {
@ -105,7 +107,8 @@ private fun getButtonData(currentState: StakingUiState): Pair<Int, () -> Unit> {
StakingStep.Amount,
StakingStep.Confirm,
StakingStep.Success,
-> R.string.common_next to { currentState.clickIntents.onNextClick() }
-> R.string.common_next to currentState.clickIntents::onNextClick
StakingStep.Validators -> R.string.common_continue to currentState.clickIntents::onNextClick
}
}
@ -113,6 +116,7 @@ private fun isButtonEnabled(uiState: StakingUiState): Boolean {
return when (uiState.currentStep) {
StakingStep.InitialInfo -> uiState.initialInfoState.isPrimaryButtonEnabled
StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled
StakingStep.Validators -> uiState.confirmStakingState.isPrimaryButtonEnabled
StakingStep.Confirm -> uiState.confirmStakingState.isPrimaryButtonEnabled
StakingStep.Success -> true
}

View file

@ -16,6 +16,7 @@ import com.tangem.common.ui.amountScreen.AmountScreenContent
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.res.TangemTheme
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 kotlinx.coroutines.delay
@ -50,13 +51,16 @@ internal fun StakingScreen(uiState: StakingUiState) {
@Composable
private fun SendAppBar(uiState: StakingUiState) {
val titleRes = when (uiState.currentStep) {
StakingStep.InitialInfo -> stringResource(id = R.string.common_stake)
StakingStep.Amount -> stringResource(id = R.string.send_amount_label)
StakingStep.Confirm -> stringResource(id = R.string.common_stake)
StakingStep.InitialInfo,
StakingStep.Validators,
StakingStep.Confirm,
-> stringResource(id = R.string.common_stake)
StakingStep.Success -> ""
}
val backIcon = when (uiState.currentStep) {
StakingStep.Amount,
StakingStep.Validators,
StakingStep.Confirm,
StakingStep.Success,
-> {
@ -130,7 +134,16 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
StakingStep.Confirm -> StakingConfirmContent(
amountState = uiState.amountState,
state = uiState.confirmStakingState,
clickIntents = uiState.clickIntents,
)
StakingStep.Validators -> {
val confirmState = uiState.confirmStakingState
if (confirmState !is StakingStates.ConfirmStakingState.Data) return@AnimatedContent
StakingValidatorListContent(
state = confirmState.validatorState,
clickIntents = uiState.clickIntents,
)
}
else -> TODO()
}
}

View file

@ -0,0 +1,129 @@
package com.tangem.features.staking.impl.presentation.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
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 androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.inputrow.InputRowImageSelector
import com.tangem.core.ui.components.rows.CornersToRound
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
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.ValidatorState
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmStakingStatePreviewData
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import java.math.BigDecimal
/**
* Staking screen with validators
*/
@Composable
internal fun StakingValidatorListContent(
state: ValidatorState,
clickIntents: StakingClickIntents,
modifier: Modifier = Modifier,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
LazyColumn(
contentPadding = PaddingValues(bottom = bottomBarHeight),
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
) {
item(key = "HEADER") {
Text(
text = stringResource(R.string.staking_validator),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier
.fillMaxWidth()
.clip(CornersToRound.TOP_2.getShape())
.background(TangemTheme.colors.background.action)
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing12,
top = TangemTheme.dimens.spacing12,
bottom = TangemTheme.dimens.spacing8,
),
)
}
if (state is ValidatorState.Content) {
val validators = state.availableValidators
items(
count = validators.size,
key = { validators[it].address },
contentType = { validators[it]::class.java },
) { index ->
val item = validators[index]
InputRowImageSelector(
subtitle = stringReference(item.name),
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
withStyle(style = SpanStyle(color = TangemTheme.colors.text.accent)) {
append(
BigDecimalFormatter.formatPercent(item.apr ?: BigDecimal.ZERO, true),
)
}
},
),
),
imageUrl = item.image.orEmpty(),
isSelected = item == state.chosenValidator,
onSelect = { clickIntents.onValidatorSelect(item) },
modifier = Modifier
.clip(
if (index == validators.lastIndex) {
CornersToRound.BOTTOM_2
} else {
CornersToRound.ZERO
}.getShape(),
)
.background(TangemTheme.colors.background.action),
)
}
}
}
}
// region Preview
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun StakingValidatorListContent_Preview(
@PreviewParameter(StakingValidatorListContentPreviewProvider::class)
data: ValidatorState,
) {
TangemThemePreview {
StakingValidatorListContent(
state = data,
clickIntents = StakingClickIntentsStub,
)
}
}
private class StakingValidatorListContentPreviewProvider : PreviewParameterProvider<ValidatorState> {
override val values: Sequence<ValidatorState>
get() = sequenceOf(ConfirmStakingStatePreviewData.confirmStakingState.validatorState)
}
// endregion

View file

@ -0,0 +1,69 @@
package com.tangem.features.staking.impl.presentation.ui.block
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import com.tangem.core.ui.components.inputrow.InputRowImageChevron
import com.tangem.core.ui.extensions.annotatedReference
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import java.math.BigDecimal
@Composable
internal fun ValidatorBlock(validatorState: ValidatorState, onClick: () -> Unit) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
onClick = onClick,
)
.padding(TangemTheme.dimens.spacing12),
) {
Text(
text = stringResource(R.string.staking_validator),
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing6),
)
if (validatorState is ValidatorState.Content) {
InputRowImageChevron(
subtitle = stringReference(validatorState.chosenValidator.name),
caption = combinedReference(
resourceReference(R.string.staking_details_apr),
annotatedReference(
buildAnnotatedString {
append(" ")
withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) {
val apr = validatorState.chosenValidator.apr ?: BigDecimal.ZERO
append(BigDecimalFormatter.formatPercent(apr, true))
}
},
),
),
imageUrl = validatorState.chosenValidator.image.orEmpty(),
)
}
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.viewmodel
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.domain.staking.model.Yield
internal interface StakingClickIntents : AmountScreenClickIntents {
@ -11,4 +12,8 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onPrevClick()
override fun onAmountNext() = onNextClick()
fun openValidators()
fun onValidatorSelect(validator: Yield.Validator)
}

View file

@ -15,22 +15,20 @@ import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.staking.impl.navigation.InnerStakingRouter
import com.tangem.features.staking.impl.presentation.state.StakingStateController
import com.tangem.features.staking.impl.presentation.state.StakingStateRouter
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.transformers.HideBalanceStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.SetConfirmStateDataStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.SetInitialDataStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountCurrencyChangeStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountMaxValueStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountPasteDismissStateTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.lifecycle.HiltViewModel
@ -48,7 +46,6 @@ internal class StakingViewModel @Inject constructor(
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -58,18 +55,17 @@ internal class StakingViewModel @Inject constructor(
var stakingStateRouter: StakingStateRouter by Delegates.notNull()
private set
private val cryptoCurrencyId: CryptoCurrency.ID = savedStateHandle.get<Bundle>(
AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY,
)
?.let { it.unbundle(CryptoCurrency.ID.serializer()) }
?: error("This screen can't be opened without `CryptoCurrency.ID`")
private val cryptoCurrencyId: CryptoCurrency.ID =
savedStateHandle.get<Bundle>(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY)
?.unbundle(CryptoCurrency.ID.serializer())
?: error("This screen can't be opened without `CryptoCurrency.ID`")
private val userWalletId: UserWalletId = savedStateHandle.get<Bundle>(AppRoute.Staking.USER_WALLET_ID_KEY)
?.let { it.unbundle(UserWalletId.serializer()) }
?.unbundle(UserWalletId.serializer())
?: error("This screen can't be opened without `UserWalletId`")
private val yield: Yield = savedStateHandle.get<Bundle>(AppRoute.Staking.YIELD_KEY)
?.let { it.unbundle(Yield.serializer()) }
?.unbundle(Yield.serializer())
?: error("This screen can't be opened without `Yield`")
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
@ -90,27 +86,6 @@ internal class StakingViewModel @Inject constructor(
override fun onNextClick() {
stakingStateRouter.onNextClick()
when (value.currentStep) {
StakingStep.Confirm -> {
stateController.update(
SetConfirmStateDataStateTransformer(
yield = yield,
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
isFeeApproximateUseCase = isFeeApproximateUseCase,
),
)
}
StakingStep.InitialInfo -> {
// TODO staking
}
StakingStep.Amount -> {
// TODO staking
}
StakingStep.Success -> {
// TODO staking
}
}
}
override fun onPrevClick() {
@ -133,6 +108,12 @@ internal class StakingViewModel @Inject constructor(
stateController.update(AmountCurrencyChangeStateTransformer(cryptoCurrencyStatus, isFiat))
}
override fun openValidators() = stakingStateRouter.showValidators()
override fun onValidatorSelect(validator: Yield.Validator) {
stateController.update(ValidatorSelectChangeTransformer(validator))
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
innerRouter = router
this.stakingStateRouter = stateRouter