Updated on 2026-08-14

This commit is contained in:
Tangem 2024-08-15 11:02:29 +05:00
parent cba839d8f2
commit 3a8897cd3c
10 changed files with 158 additions and 57 deletions

View file

@ -164,6 +164,7 @@ private fun AmountItem(
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.action)
.clickable(
enabled = onChangeApproveType != null,
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(),
onClick = { isExpandSelector = true },
@ -190,36 +191,35 @@ private fun AmountItem(
)
SpacerWMax()
Text(
text = stringResource(
when (approveType) {
ApproveType.LIMITED -> R.string.give_permission_current_transaction
ApproveType.UNLIMITED -> R.string.give_permission_unlimited
},
),
text = approveType.text.resolveReference(),
color = TangemTheme.colors.text.tertiary,
style = TangemTheme.typography.body1,
maxLines = 1,
)
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
if (onChangeApproveType != null) {
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)),
contentDescription = null,
tint = TangemTheme.colors.icon.informative,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
)
}
}
if (onChangeApproveType != null) {
DropdownSelector(
isExpanded = isExpandSelector,
onDismiss = { isExpandSelector = false },
onItemClick = { approveType ->
onChangeApproveType.let {
isExpandSelector = false
onChangeApproveType.invoke(approveType)
}
},
items = approveItems,
selectedType = approveType,
amountSize = amountSize,
)
}
DropdownSelector(
isExpanded = isExpandSelector,
onDismiss = { isExpandSelector = false },
onItemClick = { approveType ->
onChangeApproveType?.let {
isExpandSelector = false
onChangeApproveType.invoke(approveType)
}
},
items = approveItems,
selectedType = approveType,
amountSize = amountSize,
)
}
}

View file

@ -1,6 +1,8 @@
package com.tangem.common.ui.bottomsheet.permission.state
import com.tangem.common.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -26,8 +28,9 @@ sealed class GiveTxPermissionState {
) : GiveTxPermissionState()
}
enum class ApproveType {
LIMITED, UNLIMITED
enum class ApproveType(val text: TextReference) {
LIMITED(resourceReference(R.string.give_permission_current_transaction)),
UNLIMITED(resourceReference(R.string.give_permission_unlimited)),
}
data class ApprovePermissionButton(

View file

@ -69,5 +69,10 @@ internal sealed class StakingNotification(val config: NotificationConfig) {
wrappedList(cooldownPeriodDays, cooldownPeriodDays),
),
)
data class TransactionInProgress(
val title: TextReference,
val description: TextReference,
) : Warning(title = title, subtitle = description)
}
}

View file

@ -6,7 +6,8 @@ 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.*
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.StakingStates
import com.tangem.features.staking.impl.presentation.state.StakingUiState
import com.tangem.utils.Provider
@ -15,7 +16,7 @@ import kotlinx.collections.immutable.ImmutableList
internal class SetConfirmationStateAssentTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val stakingGasEstimate: StakingGasEstimate,
private val pendingActionList: ImmutableList<PendingAction>,
) : Transformer<StakingUiState> {
@ -30,6 +31,7 @@ internal class SetConfirmationStateAssentTransformer(
gasEstimate: StakingGasEstimate,
): StakingStates.ConfirmationState {
if (this is StakingStates.ConfirmationState.Data) {
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
return copy(
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Content(
@ -40,8 +42,8 @@ internal class SetConfirmationStateAssentTransformer(
decimals = gasEstimate.token.decimals,
),
),
rate = cryptoCurrencyStatusProvider().value.fiatRate,
isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate,
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),

View file

@ -14,7 +14,7 @@ import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateCompletedTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val stakingGasEstimate: StakingGasEstimate,
private val txUrl: String,
) : Transformer<StakingUiState> {
@ -29,6 +29,7 @@ internal class SetConfirmationStateCompletedTransformer(
gasEstimate: StakingGasEstimate,
): StakingStates.ConfirmationState {
if (this is StakingStates.ConfirmationState.Data) {
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
return copy(
isPrimaryButtonEnabled = true,
innerState = InnerConfirmationStakingState.COMPLETED,
@ -40,8 +41,8 @@ internal class SetConfirmationStateCompletedTransformer(
decimals = gasEstimate.token.decimals,
),
),
rate = cryptoCurrencyStatusProvider().value.fiatRate,
isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate,
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),

View file

@ -0,0 +1,32 @@
package com.tangem.features.staking.impl.presentation.state.transformers.approval
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.features.staking.impl.R
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.utils.transformer.Transformer
import kotlinx.collections.immutable.toPersistentList
internal object SetApprovalInProgressTransformer : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
val state = prevState.confirmationState as? StakingStates.ConfirmationState.Data
val notifications = state?.notifications?.toMutableList() ?: mutableListOf()
// todo staking [REDACTED_TASK_KEY] replace with correct text
notifications.add(
StakingNotification.Warning.TransactionInProgress(
title = resourceReference(R.string.warning_express_approval_in_progress_title),
description = resourceReference(R.string.warning_express_approval_in_progress_message),
),
)
val updatedConfirmationState = state?.copy(
notifications = notifications.toPersistentList(),
) ?: prevState.confirmationState
return prevState.copy(
confirmationState = updatedConfirmationState,
)
}
}

View file

@ -12,7 +12,7 @@ import com.tangem.utils.transformer.Transformer
internal class SetConfirmationStateAssentApprovalTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val fee: TransactionFee,
) : Transformer<StakingUiState> {
@ -25,12 +25,13 @@ internal class SetConfirmationStateAssentApprovalTransformer(
private fun StakingStates.ConfirmationState.copyWrapped(): StakingStates.ConfirmationState {
return if (this is StakingStates.ConfirmationState.Data) {
val isFeeConvertibleToFiat = feeCryptoCurrencyStatus?.currency?.network?.hasFiatFeeRate == true
copy(
innerState = InnerConfirmationStakingState.ASSENT,
feeState = FeeState.Content(
fee = fee.normal,
rate = cryptoCurrencyStatusProvider().value.fiatRate,
isFeeConvertibleToFiat = cryptoCurrencyStatusProvider().currency.network.hasFiatFeeRate,
rate = feeCryptoCurrencyStatus?.value?.fiatRate,
isFeeConvertibleToFiat = isFeeConvertibleToFiat,
appCurrency = appCurrencyProvider(),
isFeeApproximate = false,
),

View file

@ -19,6 +19,7 @@ import com.tangem.utils.transformer.Transformer
internal class ShowApprovalBottomSheetTransformer(
private val appCurrencyProvider: Provider<AppCurrency>,
private val cryptoCurrencyStatusProvider: Provider<CryptoCurrencyStatus>,
private val feeCryptoCurrencyStatus: CryptoCurrencyStatus?,
private val onDismiss: () -> Unit,
) : Transformer<StakingUiState> {
override fun transform(prevState: StakingUiState): StakingUiState {
@ -29,15 +30,17 @@ internal class ShowApprovalBottomSheetTransformer(
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 fee = feeState.fee ?: 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,
cryptoAmount = fee.amount.value,
cryptoCurrency = fee.amount.currencySymbol,
decimals = fee.amount.decimals,
)
val feeFiatValue = BigDecimalFormatter.formatFiatAmount(
fiatAmount = feeState.fee?.amount?.maxValue,
fiatAmount = feeCryptoCurrencyStatus?.value?.fiatRate?.multiply(fee.amount.value),
fiatCurrencyCode = appCurrencyProvider().code,
fiatCurrencySymbol = appCurrencyProvider().symbol,
)

View file

@ -1,8 +1,13 @@
package com.tangem.features.staking.impl.presentation.ui.block
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.CardWithIcon
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.staking.impl.presentation.state.StakingNotification
import kotlinx.collections.immutable.ImmutableList
@ -11,13 +16,27 @@ import kotlinx.collections.immutable.ImmutableList
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
},
)
if (notification is StakingNotification.Warning.TransactionInProgress) {
CardWithIcon(
title = notification.title.resolveReference(),
description = notification.description.resolveReference(),
icon = {
CircularProgressIndicator(
modifier = Modifier.size(TangemTheme.dimens.size16),
color = TangemTheme.colors.icon.primary1,
strokeWidth = TangemTheme.dimens.size2,
)
},
)
} else {
Notification(
config = notification.config,
iconTint = when (notification) {
is StakingNotification.Error -> TangemTheme.colors.icon.warning
is StakingNotification.Warning -> TangemTheme.colors.icon.accent
},
)
}
}
}
}

View file

@ -26,6 +26,7 @@ import com.tangem.domain.staking.model.stakekit.transaction.ActionParams
import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate
import com.tangem.domain.staking.model.stakekit.transaction.StakingTransactionType
import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.GetFeePaidCryptoCurrencyStatusSyncUseCase
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyAddress
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
@ -45,13 +46,12 @@ import com.tangem.features.staking.impl.presentation.state.transformers.amount.A
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.SetApprovalInProgressTransformer
import com.tangem.features.staking.impl.presentation.state.transformers.approval.SetConfirmationStateAssentApprovalTransformer
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 com.tangem.utils.coroutines.*
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.*
@ -68,6 +68,7 @@ internal class StakingViewModel @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
private val getCryptoCurrencyStatusSyncUseCase: GetCryptoCurrencyStatusSyncUseCase,
private val getFeePaidCryptoCurrencyStatusSyncUseCase: GetFeePaidCryptoCurrencyStatusSyncUseCase,
private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val getStakingTransactionUseCase: GetStakingTransactionUseCase,
@ -107,12 +108,14 @@ internal class StakingViewModel @Inject constructor(
?: error("This screen can't be opened without `Yield`")
private var cryptoCurrencyStatus: CryptoCurrencyStatus by Delegates.notNull()
private var feeCryptoCurrencyStatus: CryptoCurrencyStatus? = null
private var innerRouter: InnerStakingRouter by Delegates.notNull()
private var userWallet: UserWallet by Delegates.notNull()
private var appCurrency: AppCurrency by Delegates.notNull()
private var stakingApproval: StakingApproval = StakingApproval.Empty
private val allowanceTaskScheduler = SingleTaskScheduler<BigDecimal>()
private var approvalJobHolder: JobHolder = JobHolder()
@ -271,7 +274,7 @@ internal class StakingViewModel @Inject constructor(
stateController.update(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
stakingGasEstimate = stakingGasEstimate,
pendingActionList = pendingActions,
),
@ -292,7 +295,7 @@ internal class StakingViewModel @Inject constructor(
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = approvalFee,
),
)
@ -354,6 +357,7 @@ internal class StakingViewModel @Inject constructor(
ShowApprovalBottomSheetTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
) {
stateController.update(DismissBottomSheetStateTransformer())
},
@ -391,7 +395,7 @@ internal class StakingViewModel @Inject constructor(
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = TransactionFee.Single(fee),
),
)
@ -411,20 +415,49 @@ internal class StakingViewModel @Inject constructor(
stateController.update(
SetConfirmationStateAssentApprovalTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
fee = TransactionFee.Single(fee),
),
)
// TODO staking error
},
ifRight = {
getFee(confirmationState.pendingActions)
stateController.update(SetApprovalInProgressTransformer)
stateController.update(DismissBottomSheetStateTransformer())
awaitForAllowance(confirmationState.pendingActions)
},
)
}.saveIn(approvalJobHolder)
}
private fun awaitForAllowance(pendingActions: ImmutableList<PendingAction>) {
val approval = stakingApproval as? StakingApproval.Needed ?: return
allowanceTaskScheduler.scheduleTask(
scope = viewModelScope,
task = PeriodicTask(
delay = ALLOWANCE_UPDATE_DELAY,
task = {
runCatching {
getAllowanceUseCase(
userWalletId = userWalletId,
cryptoCurrency = cryptoCurrencyStatus.currency,
spenderAddress = approval.spenderAddress,
).getOrElse { BigDecimal.ZERO }
}
},
onSuccess = { allowance ->
val amount = (value.amountState as? AmountState.Data)?.amountTextField?.cryptoAmount?.value
?: error("No amount provided")
if (allowance >= amount) {
getFee(pendingActions)
allowanceTaskScheduler.cancelTask()
}
},
onError = { /* no-op */ },
),
)
}
override fun onExploreClick() {
val confirmationDataState = uiState.value.confirmationState as? StakingStates.ConfirmationState.Data
val transactionDoneState = confirmationDataState?.transactionDoneState as? TransactionDoneState.Content
@ -469,6 +502,7 @@ internal class StakingViewModel @Inject constructor(
)
getCryptoCurrencyStatusSyncUseCase(userWalletId, cryptoCurrencyId).fold(
ifRight = {
feeCryptoCurrencyStatus = getFeePaidCryptoCurrencyStatusSyncUseCase(userWalletId, it).getOrNull()
cryptoCurrencyStatus = it
setupApprovalNeeded()
@ -531,7 +565,7 @@ internal class StakingViewModel @Inject constructor(
stateController.update(
SetConfirmationStateAssentTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
stakingGasEstimate = gasEstimate,
pendingActionList = pendingActionList,
),
@ -549,7 +583,7 @@ internal class StakingViewModel @Inject constructor(
stateController.update(
SetConfirmationStateCompletedTransformer(
appCurrencyProvider = Provider { appCurrency },
cryptoCurrencyStatusProvider = Provider { cryptoCurrencyStatus },
feeCryptoCurrencyStatus = feeCryptoCurrencyStatus,
stakingGasEstimate = gasEstimate,
txUrl = txUrl,
),
@ -592,7 +626,8 @@ internal class StakingViewModel @Inject constructor(
InnerConfirmationStakingState.ASSENT
}
companion object {
private companion object {
const val WHAT_IS_STAKING_ARTICLE_URL = "TODO staking"
const val ALLOWANCE_UPDATE_DELAY = 10_000L
}
}