Updated on 2026-08-14

This commit is contained in:
Tangem 2024-06-27 20:32:12 +03:00
parent eec5a60676
commit 20dc8c9d90
19 changed files with 436 additions and 89 deletions

View file

@ -0,0 +1,20 @@
package com.tangem.features.staking.impl.presentation.state
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import java.math.BigDecimal
sealed class FeeState {
data class Content(
val fee: Fee?,
val rate: BigDecimal?,
val isFeeConvertibleToFiat: Boolean,
val appCurrency: AppCurrency,
val isFeeApproximate: Boolean,
) : FeeState()
data object Loading : FeeState()
data object Error : FeeState()
}

View file

@ -1,9 +1,7 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.TransactionFee
@Immutable
internal sealed class InnerFeeState {
data class Content(

View file

@ -7,22 +7,20 @@ internal class StakingStateRouter(
private val fragmentManager: WeakReference<FragmentManager>,
private val stateController: StakingStateController,
) {
fun clear() {
stateController.update { it.copy(currentStep = getInitialState()) }
}
fun popBackStack() {
private fun closeStaking() {
fragmentManager.get()?.popBackStack()
stateController.clear()
}
fun onBackClick(isSuccess: Boolean = false) {
val type = stateController.uiState.value.currentStep
when {
isSuccess -> popBackStack()
isSuccess -> closeStaking()
else -> when (type) {
StakingStep.Amount -> showInitial()
StakingStep.Confirm -> showAmount()
else -> popBackStack()
else -> closeStaking()
}
}
}
@ -34,14 +32,17 @@ internal class StakingStateRouter(
StakingStep.Amount,
-> showConfirm()
StakingStep.Confirm -> showSuccess()
StakingStep.Success -> onBackClick()
StakingStep.Success -> closeStaking()
}
}
fun onPrevClick() {
when (stateController.uiState.value.currentStep) {
StakingStep.Amount -> showInitial()
else -> popBackStack()
StakingStep.Confirm -> showAmount()
StakingStep.Success -> closeStaking()
StakingStep.Validators -> showConfirm()
else -> closeStaking()
}
}
@ -64,6 +65,4 @@ internal class StakingStateRouter(
fun showSuccess() {
stateController.update { it.copy(currentStep = StakingStep.Success) }
}
private fun getInitialState() = StakingStep.InitialInfo
}

View file

@ -1,16 +1,13 @@
package com.tangem.features.staking.impl.presentation.state
import androidx.compose.runtime.Immutable
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.event.StateEvent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.staking.impl.presentation.state.transformers.InfoType
import com.tangem.features.staking.impl.presentation.viewmodel.StakingClickIntents
import kotlinx.collections.immutable.ImmutableList
import java.math.BigDecimal
/**
* Ui states of the staking screen
@ -79,21 +76,12 @@ internal sealed class StakingStates {
override val isPrimaryButtonEnabled: Boolean = false,
) : ConfirmStakingState()
}
data class FeeState(
val innerFeeState: InnerFeeState,
val fee: Fee?,
val rate: BigDecimal?,
val isFeeConvertibleToFiat: Boolean,
val appCurrency: AppCurrency,
val isFeeApproximate: Boolean,
)
}
enum class StakingStep {
InitialInfo,
Amount,
Validators,
Confirm,
Validators,
Success,
}

View file

@ -3,10 +3,9 @@ package com.tangem.features.staking.impl.presentation.state.previewdata
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType.Coin
import com.tangem.blockchain.common.transaction.Fee
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.*
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
@ -65,8 +64,7 @@ internal object ConfirmStakingStatePreviewData {
val confirmStakingState = StakingStates.ConfirmStakingState.Data(
isPrimaryButtonEnabled = true,
feeState = StakingStates.FeeState(
innerFeeState = InnerFeeState.Content(TransactionFee.Single(fee)),
feeState = FeeState.Content(
fee = fee,
rate = BigDecimal.ONE,
appCurrency = AppCurrency.Default,

View file

@ -0,0 +1,42 @@
package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.domain.staking.model.Yield
import com.tangem.features.staking.impl.presentation.state.*
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.StakingUiState
import com.tangem.features.staking.impl.presentation.state.ValidatorState
import com.tangem.utils.transformer.Transformer
import kotlinx.collections.immutable.persistentListOf
@Suppress("UnusedPrivateMember")
internal class SetConfirmLoadingStateTransformer(
private val yield: Yield,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val possibleConfirmStakingState = prevState.confirmStakingState as? StakingStates.ConfirmStakingState.Data
val possibleValidatorState = possibleConfirmStakingState?.validatorState as? ValidatorState.Content
val chosenValidator = possibleValidatorState?.chosenValidator ?: yield.validators[0] // TODO staking add sorting
return prevState.copy(
confirmStakingState = StakingStates.ConfirmStakingState.Data(
isPrimaryButtonEnabled = false,
feeState = FeeState.Loading,
validatorState = ValidatorState.Content(
chosenValidator = chosenValidator,
availableValidators = yield.validators,
),
notifications = persistentListOf(
StakingNotification.Warning.EarnRewards(
currencyName = yield.token.name,
days = yield.metadata.cooldownPeriod.days,
),
),
footerText = "",
isSuccess = false,
isStaking = false, // TODO staking move to enum
),
)
}
}

View file

@ -2,34 +2,49 @@ package com.tangem.features.staking.impl.presentation.state.transformers
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.staking.model.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.transaction.usecase.IsFeeApproximateUseCase
import com.tangem.features.staking.impl.presentation.state.FeeState
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.previewdata.ConfirmStakingStatePreviewData
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
import com.tangem.blockchain.common.Amount
@Suppress("UnusedPrivateMember")
internal class SetConfirmStateDataStateTransformer(
private val yield: Yield,
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val isFeeApproximateUseCase: IsFeeApproximateUseCase,
private val stakingGasEstimate: StakingGasEstimate,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
// TODO staking fill with real data
return prevState.copy(
confirmStakingState = ConfirmStakingStatePreviewData.confirmStakingState,
confirmStakingState = prevState.confirmStakingState.copyWrapped(stakingGasEstimate),
)
}
private fun isFeeApproximate(fee: Fee): Boolean {
val cryptoCurrencyStatus = cryptoCurrencyStatusProvider()
return isFeeApproximateUseCase(
networkId = cryptoCurrencyStatus.currency.network.id,
amountType = fee.amount.type,
)
private fun StakingStates.ConfirmStakingState.copyWrapped(
gasEstimate: StakingGasEstimate,
): StakingStates.ConfirmStakingState {
if (this is StakingStates.ConfirmStakingState.Data) {
return copy(
feeState = FeeState.Content(
fee = Fee.Common(
Amount(
currencySymbol = gasEstimate.token.symbol,
value = gasEstimate.amount,
decimals = gasEstimate.token.decimals,
),
),
rate = cryptoCurrencyStatusProvider().value.fiatRate,
isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),
)
} else {
return this
}
}
}

View file

@ -116,8 +116,8 @@ 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
StakingStep.Validators -> true
}
}

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.transaction.Fee
import com.tangem.blockchain.common.transaction.TransactionFee
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.rows.SelectorRowItem
import com.tangem.core.ui.res.TangemThemePreview
@ -26,12 +25,11 @@ import com.tangem.common.ui.R
import com.tangem.common.ui.amountScreen.utils.getCryptoReference
import com.tangem.common.ui.amountScreen.utils.getFiatReference
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.features.staking.impl.presentation.state.InnerFeeState
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.FeeState
import java.math.BigDecimal
@Composable
internal fun StakingFeeBlock(feeState: StakingStates.FeeState) {
internal fun StakingFeeBlock(feeState: FeeState) {
Column(
modifier = Modifier
.fillMaxWidth()
@ -48,37 +46,45 @@ internal fun StakingFeeBlock(feeState: StakingStates.FeeState) {
Box(
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
) {
val feeAmount = feeState.fee?.amount
val (title, icon) = R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
SelectorRowItem(
titleRes = title,
iconRes = icon,
preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate),
postDot = if (feeState.isFeeConvertibleToFiat) {
getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency)
} else {
null
},
ellipsizeOffset = feeAmount?.currencySymbol?.length,
isSelected = true,
showDivider = false,
showSelectedAppearance = false,
paddingValues = PaddingValues(),
)
FeeLoading(feeState.innerFeeState)
FeeError(feeState.innerFeeState)
when (feeState) {
is FeeState.Content -> {
val feeAmount = feeState.fee?.amount
val (title, icon) = R.string.common_fee_selector_option_market to R.drawable.ic_bird_24
SelectorRowItem(
titleRes = title,
iconRes = icon,
preDot = getCryptoReference(feeAmount, feeState.isFeeApproximate),
postDot = if (feeState.isFeeConvertibleToFiat) {
getFiatReference(feeAmount?.value, feeState.rate, feeState.appCurrency)
} else {
null
},
ellipsizeOffset = feeAmount?.currencySymbol?.length,
isSelected = true,
showDivider = false,
showSelectedAppearance = false,
paddingValues = PaddingValues(),
)
}
is FeeState.Loading -> {
FeeLoading(feeState)
}
is FeeState.Error -> {
FeeError(feeState)
}
}
}
}
}
@Composable
private fun BoxScope.FeeLoading(feeSelectorState: InnerFeeState) {
private fun BoxScope.FeeLoading(feeState: FeeState) {
AnimatedContent(
targetState = feeSelectorState,
targetState = feeState,
label = "Fee Loading State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == InnerFeeState.Loading) {
if (it == FeeState.Loading) {
RectangleShimmer(
radius = TangemTheme.dimens.radius3,
modifier = Modifier.size(
@ -91,13 +97,13 @@ private fun BoxScope.FeeLoading(feeSelectorState: InnerFeeState) {
}
@Composable
private fun BoxScope.FeeError(feeSelectorState: InnerFeeState) {
private fun BoxScope.FeeError(feeState: FeeState) {
AnimatedContent(
targetState = feeSelectorState,
targetState = feeState,
label = "Fee Error State Change",
modifier = Modifier.align(Alignment.CenterEnd),
) {
if (it == InnerFeeState.Error) {
if (it == FeeState.Error) {
Text(
text = BigDecimalFormatter.EMPTY_BALANCE_SIGN,
color = TangemTheme.colors.text.primary1,
@ -111,7 +117,7 @@ private fun BoxScope.FeeError(feeSelectorState: InnerFeeState) {
@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: StakingStates.FeeState) {
private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) value: FeeState.Content) {
TangemThemePreview {
StakingFeeBlock(
feeState = value,
@ -119,9 +125,9 @@ private fun FeeBlockPreview(@PreviewParameter(FeeBlockPreviewProvider::class) va
}
}
private class FeeBlockPreviewProvider : PreviewParameterProvider<StakingStates.FeeState> {
private class FeeBlockPreviewProvider : PreviewParameterProvider<FeeState.Content> {
override val values: Sequence<StakingStates.FeeState>
override val values: Sequence<FeeState.Content>
get() = sequenceOf(
feeState,
)
@ -135,8 +141,7 @@ private class FeeBlockPreviewProvider : PreviewParameterProvider<StakingStates.F
),
)
private val feeState = StakingStates.FeeState(
innerFeeState = InnerFeeState.Content(TransactionFee.Single(normal = fee)),
private val feeState = FeeState.Content(
fee = fee,
rate = BigDecimal.ONE,
appCurrency = AppCurrency.Default,

View file

@ -8,9 +8,11 @@ import androidx.lifecycle.viewModelScope
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.staking.InitializeStakingProcessUseCase
import com.tangem.domain.staking.model.Yield
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
@ -21,6 +23,7 @@ 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.*
import com.tangem.features.staking.impl.presentation.state.transformers.amount.AmountChangeStateTransformer
@ -45,6 +48,7 @@ internal class StakingViewModel @Inject constructor(
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val initializeStakingProcessUseCase: InitializeStakingProcessUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
@ -85,10 +89,60 @@ internal class StakingViewModel @Inject constructor(
override fun onNextClick() {
stakingStateRouter.onNextClick()
when (value.currentStep) {
StakingStep.Confirm -> {
initStaking()
}
StakingStep.InitialInfo -> {
// TODO staking
}
StakingStep.Amount -> {
// TODO staking
}
StakingStep.Success -> {
// TODO staking
}
StakingStep.Validators -> {
// TODO staking
}
}
}
private fun initStaking() {
viewModelScope.launch {
stateController.update(
SetConfirmLoadingStateTransformer(
yield = yield,
),
)
val actionWithTransaction = initializeStakingProcessUseCase(
integrationId = yield.id,
amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided"),
address = cryptoCurrencyStatus.value.networkAddress?.defaultAddress?.value
?: error("No available address"),
validatorAddress = yield.validators.getOrNull(0)?.address ?: error("No available validator"),
token = yield.token,
)
val (enterAction, stakingTransaction) = actionWithTransaction.getOrElse {
error("Can't retrieve action and transaction")
}
val stakingGasEstimate = stakingTransaction.gasEstimate ?: error("Can't get fee info")
stateController.update(
SetConfirmStateDataStateTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
stakingGasEstimate = stakingGasEstimate,
),
)
}
}
override fun onPrevClick() {
stakingStateRouter.onBackClick()
stakingStateRouter.onPrevClick()
}
override fun onInfoClick(infoType: InfoType) {