Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-07 15:08:26 +03:00
commit 2a5e3fa7eb
220 changed files with 5932 additions and 1815 deletions

View file

@ -1,9 +1,11 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import java.math.BigDecimal
@Immutable
sealed class FeeState {
data class Content(

View file

@ -1,6 +1,8 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import kotlinx.collections.immutable.ImmutableList
@ -11,19 +13,23 @@ sealed class InnerYieldBalanceState {
val rewardsCrypto: String,
val rewardsFiat: String,
val isRewardsToClaim: Boolean,
val balance: List<BalanceGroupedState>,
val balance: ImmutableList<BalanceGroupedState>,
) : InnerYieldBalanceState()
data object Empty : InnerYieldBalanceState()
}
// TODO staking get rid of unstable types
@Immutable
data class BalanceGroupedState(
val items: ImmutableList<BalanceState>,
val footer: TextReference?,
val title: TextReference,
val type: BalanceGroupType,
val type: BalanceType,
val isClickable: Boolean,
)
@Immutable
data class BalanceState(
val validator: Yield.Validator,
val cryptoValue: String,
@ -33,10 +39,4 @@ data class BalanceState(
val rawCurrencyId: String?,
val unbondingPeriod: TextReference,
val pendingActions: ImmutableList<PendingAction>,
)
enum class BalanceGroupType {
ACTIVE,
UNSTAKED,
UNKNOWN,
}
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.features.staking.impl.R
@ -23,7 +24,15 @@ internal sealed class StakingNotification(val config: NotificationConfig) {
onCloseClick = onCloseClick,
),
) {
// TODO staking
data class StakedPositionNotFoundError(val message: String) : Error(
title = stringReference(message),
subtitle = stringReference(message),
)
data class Common(val subtitle: TextReference) : Error(
title = resourceReference(R.string.common_error),
subtitle = subtitle,
)
}
sealed class Warning(

View file

@ -1,8 +1,12 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.event.consumedEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.state.transformers.SetButtonsStateTransformer
import com.tangem.utils.transformer.Transformer
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -20,20 +24,26 @@ internal class StakingStateController @Inject constructor() {
val uiState: StateFlow<StakingUiState> get() = mutableUiState.asStateFlow()
private val buttonsTransformer = SetButtonsStateTransformer()
fun update(function: (StakingUiState) -> StakingUiState) {
mutableUiState.update(function = function)
mutableUiState.update(function = buttonsTransformer::transform)
}
fun update(transformer: Transformer<StakingUiState>) {
mutableUiState.update(function = transformer::transform)
mutableUiState.update(function = buttonsTransformer::transform)
}
fun clear() {
mutableUiState.update { getInitialState() }
mutableUiState.update(function = buttonsTransformer::transform)
}
private fun getInitialState(): StakingUiState {
return StakingUiState(
title = TextReference.EMPTY,
clickIntents = StakingClickIntentsStub,
cryptoCurrencyName = "",
currentStep = StakingStep.InitialInfo,
@ -44,7 +54,8 @@ internal class StakingStateController @Inject constructor() {
isBalanceHidden = false,
event = consumedEvent(),
bottomSheetConfig = null,
routeType = RouteType.STAKE,
actionType = StakingActionCommonType.ENTER,
buttonsState = NavigationButtonsState.Empty,
)
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.common.routing.AppRouter
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
internal class StakingStateRouter(
private val appRouter: AppRouter,
@ -14,12 +15,12 @@ internal class StakingStateRouter(
fun onNextClick() {
when (stateController.value.currentStep) {
StakingStep.InitialInfo -> when (stateController.value.routeType) {
RouteType.STAKE -> showAmount()
RouteType.OTHER,
RouteType.UNSTAKE,
StakingStep.InitialInfo -> when (stateController.value.actionType) {
StakingActionCommonType.ENTER -> showAmount()
StakingActionCommonType.PENDING_OTHER,
StakingActionCommonType.EXIT,
-> showConfirmation()
RouteType.CLAIM -> showRewardsValidators()
StakingActionCommonType.PENDING_REWARDS -> showRewardsValidators()
}
StakingStep.RewardsValidators,
StakingStep.Validators,
@ -32,10 +33,17 @@ internal class StakingStateRouter(
}
fun onPrevClick() {
when (stateController.uiState.value.currentStep) {
val uiState = stateController.uiState.value
when (uiState.currentStep) {
StakingStep.InitialInfo -> onBackClick()
StakingStep.Amount -> showInitial()
StakingStep.Confirmation -> showAmount()
StakingStep.Confirmation -> {
if (uiState.actionType != StakingActionCommonType.ENTER) {
showInitial()
} else {
showAmount()
}
}
StakingStep.Validators -> showConfirmation()
StakingStep.RewardsValidators -> showInitial()
}

View file

@ -2,11 +2,13 @@ package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
@ -16,6 +18,7 @@ import kotlinx.collections.immutable.ImmutableList
*/
@Immutable
internal data class StakingUiState(
val title: TextReference,
val clickIntents: StakingClickIntents,
val cryptoCurrencyName: String,
val currentStep: StakingStep,
@ -25,7 +28,8 @@ internal data class StakingUiState(
val confirmationState: StakingStates.ConfirmationState,
val isBalanceHidden: Boolean,
val bottomSheetConfig: TangemBottomSheetConfig?,
val routeType: RouteType,
val actionType: StakingActionCommonType,
val buttonsState: NavigationButtonsState,
val event: StateEvent<StakingEvent>,
) {
@ -55,17 +59,6 @@ internal sealed class StakingStates {
val isStakeMoreAvailable: Boolean,
) : InitialInfoState()
data class InitialInfoItems(
val available: String,
val onStake: String,
val aprRange: TextReference,
val unbondingPeriod: String,
val minimumRequirement: String,
val rewardClaiming: String,
val warmupPeriod: String,
val rewardSchedule: String,
)
data class Empty(
override val isPrimaryButtonEnabled: Boolean = false,
) : InitialInfoState()
@ -94,6 +87,7 @@ internal sealed class StakingStates {
val notifications: ImmutableList<StakingNotification>,
val footerText: String,
val transactionDoneState: TransactionDoneState,
val pendingActionInProgress: PendingAction? = null,
) : ConfirmationState()
data class Empty(
@ -108,11 +102,4 @@ enum class StakingStep {
Amount,
Confirmation,
Validators,
}
enum class RouteType {
STAKE,
UNSTAKE,
CLAIM,
OTHER,
}

View file

@ -10,7 +10,6 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.BalanceGroupType
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
@ -59,15 +58,18 @@ internal class YieldBalancesConverter(
.groupBy { it.type.toGroup() }
.mapNotNull { item ->
val (title, footer) = getGroupTitle(item.key)
val isClickable = getClickableType(item.key)
title?.let {
BalanceGroupedState(
items = item.value.mapBalances().toPersistentList(),
footer = footer,
title = it,
type = item.key,
isClickable = isClickable,
)
}
}
.toPersistentList()
private fun List<BalanceItem>.mapBalances(): List<BalanceState> {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
@ -111,31 +113,37 @@ internal class YieldBalancesConverter(
}
private fun BalanceType.toGroup() = when (this) {
BalanceType.PREPARING,
BalanceType.STAKED,
BalanceType.REWARDS,
BalanceType.AVAILABLE,
BalanceType.LOCKED,
-> BalanceGroupType.ACTIVE
BalanceType.UNSTAKING,
BalanceType.UNLOCKING,
BalanceType.UNSTAKED,
-> BalanceGroupType.UNSTAKED
BalanceType.UNKNOWN,
-> BalanceGroupType.UNKNOWN
-> BalanceType.UNKNOWN
else -> this
}
private fun getGroupTitle(type: BalanceGroupType) = when (type) {
BalanceGroupType.ACTIVE -> resourceReference(
R.string.staking_active,
) to resourceReference(
R.string.staking_active_footer,
)
BalanceGroupType.UNSTAKED -> resourceReference(
R.string.staking_unstaked,
) to resourceReference(
R.string.staking_unstaked_footer,
)
BalanceGroupType.UNKNOWN -> null to null
private fun getGroupTitle(type: BalanceType) = when (type) {
BalanceType.STAKED -> resourceReference(R.string.staking_active) to
resourceReference(R.string.staking_active_footer)
BalanceType.UNSTAKED -> resourceReference(R.string.staking_unstaked) to
resourceReference(R.string.staking_unstaked_footer)
BalanceType.UNSTAKING -> resourceReference(R.string.staking_unstaking) to null
BalanceType.AVAILABLE -> null to null
BalanceType.PREPARING -> null to null
BalanceType.REWARDS -> null to null
BalanceType.LOCKED -> null to null
BalanceType.UNLOCKING -> null to null
BalanceType.UNKNOWN -> null to null
}
private fun getClickableType(type: BalanceType) = when (type) {
BalanceType.STAKED,
BalanceType.UNSTAKED,
-> true
BalanceType.AVAILABLE,
BalanceType.UNSTAKING,
BalanceType.PREPARING,
BalanceType.REWARDS,
BalanceType.LOCKED,
BalanceType.UNLOCKING,
BalanceType.UNKNOWN,
-> false
}
}

View file

@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.staking.model.stakekit.BalanceType
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.*
@ -59,11 +60,12 @@ internal object InitialStakingStatePreview {
rewardsFiat = "100 $",
rewardsCrypto = "100 SOL",
isRewardsToClaim = false,
balance = listOf(
balance = persistentListOf(
BalanceGroupedState(
title = stringReference("Staked"),
footer = null,
type = BalanceGroupType.ACTIVE,
type = BalanceType.STAKED,
isClickable = true,
items = persistentListOf(
BalanceState(
cryptoValue = "100",

View file

@ -2,6 +2,7 @@ package com.tangem.features.staking.impl.presentation.state.stub
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
@ -11,7 +12,9 @@ object StakingClickIntentsStub : StakingClickIntents {
override fun onBackClick() {}
override fun onNextClick(pendingActions: ImmutableList<PendingAction>) {}
override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList<PendingAction>) {}
override fun onActionClick(pendingAction: PendingAction?) {}
override fun onPrevClick() {}
@ -33,8 +36,6 @@ object StakingClickIntentsStub : StakingClickIntents {
override fun openRewardsValidators() {}
override fun selectRewardValidator(rewardValue: String) {}
override fun onExploreClick() {}
override fun onShareClick() {}

View file

@ -0,0 +1,36 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal class AddStakingErrorTransformer(
private val error: StakingError,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmationState =
prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
return prevState.copy(
confirmationState = confirmationState.copy(
notifications = (confirmationState.notifications + convertToNotification(error)).toPersistentList(),
feeState = FeeState.Error,
),
)
}
private fun convertToNotification(error: StakingError): StakingNotification {
return when (error) {
is StakingError.StakedPositionNotFoundError -> StakingNotification.Error.StakedPositionNotFoundError(
message = error.toString(),
)
// TODO staking
else -> StakingNotification.Error.Common(
subtitle = stringReference(error.toString()),
)
}
}
}

View file

@ -0,0 +1,232 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.common.ui.navigationButtons.NavigationButton
import com.tangem.common.ui.navigationButtons.NavigationButtonsState
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal class SetButtonsStateTransformer : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val buttonsState = if (prevState.isButtonsVisible()) {
NavigationButtonsState.Data(
primaryButton = getPrimaryButton(prevState),
prevButton = getPrevButton(prevState),
secondaryButton = getSecondaryButton(prevState),
extraButtons = getExtraButtons(prevState),
txUrl = (confirmState?.transactionDoneState as? TransactionDoneState.Content)?.txUrl,
)
} else {
NavigationButtonsState.Empty
}
return prevState.copy(buttonsState = buttonsState)
}
private fun getPrimaryButton(prevState: StakingUiState): NavigationButton {
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val innerConfirmState = confirmState?.innerState
val isPrimaryInProgress =
confirmState?.pendingActions?.getPrimaryAction() == confirmState?.pendingActionInProgress
val isConfirmation = prevState.currentStep == StakingStep.Confirmation
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
val isIconVisible = isConfirmation && !isCompleted
val isShowProgress = isInProgress && isPrimaryInProgress
return NavigationButton(
textReference = prevState.getButtonText(),
iconRes = R.drawable.ic_tangem_24,
isSecondary = false,
isIconVisible = isIconVisible,
showProgress = isShowProgress,
isEnabled = prevState.isButtonEnabled(),
onClick = { prevState.onPrimaryClick() },
)
}
private fun getSecondaryButton(prevState: StakingUiState): NavigationButton? {
val confirmState = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val innerConfirmState = confirmState?.innerState
val isConfirmation = prevState.currentStep == StakingStep.Confirmation
val isInProgress = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
val isCompleted = innerConfirmState == InnerConfirmationStakingState.COMPLETED
return confirmState?.pendingActions?.getSecondaryAction()?.let { pendingAction ->
val isSecondaryInProgress = pendingAction == confirmState.pendingActionInProgress
val isShowProgress = isInProgress && isSecondaryInProgress
NavigationButton(
textReference = getPendingActionTitle(pendingAction.type),
iconRes = R.drawable.ic_tangem_24,
isSecondary = true,
isIconVisible = true,
showProgress = isShowProgress,
isEnabled = prevState.isButtonEnabled(),
onClick = { prevState.clickIntents.onActionClick(pendingAction) },
).takeIf { isConfirmation && !isCompleted }
}
}
private fun getPrevButton(prevState: StakingUiState): NavigationButton? {
return NavigationButton(
textReference = TextReference.EMPTY,
iconRes = R.drawable.ic_back_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = prevState.clickIntents::onPrevClick,
).takeIf { prevState.currentStep.isPrevButtonVisible() }
}
private fun getExtraButtons(prevState: StakingUiState): ImmutableList<NavigationButton> {
return persistentListOf(
NavigationButton(
textReference = resourceReference(R.string.common_explore),
iconRes = R.drawable.ic_web_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = prevState.clickIntents::onExploreClick,
),
NavigationButton(
textReference = resourceReference(R.string.common_share),
iconRes = R.drawable.ic_share_24,
isSecondary = true,
isIconVisible = true,
showProgress = false,
isEnabled = true,
onClick = prevState.clickIntents::onShareClick,
),
)
}
private fun List<PendingAction>.getPrimaryAction(): PendingAction? = getOrNull(0)
private fun List<PendingAction>.getSecondaryAction(): PendingAction? = getOrNull(1)
private fun StakingUiState.isButtonsVisible(): Boolean = when (currentStep) {
StakingStep.InitialInfo -> isStakeMoreAvailable()
StakingStep.RewardsValidators -> false
else -> true
}
private fun StakingUiState.getButtonText(): TextReference {
return when (currentStep) {
StakingStep.InitialInfo -> {
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
resourceReference(R.string.staking_stake_more)
} else {
resourceReference(R.string.common_next)
}
}
StakingStep.Confirmation -> {
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
resourceReference(R.string.common_close)
} else {
when (actionType) {
StakingActionCommonType.ENTER -> resourceReference(R.string.common_stake)
StakingActionCommonType.EXIT -> resourceReference(R.string.common_unstake)
StakingActionCommonType.PENDING_OTHER,
StakingActionCommonType.PENDING_REWARDS,
-> getPendingActionTitle(confirmationState.pendingActions.firstOrNull()?.type)
}
}
} else {
resourceReference(R.string.common_close)
}
}
StakingStep.Validators -> resourceReference(R.string.common_continue)
StakingStep.Amount,
StakingStep.RewardsValidators,
-> resourceReference(R.string.common_next)
}
}
private fun StakingUiState.onPrimaryClick() {
when (currentStep) {
StakingStep.InitialInfo -> {
val actionType = StakingActionCommonType.ENTER.takeIf { isStakeMoreAvailable() }
clickIntents.onAmountValueChange("") // reset amount state
clickIntents.onNextClick(actionType)
}
StakingStep.Validators,
StakingStep.Amount,
-> clickIntents.onNextClick()
StakingStep.Confirmation -> {
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
clickIntents.onBackClick()
} else {
clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull())
}
} else {
clickIntents.onBackClick()
}
}
StakingStep.RewardsValidators -> Unit
}
}
private fun StakingStep.isPrevButtonVisible(): Boolean = when (this) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.Confirmation,
StakingStep.Validators,
-> false
StakingStep.Amount,
-> true
}
private fun StakingUiState.isButtonEnabled(): Boolean {
return when (currentStep) {
StakingStep.InitialInfo -> initialInfoState.isPrimaryButtonEnabled
StakingStep.Amount -> amountState.isPrimaryButtonEnabled
StakingStep.Confirmation -> confirmationState.isPrimaryButtonEnabled
StakingStep.RewardsValidators -> rewardsValidatorsState.isPrimaryButtonEnabled
StakingStep.Validators -> true
}
}
@Suppress("CyclomaticComplexMethod")
private fun getPendingActionTitle(type: StakingActionType?): TextReference = when (type) {
StakingActionType.CLAIM_REWARDS -> resourceReference(R.string.common_claim_rewards)
StakingActionType.RESTAKE_REWARDS -> resourceReference(R.string.staking_restake_rewards)
StakingActionType.WITHDRAW -> resourceReference(R.string.staking_withdraw)
StakingActionType.RESTAKE -> resourceReference(R.string.staking_restake)
StakingActionType.CLAIM_UNSTAKED -> resourceReference(R.string.staking_claim_unstaked)
StakingActionType.UNLOCK_LOCKED -> resourceReference(R.string.staking_unlocked_locked)
StakingActionType.STAKE_LOCKED -> resourceReference(R.string.staking_stake_locked)
StakingActionType.VOTE -> resourceReference(R.string.staking_vote)
StakingActionType.REVOKE -> resourceReference(R.string.staking_revoke)
StakingActionType.VOTE_LOCKED -> resourceReference(R.string.staking_vote_locked)
StakingActionType.REVOTE -> resourceReference(R.string.staking_revote)
StakingActionType.REBOND -> resourceReference(R.string.staking_rebond)
StakingActionType.MIGRATE -> resourceReference(R.string.staking_migrate)
StakingActionType.STAKE -> resourceReference(R.string.common_stake)
StakingActionType.UNSTAKE -> resourceReference(R.string.common_unstake)
StakingActionType.UNKNOWN -> TextReference.EMPTY
null -> TextReference.EMPTY
}
private fun StakingUiState.isStakeMoreAvailable(): Boolean {
val initialState = initialInfoState as? StakingStates.InitialInfoState.Data
return initialState?.isStakeMoreAvailable == true || initialState?.yieldBalance is InnerYieldBalanceState.Empty
}
}

View file

@ -1,11 +1,14 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateInProgressTransformer : Transformer<StakingUiState> {
internal class SetConfirmationStateInProgressTransformer(
private val pendingAction: PendingAction?,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
@ -19,6 +22,7 @@ internal class SetConfirmationStateInProgressTransformer : Transformer<StakingUi
isPrimaryButtonEnabled = false,
innerState = InnerConfirmationStakingState.IN_PROGRESS,
validatorState = validatorState.copySealed(isClickable = false),
pendingActionInProgress = pendingAction,
)
} else {
this

View file

@ -33,6 +33,7 @@ internal class SetConfirmationStateLoadingTransformer(
footerText = "",
transactionDoneState = TransactionDoneState.Empty,
pendingActions = persistentListOf(),
pendingActionInProgress = null,
),
)
}

View file

@ -5,14 +5,10 @@ import com.tangem.common.ui.amountScreen.converters.AmountStateConverter
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.currency.icon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.list.RoundedListWithDividersItemData
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.features.staking.impl.R
@ -25,10 +21,9 @@ import com.tangem.features.staking.impl.presentation.state.converters.YieldBalan
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.Provider
import com.tangem.utils.extensions.orZero
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import java.math.BigDecimal
internal class SetInitialDataStateTransformer(
@ -62,6 +57,10 @@ internal class SetInitialDataStateTransformer(
override fun transform(prevState: StakingUiState): StakingUiState {
return prevState.copy(
title = TextReference.Res(
R.string.staking_initial_info_title,
wrappedList(cryptoCurrencyStatusProvider().currency.name),
),
clickIntents = clickIntents,
currentStep = StakingStep.InitialInfo,
initialInfoState = createInitialInfoState(),
@ -87,70 +86,100 @@ internal class SetInitialDataStateTransformer(
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
val yieldBalance = cryptoCurrencyStatus.value.yieldBalance
return persistentListOf(
RoundedListWithDividersItemData(
id = R.string.staking_details_available,
startText = TextReference.Res(R.string.staking_details_available),
endText = TextReference.Str(
value = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoCurrencyStatus.value.amount,
cryptoCurrency = cryptoCurrencyStatus.currency.symbol,
decimals = cryptoCurrencyStatus.currency.decimals,
),
return listOfNotNull(
createAvailableItem(cryptoCurrencyStatus),
createApyItem(),
createUnbondingPeriodItem(),
createMinimumRequirementItem(cryptoCurrencyStatus),
createRewardClaimingItem(),
createWarmupPeriodItem(),
createRewardScheduleItem(),
).toPersistentList()
}
private fun createAvailableItem(cryptoCurrencyStatus: CryptoCurrencyStatus): RoundedListWithDividersItemData {
return RoundedListWithDividersItemData(
id = R.string.staking_details_available,
startText = TextReference.Res(R.string.staking_details_available),
endText = TextReference.Str(
value = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = cryptoCurrencyStatus.value.amount,
cryptoCurrency = cryptoCurrencyStatus.currency.symbol,
decimals = cryptoCurrencyStatus.currency.decimals,
),
),
RoundedListWithDividersItemData(
id = R.string.staking_details_annual_percentage_rate,
startText = TextReference.Res(R.string.staking_details_annual_percentage_rate),
endText = getAprRange(),
iconClick = { clickIntents.onInfoClick(InfoType.APY) },
),
RoundedListWithDividersItemData(
id = 0, // todo remove in merge
startText = TextReference.Res(0), // todo remove in merge
endText = TextReference.Str(
value = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = (yieldBalance as? YieldBalance.Data)?.getTotalStakingBalance().orZero(),
cryptoCurrency = cryptoCurrencyStatus.currency.symbol,
decimals = cryptoCurrencyStatus.currency.decimals,
),
),
),
RoundedListWithDividersItemData(
id = R.string.staking_details_unbonding_period,
startText = TextReference.Res(R.string.staking_details_unbonding_period),
endText = TextReference.Str(yield.metadata.cooldownPeriod.days.toString()),
iconClick = { clickIntents.onInfoClick(InfoType.UNBOUNDING_PERIOD) },
)
}
private fun createApyItem(): RoundedListWithDividersItemData {
return RoundedListWithDividersItemData(
id = R.string.staking_details_annual_percentage_rate,
startText = TextReference.Res(R.string.staking_details_annual_percentage_rate),
endText = getAprRange(),
iconClick = { clickIntents.onInfoClick(InfoType.APY) },
)
}
private fun createUnbondingPeriodItem(): RoundedListWithDividersItemData {
return RoundedListWithDividersItemData(
id = R.string.staking_details_unbonding_period,
startText = TextReference.Res(R.string.staking_details_unbonding_period),
endText = pluralReference(
id = R.plurals.common_days,
count = yield.metadata.cooldownPeriod.days,
formatArgs = wrappedList(yield.metadata.cooldownPeriod.days),
),
iconClick = { clickIntents.onInfoClick(InfoType.UNBONDING_PERIOD) },
)
}
private fun createMinimumRequirementItem(
cryptoCurrencyStatus: CryptoCurrencyStatus,
): RoundedListWithDividersItemData? {
val minimumCryptoAmount = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]?.minimum
return minimumCryptoAmount?.let {
RoundedListWithDividersItemData(
id = R.string.staking_details_minimum_requirement,
startText = TextReference.Res(R.string.staking_details_minimum_requirement),
endText = TextReference.Str(
value = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = yield.args.enter.args[KEY_AMOUNT]?.minimum?.toBigDecimal(),
cryptoAmount = it,
cryptoCurrency = cryptoCurrencyStatus.currency.symbol,
decimals = cryptoCurrencyStatus.currency.decimals,
),
),
)
}
}
private fun createRewardClaimingItem(): RoundedListWithDividersItemData {
return RoundedListWithDividersItemData(
id = R.string.staking_details_reward_claiming,
startText = TextReference.Res(R.string.staking_details_reward_claiming),
endText = TextReference.Str(yield.metadata.rewardClaiming),
iconClick = { clickIntents.onInfoClick(InfoType.REWARD_CLAIMING) },
)
}
private fun createWarmupPeriodItem(): RoundedListWithDividersItemData {
return RoundedListWithDividersItemData(
id = R.string.staking_details_warmup_period,
startText = TextReference.Res(R.string.staking_details_warmup_period),
endText = pluralReference(
id = R.plurals.common_days,
count = yield.metadata.warmupPeriod.days,
formatArgs = wrappedList(yield.metadata.warmupPeriod.days),
),
RoundedListWithDividersItemData(
id = R.string.staking_details_reward_claiming,
startText = TextReference.Res(R.string.staking_details_reward_claiming),
endText = TextReference.Str(yield.metadata.rewardClaiming),
iconClick = { clickIntents.onInfoClick(InfoType.REWARD_CLAIMING) },
),
RoundedListWithDividersItemData(
id = R.string.staking_details_warmup_period,
startText = TextReference.Res(R.string.staking_details_warmup_period),
endText = TextReference.Str(yield.metadata.warmupPeriod.days.toString()),
iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) },
),
RoundedListWithDividersItemData(
id = R.string.staking_details_reward_schedule,
startText = TextReference.Res(R.string.staking_details_reward_schedule),
endText = TextReference.Str(yield.metadata.rewardSchedule),
iconClick = { clickIntents.onInfoClick(InfoType.REWARD_SCHEDULE) },
),
iconClick = { clickIntents.onInfoClick(InfoType.WARMUP_PERIOD) },
)
}
private fun createRewardScheduleItem(): RoundedListWithDividersItemData {
return RoundedListWithDividersItemData(
id = R.string.staking_details_reward_schedule,
startText = TextReference.Res(R.string.staking_details_reward_schedule),
endText = TextReference.Str(yield.metadata.rewardSchedule),
iconClick = { clickIntents.onInfoClick(InfoType.REWARD_SCHEDULE) },
)
}
@ -191,6 +220,5 @@ internal class SetInitialDataStateTransformer(
companion object {
private val EQUALITY_THRESHOLD = BigDecimal(1E-10)
private const val KEY_AMOUNT = "amount"
}
}

View file

@ -22,7 +22,7 @@ internal class ShowInfoBottomSheetStateTransformer(
title = resourceReference(R.string.staking_details_annual_percentage_rate),
text = resourceReference(R.string.staking_details_annual_percentage_rate_info),
)
InfoType.UNBOUNDING_PERIOD -> StakingInfoBottomSheetConfig(
InfoType.UNBONDING_PERIOD -> StakingInfoBottomSheetConfig(
title = resourceReference(R.string.staking_details_unbonding_period),
text = resourceReference(R.string.staking_details_unbonding_period_info),
)
@ -46,7 +46,7 @@ internal class ShowInfoBottomSheetStateTransformer(
enum class InfoType {
APY,
UNBOUNDING_PERIOD,
UNBONDING_PERIOD,
REWARD_CLAIMING,
WARMUP_PERIOD,
REWARD_SCHEDULE,

View file

@ -1,18 +1,31 @@
package com.tangem.features.staking.impl.presentation.state.transformers.amount
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldChangeTransformer
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class AmountChangeStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yield: Yield,
private val value: String,
) : Transformer<StakingUiState> {
private val amountRequirementStateTransformer = AmountRequirementStateTransformer(
cryptoCurrencyStatus,
yield,
value,
)
override fun transform(prevState: StakingUiState): StakingUiState {
val updatedAmountState = AmountFieldChangeTransformer(
cryptoCurrencyStatus,
value,
).transform(prevState.amountState)
return prevState.copy(
amountState = AmountFieldChangeTransformer(cryptoCurrencyStatus, value).transform(prevState.amountState),
amountState = amountRequirementStateTransformer.transform(updatedAmountState),
)
}
}

View file

@ -1,16 +1,29 @@
package com.tangem.features.staking.impl.presentation.state.transformers.amount
import com.tangem.common.ui.amountScreen.converters.field.AmountFieldMaxAmountTransformer
import com.tangem.core.ui.utils.parseBigDecimal
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class AmountMaxValueStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yield: Yield,
) : Transformer<StakingUiState> {
private val amountRequirementStateTransformer = AmountRequirementStateTransformer(
cryptoCurrencyStatus = cryptoCurrencyStatus,
yield = yield,
value = cryptoCurrencyStatus.value.amount
?.parseBigDecimal(cryptoCurrencyStatus.currency.decimals)
.orEmpty(),
)
override fun transform(prevState: StakingUiState): StakingUiState {
val updatedAmountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState)
return prevState.copy(
amountState = AmountFieldMaxAmountTransformer(cryptoCurrencyStatus).transform(prevState.amountState),
amountState = amountRequirementStateTransformer.transform(updatedAmountState),
)
}
}

View file

@ -0,0 +1,65 @@
package com.tangem.features.staking.impl.presentation.state.transformers.amount
import com.tangem.common.extensions.isZero
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.utils.parseToBigDecimal
import com.tangem.domain.staking.model.stakekit.AddressArgument
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
import com.tangem.utils.transformer.Transformer
internal class AmountRequirementStateTransformer(
private val cryptoCurrencyStatus: CryptoCurrencyStatus,
private val yield: Yield,
private val value: String,
) : Transformer<AmountState> {
override fun transform(prevState: AmountState): AmountState {
val amountRequirements = yield.args.enter.args[Yield.Args.ArgType.AMOUNT]
return if (prevState !is AmountState.Data || amountRequirements == null) {
prevState
} else {
updateWithError(prevState, amountRequirements)
}
}
private fun updateWithError(prevState: AmountState.Data, amountRequirements: AddressArgument): AmountState {
val isRequirementError = isRequirementError(prevState, amountRequirements)
return if (isRequirementError) {
prevState.copy(
amountTextField = prevState.amountTextField.copy(
isError = true,
error = resourceReference(
R.string.staking_amount_requirement_error,
wrappedList(
BigDecimalFormatter.formatCryptoAmount(
amountRequirements.minimum,
cryptoCurrencyStatus.currency.symbol,
cryptoCurrencyStatus.currency.decimals,
),
),
),
),
)
} else {
prevState
}
}
private fun isRequirementError(prevState: AmountState.Data, amountRequirements: AddressArgument): Boolean {
val amountDecimal = value.parseToBigDecimal(cryptoCurrencyStatus.currency.decimals)
val isAlreadyErrorState = prevState.amountTextField.isError
val isAmountRequired = amountRequirements.required
val isAmountZero = amountDecimal.isZero()
val isExceedsRequirements =
amountRequirements.maximum?.compareTo(amountDecimal) == -1 ||
amountRequirements.minimum?.compareTo(amountDecimal) == 1
return !isAmountZero && isAmountRequired && isExceedsRequirements && !isAlreadyErrorState
}
}

View file

@ -63,7 +63,7 @@ internal fun StakingClaimRewardsValidatorContent(
.background(TangemTheme.colors.background.action)
.clickable(
onClick = {
clickIntents.selectRewardValidator(item.cryptoValue)
clickIntents.onActiveStake(item)
},
),
)

View file

@ -18,8 +18,8 @@ import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.transactions.TransactionDoneTitle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.RouteType
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.TransactionDoneState
import com.tangem.features.staking.impl.presentation.state.previewdata.ConfirmationStatePreviewData
@ -34,15 +34,14 @@ internal fun StakingConfirmationContent(
amountState: AmountState,
state: StakingStates.ConfirmationState,
clickIntents: StakingClickIntents,
type: RouteType,
type: StakingActionCommonType,
) {
if (state !is StakingStates.ConfirmationState.Data) return
Column(
modifier = Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.tertiary)
.padding(TangemTheme.dimens.spacing16)
.padding(horizontal = TangemTheme.dimens.spacing16)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
) {
@ -62,7 +61,7 @@ internal fun StakingConfirmationContent(
isEditingDisabled = true,
onClick = {},
)
if (type == RouteType.STAKE) {
if (type == StakingActionCommonType.ENTER) {
ValidatorBlock(validatorState = state.validatorState, onClick = clickIntents::openValidators)
}
StakingFeeBlock(feeState = state.feeState)
@ -93,7 +92,7 @@ private fun Preview_StakingConfirmationContent() {
amountState = AmountStatePreviewData.amountState,
state = ConfirmationStatePreviewData.assentStakingState,
clickIntents = StakingClickIntentsStub,
type = RouteType.STAKE,
type = StakingActionCommonType.ENTER,
)
}
}

View file

@ -1,12 +1,12 @@
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.ExperimentalFoundationApi
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.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
@ -22,59 +22,87 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.components.SpacerH12
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.list.RoundedListWithDividers
import com.tangem.core.ui.components.list.roundedListWithDividersItems
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.domain.staking.model.stakekit.BalanceType
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.*
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
import com.tangem.features.staking.impl.presentation.state.previewdata.InitialStakingStatePreview
import com.tangem.features.staking.impl.presentation.state.stub.StakingClickIntentsStub
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import com.tangem.utils.StringsSigns.DOT
import com.tangem.utils.StringsSigns.PLUS
import com.tangem.utils.extensions.orZero
import kotlinx.collections.immutable.ImmutableList
// TODO staking metrics block is temporary disabled
// private const val METRICS_BLOCK_KEY = "MetricsBlock"
private const val STAKING_REWARD_BLOCK_KEY = "StakingRewardBlock"
private const val ACTIVE_STAKING_BLOCK_KEY = "ActiveStakingBlock"
@OptIn(ExperimentalFoundationApi::class)
@Composable
internal fun StakingInitialInfoContent(state: StakingStates.InitialInfoState, clickIntents: StakingClickIntents) {
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(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
LazyColumn(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.padding(horizontal = TangemTheme.dimens.spacing16),
) {
AnimatedVisibility(state.yieldBalance == InnerYieldBalanceState.Empty) {
MetricsBlock(state)
}
RoundedListWithDividers(state.infoItems)
AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") {
if (it is InnerYieldBalanceState.Data) {
StakingRewardBlock(
rewardCrypto = it.rewardsCrypto,
rewardFiat = it.rewardsFiat,
isRewardsToClaim = it.isRewardsToClaim,
onRewardsClick = clickIntents::openRewardsValidators,
)
// TODO staking metrics block is temporary disabled
// https://www.figma.com/design/Vs6SkVsFnUPsSCNwlnVf5U?node-id=12484-35755#876661319
// if (state.yieldBalance == InnerYieldBalanceState.Empty) {
// item(key = METRICS_BLOCK_KEY) {
// Column(modifier = Modifier.animateItemPlacement()) {
// MetricsBlock(state)
// SpacerH12()
// }
// }
// }
this.roundedListWithDividersItems(
rows = state.infoItems,
footerContent = { SpacerH12() },
)
if (state.yieldBalance is InnerYieldBalanceState.Data) {
item(key = STAKING_REWARD_BLOCK_KEY) {
Column(modifier = Modifier.animateItemPlacement()) {
StakingRewardBlock(
rewardCrypto = state.yieldBalance.rewardsCrypto,
rewardFiat = state.yieldBalance.rewardsFiat,
isRewardsToClaim = state.yieldBalance.isRewardsToClaim,
onRewardsClick = clickIntents::openRewardsValidators,
)
SpacerH12()
}
}
}
AnimatedContent(targetState = state.yieldBalance, label = "Rewards block visibility animation") {
if (it is InnerYieldBalanceState.Data) {
ActiveStakingBlock(it.balance, clickIntents::onActiveStake)
if (state.yieldBalance is InnerYieldBalanceState.Data) {
item(key = ACTIVE_STAKING_BLOCK_KEY) {
Column(modifier = Modifier.animateItemPlacement()) {
ActiveStakingBlock(state.yieldBalance.balance, clickIntents::onActiveStake)
SpacerH12()
}
}
}
}
}
@Suppress("UnusedPrivateMember")
@Composable
private fun MetricsBlock(state: StakingStates.InitialInfoState.Data) {
Column(
@ -160,7 +188,7 @@ private fun StakingRewardBlock(
InputRowDefault(
title = resourceReference(R.string.staking_rewards),
text = text,
iconRes = R.drawable.ic_chevron_right_24,
iconRes = R.drawable.ic_chevron_right_24.takeIf { isRewardsToClaim },
textColor = textColor,
modifier = Modifier
.clip(TangemTheme.shapes.roundedCornersXMedium)
@ -168,13 +196,14 @@ private fun StakingRewardBlock(
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
enabled = isRewardsToClaim,
onClick = onRewardsClick,
),
)
}
@Composable
private fun ActiveStakingBlock(groups: List<BalanceGroupedState>, onClick: (BalanceState) -> Unit) {
private fun ActiveStakingBlock(groups: ImmutableList<BalanceGroupedState>, onClick: (BalanceState) -> Unit) {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing12),
) {
@ -192,18 +221,20 @@ private fun ActiveStakingBlock(groups: List<BalanceGroupedState>, onClick: (Bala
) {
group.items.forEachIndexed { index, balance ->
key(balance.validator.address) {
val caption = combinedReference(
if (group.type == BalanceGroupType.UNSTAKED) {
resourceReference(R.string.staking_details_unbonding_period)
val caption = if (group.type == BalanceType.UNSTAKING) {
combinedReference(
resourceReference(R.string.staking_details_unbonding_period),
annotatedReference {
appendSpace()
appendColored(
text = balance.unbondingPeriod.resolveReference(),
color = TangemTheme.colors.text.accent,
)
}
} else {
resourceReference(R.string.app_name)
},
)
} else {
combinedReference(
resourceReference(R.string.app_name),
annotatedReference {
appendSpace()
appendColored(
@ -213,20 +244,21 @@ private fun ActiveStakingBlock(groups: List<BalanceGroupedState>, onClick: (Bala
),
color = TangemTheme.colors.text.accent,
)
}
},
)
},
)
}
InputRowImageInfo(
title = group.title.takeIf { index == 0 },
subtitle = stringReference(balance.validator.name),
caption = caption,
isGrayscaleImage = group.type == BalanceGroupType.UNSTAKED,
isGrayscaleImage = group.type == BalanceType.UNSTAKING,
infoTitle = balance.fiatAmount,
infoSubtitle = balance.cryptoAmount,
imageUrl = balance.validator.image.orEmpty(),
modifier = Modifier.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
enabled = group.isClickable,
onClick = { onClick(balance) },
),
)

View file

@ -1,201 +0,0 @@
package com.tangem.features.staking.impl.presentation.ui
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
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.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.amountScreen.ui.SendDoneButtons
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerW12
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.*
@Composable
internal fun StakingNavigationButtons(uiState: StakingUiState, modifier: Modifier = Modifier) {
val confirmInnerState = (uiState.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState
val isSuccessState = confirmInnerState == InnerConfirmationStakingState.COMPLETED
Column(
modifier = modifier
.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
) {
val confirmationDataState = uiState.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
SendDoneButtons(
txUrl = transactionDoneState?.txUrl.orEmpty(),
onExploreClick = uiState.clickIntents::onExploreClick,
onShareClick = uiState.clickIntents::onShareClick,
isVisible = isSuccessState,
)
StakingNavigationButton(
uiState = uiState,
modifier = Modifier,
)
}
}
@Composable
private fun StakingNavigationButton(uiState: StakingUiState, modifier: Modifier = Modifier) {
val hapticFeedback = LocalHapticFeedback.current
val isButtonsVisible = isPrevButtonVisible(uiState.currentStep)
val innerConfirmState = (uiState.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState
val isInProgressInnerState = innerConfirmState == InnerConfirmationStakingState.IN_PROGRESS
val isInAssentInnerState = innerConfirmState == InnerConfirmationStakingState.ASSENT
val showTangemIcon = uiState.currentStep == StakingStep.Confirmation &&
(isInProgressInnerState || isInAssentInnerState)
val buttonTextId = getButtonData(currentState = uiState)
val (isButtonEnabled, isButtonDisplayed) = isButtonEnabled(uiState)
val buttonIcon = if (showTangemIcon) {
TangemButtonIconPosition.End(R.drawable.ic_tangem_24)
} else {
TangemButtonIconPosition.None
}
Row(modifier = modifier) {
AnimatedVisibility(
visible = isButtonsVisible,
enter = expandHorizontally(expandFrom = Alignment.End),
exit = shrinkHorizontally(shrinkTowards = Alignment.End),
) {
Row {
Icon(
painter = painterResource(R.drawable.ic_back_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
modifier = Modifier
.clip(RoundedCornerShape(TangemTheme.dimens.radius16))
.background(TangemTheme.colors.button.secondary)
.clickable { uiState.clickIntents.onPrevClick() }
.padding(TangemTheme.dimens.spacing12),
)
SpacerW12()
}
}
AnimatedVisibility(
visible = isButtonDisplayed,
enter = fadeIn(),
exit = fadeOut(),
) {
TangemButton(
text = stringResource(buttonTextId),
icon = buttonIcon,
enabled = isButtonEnabled && isButtonDisplayed,
onClick = {
if (showTangemIcon) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onPrimaryClick(uiState)
},
showProgress = isInProgressInnerState,
modifier = Modifier.fillMaxWidth(),
colors = TangemButtonsDefaults.primaryButtonColors,
)
}
}
}
private fun getButtonData(currentState: StakingUiState): Int {
return when (currentState.currentStep) {
StakingStep.InitialInfo -> {
val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
R.string.staking_stake_more
} else {
R.string.common_next
}
}
StakingStep.Confirmation -> {
val confirmationState = currentState.confirmationState
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
R.string.common_close
} else {
R.string.common_stake
}
} else {
R.string.common_close
}
}
StakingStep.Validators -> R.string.common_continue
StakingStep.Amount,
StakingStep.RewardsValidators,
-> R.string.common_next
}
}
private fun onPrimaryClick(currentState: StakingUiState) {
when (currentState.currentStep) {
StakingStep.InitialInfo -> {
val initialState = currentState.initialInfoState as? StakingStates.InitialInfoState.Data
if (initialState?.yieldBalance is InnerYieldBalanceState.Data) {
if (initialState.isStakeMoreAvailable) {
currentState.clickIntents.onNextClick()
}
} else {
currentState.clickIntents.onNextClick()
}
}
StakingStep.Amount -> currentState.clickIntents.onNextClick()
StakingStep.Confirmation -> {
val confirmationState = currentState.confirmationState
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
currentState.clickIntents.onBackClick()
} else {
currentState.clickIntents.onNextClick()
}
} else {
currentState.clickIntents.onBackClick()
}
}
StakingStep.Validators -> currentState.clickIntents.onNextClick()
StakingStep.RewardsValidators -> Unit
}
}
private fun isPrevButtonVisible(step: StakingStep): Boolean = when (step) {
StakingStep.InitialInfo,
StakingStep.RewardsValidators,
StakingStep.Confirmation,
-> false
StakingStep.Amount,
StakingStep.Validators,
-> true
}
private fun isButtonEnabled(uiState: StakingUiState): Pair<Boolean, Boolean> {
return when (uiState.currentStep) {
StakingStep.InitialInfo -> {
val initialState = uiState.initialInfoState as? StakingStates.InitialInfoState.Data
val isDisplayed = initialState?.isStakeMoreAvailable == true
uiState.initialInfoState.isPrimaryButtonEnabled to isDisplayed
}
StakingStep.Amount -> uiState.amountState.isPrimaryButtonEnabled to true
StakingStep.Confirmation -> uiState.confirmationState.isPrimaryButtonEnabled to true
StakingStep.RewardsValidators -> uiState.rewardsValidatorsState.isPrimaryButtonEnabled to false
StakingStep.Validators -> true to true
}
}

View file

@ -13,8 +13,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.tangem.common.ui.amountScreen.AmountScreenContent
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlock
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.R
import com.tangem.features.staking.impl.presentation.state.StakingStates
@ -29,7 +31,7 @@ import kotlinx.coroutines.flow.withIndex
@Composable
internal fun StakingScreen(uiState: StakingUiState) {
BackHandler(onBack = uiState.clickIntents::onBackClick)
BackHandler(onBack = uiState.clickIntents::onPrevClick)
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.tertiary)
@ -45,8 +47,13 @@ internal fun StakingScreen(uiState: StakingUiState) {
uiState = uiState,
modifier = Modifier.weight(1f),
)
StakingNavigationButtons(
uiState = uiState,
NavigationButtonsBlock(
buttonState = uiState.buttonsState,
modifier = Modifier.padding(
start = TangemTheme.dimens.spacing16,
end = TangemTheme.dimens.spacing16,
bottom = TangemTheme.dimens.spacing16,
),
)
StakingBottomSheet(bottomSheetConfig = uiState.bottomSheetConfig)
}
@ -68,7 +75,7 @@ private fun SendAppBar(uiState: StakingUiState) {
StakingStep.RewardsValidators,
StakingStep.Validators,
StakingStep.Confirmation,
-> stringResource(id = R.string.common_stake)
-> uiState.title.resolveReference()
}
val backIcon = when (uiState.currentStep) {
StakingStep.Amount,
@ -155,7 +162,7 @@ private fun StakingScreenContent(uiState: StakingUiState, modifier: Modifier = M
amountState = uiState.amountState,
state = uiState.confirmationState,
clickIntents = uiState.clickIntents,
type = uiState.routeType,
type = uiState.actionType,
)
StakingStep.Validators -> {
val confirmState = uiState.confirmationState

View file

@ -1,13 +1,23 @@
package com.tangem.features.staking.impl.presentation.ui.block
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import kotlinx.collections.immutable.ImmutableList
@Composable
internal fun NotificationsBlock(notifications: List<StakingNotification>) {
notifications.forEach {
Notification(config = it.config, iconTint = TangemTheme.colors.icon.accent)
internal fun NotificationsBlock(notifications: ImmutableList<StakingNotification>) {
notifications.forEach { notification ->
key(notification) {
Notification(
config = notification.config,
iconTint = when (notification) {
is StakingNotification.Error -> TangemTheme.colors.icon.warning
is StakingNotification.Warning -> TangemTheme.colors.icon.accent
},
)
}
}
}

View file

@ -39,7 +39,7 @@ internal fun StakingFeeBlock(feeState: FeeState) {
) {
Text(
text = stringResource(R.string.common_network_fee_title),
style = TangemTheme.typography.caption2,
style = TangemTheme.typography.subtitle2,
color = TangemTheme.colors.text.secondary,
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.staking.impl.presentation.viewmodel
import com.tangem.common.ui.amountScreen.AmountScreenClickIntents
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.Yield
import com.tangem.domain.staking.model.stakekit.action.StakingActionCommonType
import com.tangem.features.staking.impl.presentation.state.BalanceState
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import kotlinx.collections.immutable.ImmutableList
@ -12,13 +13,18 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onBackClick()
fun onNextClick(pendingActions: ImmutableList<PendingAction> = persistentListOf())
fun onNextClick(
actionType: StakingActionCommonType? = null,
pendingActions: ImmutableList<PendingAction> = persistentListOf(),
)
fun onActionClick(pendingAction: PendingAction?)
fun onPrevClick()
fun onInfoClick(infoType: InfoType)
override fun onAmountNext() = onNextClick()
override fun onAmountNext() = onNextClick(actionType = null)
fun openValidators()
@ -26,8 +32,6 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun openRewardsValidators()
fun selectRewardValidator(rewardValue: String)
fun onActiveStake(activeStake: BalanceState)
fun onExploreClick()

View file

@ -22,6 +22,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase
@ -62,6 +63,7 @@ internal class StakingViewModel @Inject constructor(
private val saveUnsubmittedHashUseCase: SaveUnsubmittedHashUseCase,
private val submitHashUseCase: SubmitHashUseCase,
private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase,
private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -100,28 +102,36 @@ internal class StakingViewModel @Inject constructor(
stakingStateRouter.onBackClick()
}
override fun onNextClick(pendingActions: ImmutableList<PendingAction>) {
handleOnNextConfirmationClick()
override fun onNextClick(actionType: StakingActionCommonType?, pendingActions: ImmutableList<PendingAction>) {
if (actionType != null) {
stateController.update { it.copy(actionType = actionType) }
}
stakingStateRouter.onNextClick()
if (isAssentState()) {
estimateGas(pendingActions)
}
}
private fun handleOnNextConfirmationClick() {
override fun onActionClick(pendingAction: PendingAction?) {
handleOnNextConfirmationClick(pendingAction)
stakingStateRouter.onNextClick()
}
private fun handleOnNextConfirmationClick(pendingAction: PendingAction?) {
if (isAssentState()) {
viewModelScope.launch {
stateController.update(SetConfirmationStateInProgressTransformer())
stateController.update(SetConfirmationStateInProgressTransformer(pendingAction))
val confirmationState =
value.confirmationState as? StakingStates.ConfirmationState.Data ?: error("No confirmation state")
val validatorState = confirmationState.validatorState as? ValidatorState.Content
?: error("No validator provided")
val pendingActions = confirmationState.pendingActions
val stakingTransaction = getStakingTransactionUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = getStakingCommonType(),
actionCommonType = value.actionType,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
@ -129,8 +139,8 @@ internal class StakingViewModel @Inject constructor(
?: error("No available address"),
validatorAddress = validatorState.chosenValidator.address,
token = yield.token,
passthrough = pendingActions.firstOrNull()?.passthrough,
type = pendingActions.firstOrNull()?.type,
passthrough = pendingAction?.passthrough,
type = pendingAction?.type,
),
).getOrElse {
error(it)
@ -141,7 +151,7 @@ internal class StakingViewModel @Inject constructor(
transactionId = stakingTransaction.id,
gasEstimate = stakingTransaction.gasEstimate ?: error("No gas estimate available"),
txData = TransactionData.Compiled(value = it.hexToBytes()),
pendingActions = pendingActions,
pendingActionList = confirmationState.pendingActions,
)
} ?: error("No unsigned transaction available")
}
@ -156,21 +166,31 @@ internal class StakingViewModel @Inject constructor(
),
)
val cryptoCurrencyValue = cryptoCurrencyStatus.value
val confirmationState = value.confirmationState as? StakingStates.ConfirmationState.Data
?: error("No confirmation state")
val validatorState = confirmationState.validatorState as? ValidatorState.Content
?: error("No validator provided")
val pendingAction = pendingActions.firstOrNull()
val stakingGasEstimate = estimateGasUseCase(
userWalletId = userWalletId,
network = cryptoCurrencyStatus.currency.network,
params = ActionParams(
actionCommonType = getStakingCommonType(),
actionCommonType = value.actionType,
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
address = cryptoCurrencyValue.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"),
validatorAddress = validatorState.chosenValidator.address,
token = yield.token,
passthrough = pendingActions.firstOrNull()?.passthrough,
type = pendingActions.firstOrNull()?.type,
passthrough = pendingAction?.passthrough,
type = pendingAction?.type,
),
).getOrElse { error("Can't get fee info") }
).getOrElse {
stateController.update(AddStakingErrorTransformer(it))
return@launch
}
stateController.update(
SetConfirmationStateAssentTransformer(
@ -196,7 +216,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onAmountValueChange(value: String) {
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, value))
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, value))
}
override fun onAmountPasteTriggerDismiss() {
@ -204,7 +224,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onMaxValueClick() {
stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus))
stateController.update(AmountMaxValueStateTransformer(cryptoCurrencyStatus, yield))
}
override fun onCurrencyChangeClick(isFiat: Boolean) {
@ -217,25 +237,17 @@ internal class StakingViewModel @Inject constructor(
stateController.update(ValidatorSelectChangeTransformer(validator))
}
override fun openRewardsValidators() {
stateController.update { it.copy(routeType = RouteType.CLAIM) }
onNextClick()
}
override fun selectRewardValidator(rewardValue: String) {
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, rewardValue))
onNextClick()
}
override fun openRewardsValidators() = onNextClick(actionType = StakingActionCommonType.PENDING_REWARDS)
override fun onActiveStake(activeStake: BalanceState) {
val routeType = if (activeStake.pendingActions.isEmpty()) {
RouteType.UNSTAKE
val actionType = if (activeStake.pendingActions.isEmpty()) {
StakingActionCommonType.EXIT
} else {
RouteType.OTHER
StakingActionCommonType.PENDING_OTHER
}
stateController.update { it.copy(routeType = routeType) }
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, activeStake.cryptoValue))
onNextClick(activeStake.pendingActions)
stateController.update(ValidatorSelectChangeTransformer(activeStake.validator))
stateController.update(AmountChangeStateTransformer(cryptoCurrencyStatus, yield, activeStake.cryptoValue))
onNextClick(actionType, activeStake.pendingActions)
}
override fun onExploreClick() {
@ -249,7 +261,7 @@ internal class StakingViewModel @Inject constructor(
}
override fun onShareClick() {
// TODO staking analytics event
// TODO add hash to clipboard and send analytics event
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
@ -317,7 +329,7 @@ internal class StakingViewModel @Inject constructor(
transactionId: String,
gasEstimate: StakingGasEstimate,
txData: TransactionData,
pendingActions: ImmutableList<PendingAction>,
pendingActionList: ImmutableList<PendingAction>,
) {
sendTransactionUseCase(
txData = txData,
@ -331,14 +343,14 @@ internal class StakingViewModel @Inject constructor(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = gasEstimate,
pendingActionList = pendingActions,
pendingActionList = pendingActionList,
),
)
// todo add error dialog
},
ifRight = { txHash ->
submitHash(transactionId, txHash)
updateStakeBalance()
val txUrl = getExplorerTransactionUrlUseCase(
txHash = txHash,
networkId = cryptoCurrencyStatus.currency.network.id,
@ -371,17 +383,22 @@ internal class StakingViewModel @Inject constructor(
}
}
private fun updateStakeBalance() {
viewModelScope.launch {
stakingYieldBalanceUseCase(
userWalletId = userWalletId,
address = CryptoCurrencyAddress(
cryptoCurrencyStatus.currency,
cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value.orEmpty(),
),
refresh = true,
)
}
}
private fun isAssentState(): Boolean {
return value.currentStep == StakingStep.Confirmation &&
(value.confirmationState as? StakingStates.ConfirmationState.Data)?.innerState ==
InnerConfirmationStakingState.ASSENT
}
private fun getStakingCommonType() = when (value.routeType) {
RouteType.STAKE -> StakingActionCommonType.ENTER
RouteType.UNSTAKE -> StakingActionCommonType.EXIT
RouteType.CLAIM,
RouteType.OTHER,
-> StakingActionCommonType.PENDING
}
}