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

@ -156,4 +156,16 @@ internal object StakingDomainModule {
stakingErrorResolver = stakingErrorResolver,
)
}
@Provides
@Singleton
fun provideIsApproveNeededUseCase(
stakingRepository: StakingRepository,
stakingErrorResolver: StakingErrorResolver,
): IsApproveNeededUseCase {
return IsApproveNeededUseCase(
stakingRepository = stakingRepository,
stakingErrorResolver = stakingErrorResolver,
)
}
}

View file

@ -54,7 +54,7 @@ class AmountStateConverter(
return AmountState.Data(
walletName = userWallet.name,
walletBalance = resourceReference(R.string.send_wallet_balance_format, wrappedList(crypto, fiat)),
walletBalance = resourceReference(R.string.common_crypto_fiat_format, wrappedList(crypto, fiat)),
tokenIconState = iconStateConverter.convert(status),
amountTextField = amountFieldConverter.convert(value),
isPrimaryButtonEnabled = false,

View file

@ -93,10 +93,12 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh
PrimaryButtonIconEnd(
text = stringResource(id = R.string.common_approve),
iconResId = R.drawable.ic_tangem_24,
showProgress = data.approveButton.loading,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
onClick = data.approveButton.onClick,
enabled = data.approveButton.enabled,
)
SpacerH12()
@ -107,6 +109,7 @@ private fun GiveTxPermissionBottomSheetContent(content: GiveTxPermissionBottomSh
.fillMaxWidth()
.padding(horizontal = TangemTheme.dimens.spacing16),
onClick = content.onCancel,
enabled = data.cancelButton.enabled,
)
SpacerH16()
@ -151,7 +154,7 @@ private fun AmountItem(
currency: String,
approveType: ApproveType,
approveItems: ImmutableList<ApproveType>,
onChangeApproveType: (ApproveType) -> Unit,
onChangeApproveType: ((ApproveType) -> Unit)?,
) {
var isExpandSelector by remember { mutableStateOf(false) }
var amountSize by remember { mutableStateOf(IntSize.Zero) }
@ -208,8 +211,10 @@ private fun AmountItem(
isExpanded = isExpandSelector,
onDismiss = { isExpandSelector = false },
onItemClick = { approveType ->
isExpandSelector = false
onChangeApproveType.invoke(approveType)
onChangeApproveType?.let {
isExpandSelector = false
onChangeApproveType.invoke(approveType)
}
},
items = approveItems,
selectedType = approveType,

View file

@ -22,7 +22,7 @@ sealed class GiveTxPermissionState {
val approveItems: ImmutableList<ApproveType> = ApproveType.entries.toImmutableList(),
val approveButton: ApprovePermissionButton,
val cancelButton: CancelPermissionButton,
val onChangeApproveType: (ApproveType) -> Unit,
val onChangeApproveType: ((ApproveType) -> Unit)? = null,
) : GiveTxPermissionState()
}

View file

@ -88,6 +88,7 @@
<string name="common_copy">Kopieren</string>
<string name="common_copy_address">Adresse kopieren</string>
<string name="common_create">Erstellen</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
<string name="common_custom">Eigen</string>
<plurals name="common_days">
<item quantity="one">%d tag</item>
@ -639,7 +640,6 @@
<string name="send_transaction_success">Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert</string>
<string name="send_tron_account_activation_error">%1$s ist ein Vermögenswert im Tron-Netzwerk. Um die Gebühr zu berechnen und eine Transaktion durchzuführen, musst du etwas Tron (TRX) auf deinem Konto einzahlen.</string>
<string name="send_validation_invalid_address">Ungültige Adresse</string>
<string name="send_wallet_balance_format">%1$s (%2$s)</string>
<string name="sent_transaction_sent_title">Transaktion gesendet</string>
<string name="settings_card_settings_footer">Bereite das Scannen der Karte vor, die du einrichten möchtest.</string>
<string name="settings_forget_wallet">Entferne diese Wallet</string>

View file

@ -86,6 +86,7 @@
<string name="common_copy">Copier</string>
<string name="common_copy_address">Copier l\'adresse</string>
<string name="common_create">Créer</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
<string name="common_custom">Personnalisé</string>
<plurals name="common_days">
<item quantity="one">%d jour</item>
@ -567,7 +568,6 @@
<string name="send_transaction_success">La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps</string>
<string name="send_tron_account_activation_error">%1$s est un actif du réseau Tron. Pour calculer les frais et effectuer une transaction, déposez du Tron (TRX) sur votre compte.</string>
<string name="send_validation_invalid_address">Adresse incorrecte</string>
<string name="send_wallet_balance_format">%1$s (%2$s)</string>
<string name="sent_transaction_sent_title">Transaction envoyée</string>
<string name="settings_card_settings_footer">Préparez-vous à numériser la carte que vous souhaitez configurer.</string>
<string name="settings_forget_wallet">Oublier le portefeuille</string>

View file

@ -87,6 +87,7 @@
<string name="common_copy">コピー</string>
<string name="common_copy_address">アドレスをコピー</string>
<string name="common_create">作成</string>
<string name="common_crypto_fiat_format">%1$s ( %2$s )</string>
<string name="common_custom">設定</string>
<plurals name="common_days">
<item quantity="other">%d 日</item>
@ -627,7 +628,6 @@
<string name="send_transaction_success">取引は正常に署名され、ブロックチェーンノードに送信されました。ウォレットの残高はしばらくして更新されます。</string>
<string name="send_tron_account_activation_error">%1$sはTronネットワークのアセットです。手数料を計算して取引を行うには、アカウントにTronTRXを入金する必要があります。</string>
<string name="send_validation_invalid_address">無効なアドレス</string>
<string name="send_wallet_balance_format">%1$s ( %2$s )</string>
<string name="sent_transaction_sent_title">取引が送信されました</string>
<string name="settings_card_settings_footer">セットアップしたいカードをスキャンするために準備してください。</string>
<string name="settings_forget_wallet">ウォレット削除</string>

View file

@ -90,6 +90,7 @@
<string name="common_copy">Копіювати</string>
<string name="common_copy_address">Скопіювати адресу</string>
<string name="common_create">Створити</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
<string name="common_custom">Власна</string>
<plurals name="common_days">
<item quantity="one">%d день</item>
@ -651,7 +652,6 @@
<string name="send_transaction_success">Транзакція успішно підписана і відправлена до блокчейну. Баланс гаманця буде оновлено через деякий час</string>
<string name="send_tron_account_activation_error">%1$s — це монета у мережі Tron. Щоб розрахувати комісію та здійснити транзакцію, вам необхідно внести певну кількість Tron (TRX) на свій рахунок.</string>
<string name="send_validation_invalid_address">Недійсна адреса</string>
<string name="send_wallet_balance_format">%1$s (%2$s)</string>
<string name="sent_transaction_sent_title">Трансакцію надіслано</string>
<string name="settings_card_settings_footer">Підготуйтеся до сканування картку, яку потрібно налаштувати.</string>
<string name="settings_forget_wallet">Забути гаманець</string>

View file

@ -88,6 +88,7 @@
<string name="common_copy">Copy</string>
<string name="common_copy_address">Copy address</string>
<string name="common_create">Create</string>
<string name="common_crypto_fiat_format">%1$s (%2$s)</string>
<string name="common_custom">Custom</string>
<plurals name="common_days">
<item quantity="one">%d day</item>
@ -635,7 +636,6 @@
<string name="send_transaction_success">Transaction has been successfully signed and sent to the blockchain node. Wallet balance will be updated in a while</string>
<string name="send_tron_account_activation_error">%1$s is an asset in the Tron network. To calculate the fee and make a transaction you need to deposit some Tron (TRX) in your account.</string>
<string name="send_validation_invalid_address">Invalid address</string>
<string name="send_wallet_balance_format">%1$s (%2$s)</string>
<string name="sent_transaction_sent_title">Transaction sent</string>
<string name="settings_card_settings_footer">Prepare to scan card you want to setup.</string>
<string name="settings_forget_wallet">Forget wallet</string>

View file

@ -52,7 +52,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@Suppress("LargeClass", "LongParameterList")
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
internal class DefaultStakingRepository(
private val stakeKitApi: StakeKitApi,
private val appPreferencesStore: AppPreferencesStore,
@ -99,8 +99,8 @@ internal class DefaultStakingRepository(
value = emptyMap<UserWalletId, Boolean>(),
)
override fun isStakingSupported(currencyId: String): Boolean {
return integrationIdMap.containsKey(currencyId)
override fun isStakingSupported(integrationKey: String): Boolean {
return integrationIdMap.containsKey(integrationKey)
}
override suspend fun fetchEnabledYields(refresh: Boolean) {
@ -153,7 +153,7 @@ internal class DefaultStakingRepository(
val yields = getEnabledYields() ?: return@withContext StakingAvailability.Unavailable
val prefetchedYield = findPrefetchedYield(yields, rawCurrencyId, symbol)
val isSupported = isStakingSupported(rawCurrencyId)
val isSupported = isStakingSupported(cryptoCurrencyId.getIntegrationKey())
when {
prefetchedYield != null && isSupported -> {
@ -250,10 +250,7 @@ internal class DefaultStakingRepository(
if (!stakingFeatureToggle.isStakingEnabled) return@withContext
val cryptoCurrency = address.cryptoCurrency
val rawCurrencyId =
cryptoCurrency.id.rawCurrencyId ?: error("Staking custom tokens is not available")
val integrationId = integrationIdMap[rawCurrencyId] ?: return@withContext
val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()] ?: return@withContext
cacheRegistry.invokeOnExpire(
key = getYieldBalancesKey(userWalletId),
@ -284,7 +281,7 @@ internal class DefaultStakingRepository(
send(YieldBalance.Empty)
} else {
launch(dispatchers.io) {
val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId]
val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()]
?: error("Could not get integrationId")
stakingBalanceStore.get(integrationId)
.collectLatest {
@ -317,7 +314,7 @@ internal class DefaultStakingRepository(
} else {
fetchSingleYieldBalance(userWalletId, address)
val integrationId = integrationIdMap[address.cryptoCurrency.id.rawCurrencyId]
val integrationId = integrationIdMap[address.cryptoCurrency.id.getIntegrationKey()]
?: error("Could not get integrationId")
val result = stakingBalanceStore.getSyncOrNull(integrationId) ?: return@withContext YieldBalance.Error
yieldBalanceConverter.convert(
@ -347,8 +344,7 @@ internal class DefaultStakingRepository(
addresses
.mapNotNull { networkAddress ->
val cryptoCurrency = networkAddress.cryptoCurrency
val rawCurrencyId = cryptoCurrency.id.rawCurrencyId ?: error("Currency raw id is null")
val integrationId = integrationIdMap[rawCurrencyId]
val integrationId = integrationIdMap[cryptoCurrency.id.getIntegrationKey()]
if (integrationId != null) {
networkAddress.address to integrationId
@ -534,6 +530,13 @@ internal class DefaultStakingRepository(
}
}
override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean {
return when (cryptoCurrency.id.getIntegrationKey()) {
Blockchain.Ethereum.id.plus(Blockchain.Polygon.toCoinId()) -> true
else -> false
}
}
private fun findPrefetchedYield(yields: List<Yield>, currencyId: String, symbol: String): Yield? {
return yields.find { it.token.coinGeckoId == currencyId && it.token.symbol == symbol }
}
@ -559,13 +562,15 @@ internal class DefaultStakingRepository(
private fun getYieldBalancesKey(userWalletId: UserWalletId) = "yield_balance_${userWalletId.stringValue}"
private fun CryptoCurrency.ID.getIntegrationKey(): String = rawNetworkId.plus(rawCurrencyId)
private companion object {
const val YIELDS_STORE_KEY = "yields"
const val SOLANA_INTEGRATION_ID = "solana-sol-native-multivalidator-staking"
const val COSMOS_INTEGRATION_ID = "cosmos-atom-native-staking"
const val ETHEREUM_POLYGON_INTEGRATION_ID = "ethereum-matic-native-staking"
const val POLKADOT_INTEGRATION_ID = "polkadot-dot-validator-staking"
const val ETHEREUM_INTEGRATION_ID = "ethereum-matic-native-staking"
const val AVALANCHE_INTEGRATION_ID = "avalanche-avax-native-staking"
const val TRON_INTEGRATION_ID = "tron-trx-native-staking"
const val CRONOS_INTEGRATION_ID = "cronos-cro-native-staking"
@ -576,17 +581,17 @@ internal class DefaultStakingRepository(
// uncomment items as implementation is ready
val integrationIdMap = mapOf(
Blockchain.Solana.toCoinId() to SOLANA_INTEGRATION_ID,
Blockchain.Cosmos.toCoinId() to COSMOS_INTEGRATION_ID,
// Blockchain.Polkadot.toCoinId() to POLKADOT_INTEGRATION_ID,
// Blockchain.Polygon.toCoinId() to ETHEREUM_INTEGRATION_ID,
// Blockchain.Avalanche.toCoinId() to AVALANCHE_INTEGRATION_ID,
// Blockchain.Tron.toCoinId() to TRON_INTEGRATION_ID,
// Blockchain.Cronos.toCoinId() to CRONOS_INTEGRATION_ID,
// Blockchain.BSC.toCoinId() to BINANCE_INTEGRATION_ID,
// Blockchain.Kava.toCoinId() to KAVA_INTEGRATION_ID,
// Blockchain.Near.toCoinId() to NEAR_INTEGRATION_ID,
// Blockchain.Tezos.toCoinId() to TEZOS_INTEGRATION_ID,
Blockchain.Solana.run { id + toCoinId() } to SOLANA_INTEGRATION_ID,
Blockchain.Cosmos.run { id + toCoinId() } to COSMOS_INTEGRATION_ID,
Blockchain.Ethereum.id + Blockchain.Polygon.toCoinId() to ETHEREUM_POLYGON_INTEGRATION_ID,
// Blockchain.Polkadot.run { id + toCoinId() } to POLKADOT_INTEGRATION_ID,
// Blockchain.Avalanche.run { id + toCoinId() } to AVALANCHE_INTEGRATION_ID,
// Blockchain.Tron.run { id + toCoinId() } to TRON_INTEGRATION_ID,
// Blockchain.Cronos.run { id + toCoinId() } to CRONOS_INTEGRATION_ID,
// Blockchain.BSC.run { id + toCoinId() } to BINANCE_INTEGRATION_ID,
// Blockchain.Kava.run { id + toCoinId() } to KAVA_INTEGRATION_ID,
// Blockchain.Near.run { id + toCoinId() } to NEAR_INTEGRATION_ID,
// Blockchain.Tezos.run { id + toCoinId() } to TEZOS_INTEGRATION_ID,
)
}
}

View file

@ -0,0 +1,18 @@
package com.tangem.domain.staking
import arrow.core.Either
import com.tangem.domain.staking.model.stakekit.StakingError
import com.tangem.domain.staking.repositories.StakingErrorResolver
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
class IsApproveNeededUseCase(
private val stakingRepository: StakingRepository,
private val stakingErrorResolver: StakingErrorResolver,
) {
operator fun invoke(cryptoCurrency: CryptoCurrency): Either<StakingError, Boolean> {
return Either
.catch { stakingRepository.isApproveNeeded(cryptoCurrency) }
.mapLeft { stakingErrorResolver.resolve(it) }
}
}

View file

@ -15,9 +15,10 @@ import com.tangem.domain.tokens.model.Network
import com.tangem.domain.wallets.models.UserWalletId
import kotlinx.coroutines.flow.Flow
@Suppress("TooManyFunctions")
interface StakingRepository {
fun isStakingSupported(currencyId: String): Boolean
fun isStakingSupported(integrationKey: String): Boolean
suspend fun fetchEnabledYields(refresh: Boolean)
@ -75,4 +76,7 @@ interface StakingRepository {
/** Returns whether additional staking is possible if there is already active staking */
fun isStakeMoreAvailable(networkId: Network.ID): Boolean
/** Returns whether approve is needed before staking */
fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean
}

View file

@ -2,11 +2,13 @@ package com.tangem.domain.tokens.repository
import com.tangem.domain.core.lce.LceFlow
import com.tangem.domain.core.lce.lceFlow
import com.tangem.domain.staking.model.*
import com.tangem.domain.staking.model.StakingAvailability
import com.tangem.domain.staking.model.StakingEntryInfo
import com.tangem.domain.staking.model.UnsubmittedTransactionMetadata
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.action.StakingAction
import com.tangem.domain.staking.model.stakekit.action.StakingActionStatus
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import com.tangem.domain.staking.model.stakekit.*
import com.tangem.domain.staking.model.stakekit.transaction.*
import com.tangem.domain.staking.repositories.StakingRepository
import com.tangem.domain.tokens.model.CryptoCurrency
@ -237,4 +239,6 @@ class MockStakingRepository : StakingRepository {
}
override fun isStakeMoreAvailable(networkId: Network.ID): Boolean = true
override fun isApproveNeeded(cryptoCurrency: CryptoCurrency): Boolean = true
}

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(