Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-12 13:54:35 +05:00
parent faa58cf142
commit ea8c9079b7
24 changed files with 311 additions and 69 deletions

View file

@ -88,6 +88,7 @@ internal sealed class StakingStates {
val footerText: String,
val transactionDoneState: TransactionDoneState,
val pendingActionInProgress: PendingAction? = null,
val isApprovalNeeded: Boolean,
) : ConfirmationState()
data class Empty(

View file

@ -86,5 +86,6 @@ internal object ConfirmationStatePreviewData {
),
transactionDoneState = TransactionDoneState.Empty,
pendingActions = persistentListOf(),
isApprovalNeeded = false,
)
}

View file

@ -38,6 +38,10 @@ object StakingClickIntentsStub : StakingClickIntents {
override fun openRewardsValidators() {}
override fun showApprovalBottomSheet() {}
override fun onApprovalClick() {}
override fun onExploreClick() {}
override fun onShareClick() {}

View file

@ -135,23 +135,7 @@ internal class SetButtonsStateTransformer : Transformer<StakingUiState> {
}
}
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.Confirmation -> getConfirmationButtonText()
StakingStep.Validators -> resourceReference(R.string.common_continue)
StakingStep.Amount,
StakingStep.RewardsValidators,
@ -159,6 +143,30 @@ internal class SetButtonsStateTransformer : Transformer<StakingUiState> {
}
}
private fun StakingUiState.getConfirmationButtonText(): TextReference {
return if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
resourceReference(R.string.common_close)
} else {
when (actionType) {
StakingActionCommonType.ENTER -> {
if (confirmationState.isApprovalNeeded) {
resourceReference(R.string.give_permission_title)
} else {
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)
}
}
private fun StakingUiState.onPrimaryClick() {
when (currentStep) {
StakingStep.InitialInfo -> {
@ -169,18 +177,27 @@ internal class SetButtonsStateTransformer : Transformer<StakingUiState> {
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())
}
StakingStep.Confirmation -> onConfirmationClick()
StakingStep.RewardsValidators -> Unit
}
}
private fun StakingUiState.onConfirmationClick() {
if (confirmationState is StakingStates.ConfirmationState.Data) {
if (confirmationState.innerState == InnerConfirmationStakingState.COMPLETED) {
clickIntents.onBackClick()
} else {
val isEnterAction = actionType == StakingActionCommonType.ENTER
val isApproveNeeded = confirmationState.isApprovalNeeded
if (isEnterAction && isApproveNeeded) {
clickIntents.showApprovalBottomSheet()
} else {
clickIntents.onBackClick()
clickIntents.onActionClick(confirmationState.pendingActions.firstOrNull())
}
}
StakingStep.RewardsValidators -> Unit
} else {
clickIntents.onBackClick()
}
}

View file

@ -6,8 +6,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.staking.model.stakekit.PendingAction
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.presentation.state.FeeState
import com.tangem.features.staking.impl.presentation.state.InnerConfirmationStakingState
import com.tangem.features.staking.impl.presentation.state.*
import com.tangem.features.staking.impl.presentation.state.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.Provider
@ -49,6 +48,7 @@ internal class SetConfirmationStateAssentTransformer(
validatorState = validatorState.copySealed(isClickable = true),
pendingActions = pendingActionList,
isPrimaryButtonEnabled = true,
isApprovalNeeded = false,
)
} else {
return this

View file

@ -20,7 +20,7 @@ internal class SetConfirmationStateLoadingTransformer(
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Loading,
validatorState = ValidatorState.Content(
isClickable = false,
isClickable = true,
chosenValidator = chosenValidator,
availableValidators = yield.validators,
),
@ -34,6 +34,7 @@ internal class SetConfirmationStateLoadingTransformer(
transactionDoneState = TransactionDoneState.Empty,
pendingActions = persistentListOf(),
pendingActionInProgress = null,
isApprovalNeeded = false,
),
)
}

View file

@ -0,0 +1,34 @@
package com.tangem.features.staking.impl.presentation.state.transformers.approval
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.transformer.Transformer
internal class SetApprovalBottomSheetInProgressTransformer(
private val onDismiss: () -> Unit,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val approvalBottomSheetConfig = prevState.bottomSheetConfig?.content as? GiveTxPermissionBottomSheetConfig
return prevState.copy(
bottomSheetConfig = prevState.bottomSheetConfig?.copy(
onDismissRequest = onDismiss,
isShow = true,
content = approvalBottomSheetConfig?.let { config ->
config.copy(
data = config.data.copy(
approveButton = config.data.approveButton.copy(
enabled = false,
loading = true,
),
cancelButton = config.data.cancelButton.copy(
enabled = false,
),
),
onCancel = onDismiss,
)
} as TangemBottomSheetConfigContent,
),
)
}
}

View file

@ -0,0 +1,78 @@
package com.tangem.features.staking.impl.presentation.state.transformers.approval
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.common.ui.bottomsheet.permission.state.*
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.features.staking.impl.R
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.ValidatorState
import com.tangem.utils.Provider
import com.tangem.utils.transformer.Transformer
internal class ShowApprovalBottomSheetTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val onDismiss: () -> Unit,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val cryptoCurrency = cryptoCurrencyStatusProvider().currency
val cryptoCurrencyValue = cryptoCurrencyStatusProvider().value
val amountState = prevState.amountState as? AmountState.Data ?: return prevState
val confirmationState = prevState.confirmationState as? StakingStates.ConfirmationState.Data ?: return prevState
val validatorState = confirmationState.validatorState as? ValidatorState.Content ?: return prevState
val feeState = confirmationState.feeState as? FeeState.Content ?: return prevState
val walletAddress = cryptoCurrencyValue.networkAddress?.defaultAddress?.value.orEmpty()
val validatorAddress = validatorState.chosenValidator.address
val feeCryptoValue = BigDecimalFormatter.formatCryptoAmount(
cryptoAmount = feeState.fee?.amount?.value,
cryptoCurrency = cryptoCurrency,
)
val feeFiatValue = BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeState.fee?.amount?.maxValue,
fiatCurrencyCode = appCurrencyProvider().code,
fiatCurrencySymbol = appCurrencyProvider().symbol,
)
return prevState.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShow = true,
onDismissRequest = onDismiss,
content = GiveTxPermissionBottomSheetConfig(
data = GiveTxPermissionState.ReadyForRequest(
currency = cryptoCurrency.symbol,
amount = amountState.amountTextField.value,
approveType = ApproveType.LIMITED,
walletAddress = walletAddress,
spenderAddress = validatorAddress,
fee = resourceReference(
R.string.common_crypto_fiat_format,
wrappedList(feeCryptoValue, feeFiatValue),
),
approveButton = ApprovePermissionButton(
enabled = true,
loading = false,
onClick = prevState.clickIntents::onApprovalClick,
),
cancelButton = CancelPermissionButton(
enabled = true,
),
subtitle = resourceReference(
id = R.string.give_permission_staking_subtitle,
formatArgs = wrappedList(cryptoCurrency.symbol),
),
dialogText = resourceReference(R.string.give_permission_staking_footer),
),
onCancel = onDismiss,
),
),
)
}
}

View file

@ -13,6 +13,8 @@ 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.bottomsheet.permission.GiveTxPermissionBottomSheet
import com.tangem.common.ui.bottomsheet.permission.state.GiveTxPermissionBottomSheetConfig
import com.tangem.common.ui.navigationButtons.NavigationButtonsBlock
import com.tangem.core.ui.components.appbar.AppBarWithBackButtonAndIcon
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
@ -64,6 +66,7 @@ fun StakingBottomSheet(bottomSheetConfig: TangemBottomSheetConfig?) {
if (bottomSheetConfig == null) return
when (bottomSheetConfig.content) {
is StakingInfoBottomSheetConfig -> StakingInfoBottomSheet(bottomSheetConfig)
is GiveTxPermissionBottomSheetConfig -> GiveTxPermissionBottomSheet(bottomSheetConfig)
}
}

View file

@ -36,6 +36,10 @@ internal interface StakingClickIntents : AmountScreenClickIntents {
fun onActiveStake(activeStake: BalanceState)
fun showApprovalBottomSheet()
fun onApprovalClick()
fun onExploreClick()
fun onShareClick()

View file

@ -11,6 +11,9 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.bundle.unbundle
import com.tangem.common.ui.amountScreen.models.AmountState
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.haptic.VibratorHapticManager
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
@ -36,9 +39,13 @@ import com.tangem.features.staking.impl.presentation.state.transformers.amount.A
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.approval.SetApprovalBottomSheetInProgressTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.ShowApprovalBottomSheetTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.validator.ValidatorSelectChangeTransformer
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.*
@ -64,14 +71,16 @@ internal class StakingViewModel @Inject constructor(
private val submitHashUseCase: SubmitHashUseCase,
private val isStakeMoreAvailableUseCase: IsStakeMoreAvailableUseCase,
private val stakingYieldBalanceUseCase: FetchStakingYieldBalanceUseCase,
private val isApproveNeededUseCase: IsApproveNeededUseCase,
private val clipboardManager: ClipboardManager,
private val vibratorHapticManager: VibratorHapticManager,
savedStateHandle: SavedStateHandle,
) : ViewModel(), DefaultLifecycleObserver, StakingClickIntents {
val uiState: StateFlow<StakingUiState> = stateController.uiState
val value: StakingUiState get() = uiState.value
var stakingStateRouter: StakingStateRouter by Delegates.notNull()
private set
private var stakingStateRouter: StakingStateRouter by Delegates.notNull()
private val cryptoCurrencyId: CryptoCurrency.ID =
savedStateHandle.get<Bundle>(AppRoute.Staking.CRYPTO_CURRENCY_ID_KEY)
@ -92,6 +101,10 @@ internal class StakingViewModel @Inject constructor(
private var userWallet: UserWallet by Delegates.notNull()
private var appCurrency: AppCurrency by Delegates.notNull()
private var isApprovalNeeded: Boolean = false
private var approvalJobHolder: JobHolder = JobHolder()
init {
subscribeOnSelectedAppCurrency()
subscribeOnBalanceHiding()
@ -254,6 +267,29 @@ internal class StakingViewModel @Inject constructor(
onNextClick(actionType, activeStake.pendingActions)
}
override fun showApprovalBottomSheet() {
stateController.update(
ShowApprovalBottomSheetTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
) {
stateController.update(DismissBottomSheetStateTransformer())
},
)
}
override fun onApprovalClick() {
viewModelScope.launch {
stateController.update(
SetApprovalBottomSheetInProgressTransformer {
stateController.update(DismissBottomSheetStateTransformer())
},
)
// todo staking polygon
}.saveIn(approvalJobHolder)
}
override fun onExploreClick() {
val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
@ -265,7 +301,16 @@ internal class StakingViewModel @Inject constructor(
}
override fun onShareClick() {
// TODO add hash to clipboard and send analytics event
val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
val txUrl = transactionDoneState?.txUrl
if (txUrl != null) {
vibratorHapticManager.performOneTime(TangemHapticEffect.OneTime.Click)
clipboardManager.setText(text = txUrl)
}
// TODO staking [REDACTED_TASK_KEY]
}
fun setRouter(router: InnerStakingRouter, stateRouter: StakingStateRouter) {
@ -273,6 +318,10 @@ internal class StakingViewModel @Inject constructor(
this.stakingStateRouter = stateRouter
}
private fun setupApprovalNeeded() {
isApprovalNeeded = isApproveNeededUseCase(cryptoCurrencyStatus.currency).getOrElse { false }
}
private fun subscribeOnCurrencyStatusUpdates() {
viewModelScope.launch {
getUserWalletUseCase(userWalletId).fold(
@ -287,6 +336,8 @@ internal class StakingViewModel @Inject constructor(
ifRight = {
cryptoCurrencyStatus = it
setupApprovalNeeded()
val networkId = cryptoCurrencyStatus.currency.network.id
val isStakeMoreAvailable = isStakeMoreAvailableUseCase(networkId)
stateController.update(