Updated on 2026-08-14
This commit is contained in:
parent
a3b65cd194
commit
d22bfd0c87
27 changed files with 342 additions and 139 deletions
|
|
@ -16,6 +16,7 @@ interface GiveApprovalComponent : ComposableBottomSheetComponent {
|
|||
val spenderAddress: String,
|
||||
val subtitle: TextReference,
|
||||
val isHoldToConfirm: Boolean = false,
|
||||
val isResetApproval: Boolean = false,
|
||||
val callback: Callback,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.child
|
||||
import com.tangem.core.decompose.model.getOrCreateModel
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
|
||||
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
|
||||
|
|
@ -23,7 +24,6 @@ import com.tangem.features.send.v2.api.params.FeeSelectorParams
|
|||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import com.tangem.common.ui.R as CommonUiR
|
||||
|
||||
internal class DefaultGiveApprovalComponent @AssistedInject constructor(
|
||||
@Assisted appComponentContext: AppComponentContext,
|
||||
|
|
@ -38,6 +38,7 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor(
|
|||
params = FeeSelectorParams.FeeSelectorBlockParams(
|
||||
state = FeeSelectorUM.Loading,
|
||||
onLoadFee = { model.loadFee() },
|
||||
onDisableCustomFee = { model.shouldDisableCustomFee() },
|
||||
onLoadFeeExtended = { selectedFeeToken -> model.loadFeeExtended(selectedFeeToken) },
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
|
|
@ -71,22 +72,23 @@ internal class DefaultGiveApprovalComponent @AssistedInject constructor(
|
|||
TangemBottomSheet<TangemBottomSheetConfigContent.Empty>(
|
||||
config = config,
|
||||
containerColor = TangemTheme.colors.background.secondary,
|
||||
titleText = resourceReference(CommonUiR.string.give_permission_title),
|
||||
titleText = resourceReference(
|
||||
if (uiState.isResetApproval) {
|
||||
R.string.update_approval_permission_title
|
||||
} else {
|
||||
R.string.give_permission_title
|
||||
},
|
||||
),
|
||||
titleAction = TopAppBarButtonUM.Icon(
|
||||
iconRes = CommonUiR.drawable.ic_information_24,
|
||||
iconRes = R.drawable.ic_information_24,
|
||||
onClicked = model::showPermissionInfoDialog,
|
||||
),
|
||||
) {
|
||||
GiveApprovalContent(
|
||||
currency = currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = uiState.approveType,
|
||||
approveItems = uiState.approveItems,
|
||||
uiState = uiState,
|
||||
onChangeApproveType = model::onChangeApproveType,
|
||||
walletInteractionIcon = uiState.walletInteractionIcon,
|
||||
isApproveEnabled = uiState.isApproveButtonEnabled,
|
||||
isApproveLoading = uiState.isApproveLoading,
|
||||
isHoldToConfirm = uiState.isHoldToConfirm,
|
||||
onApproveClick = model::onApproveClick,
|
||||
onCancelClick = model::onCancelClick,
|
||||
onOpenLearnMoreAboutApproveClick = model::onOpenLearnMoreAboutApproveClick,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import arrow.core.Either
|
|||
import arrow.core.getOrElse
|
||||
import arrow.core.left
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionSender.MultipleTransactionSendMode
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
|
|
@ -22,8 +23,10 @@ import com.tangem.core.ui.message.DialogMessage
|
|||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.transaction.error.GetFeeError
|
||||
import com.tangem.domain.transaction.models.AllowanceInfo
|
||||
import com.tangem.domain.transaction.models.TransactionFeeExtended
|
||||
import com.tangem.domain.transaction.usecase.CreateApprovalTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetAllowanceInfoUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.transaction.usecase.gasless.CreateAndSendGaslessTransactionUseCase
|
||||
|
|
@ -32,6 +35,7 @@ import com.tangem.domain.transaction.usecase.gasless.GetFeeForTokenUseCase
|
|||
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
||||
import com.tangem.features.approval.api.GiveApprovalComponent
|
||||
import com.tangem.features.send.v2.api.callbacks.FeeSelectorModelCallback
|
||||
import com.tangem.features.send.v2.api.entity.FeeItem
|
||||
import com.tangem.features.send.v2.api.entity.FeeSelectorUM
|
||||
import com.tangem.utils.TangemBlogUrlBuilder.RESOURCE_TO_LEARN_ABOUT_APPROVING_IN_SWAP
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -41,15 +45,17 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import javax.inject.Inject
|
||||
|
||||
@Stable
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
@Suppress("LongParameterList", "LargeClass")
|
||||
internal class GiveApprovalModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
paramsContainer: ParamsContainer,
|
||||
private val createApprovalTransactionUseCase: CreateApprovalTransactionUseCase,
|
||||
private val getAllowanceInfoUseCase: GetAllowanceInfoUseCase,
|
||||
private val sendTransactionUseCase: SendTransactionUseCase,
|
||||
private val getFeeUseCase: GetFeeUseCase,
|
||||
private val getFeeForGaslessUseCase: GetFeeForGaslessUseCase,
|
||||
|
|
@ -77,11 +83,14 @@ internal class GiveApprovalModel @Inject constructor(
|
|||
isApproveButtonEnabled = false,
|
||||
isApproveLoading = false,
|
||||
isHoldToConfirm = params.isHoldToConfirm,
|
||||
isResetApproval = params.isResetApproval,
|
||||
),
|
||||
)
|
||||
|
||||
private var feeSelectorUM: FeeSelectorUM = FeeSelectorUM.Loading
|
||||
|
||||
private var approvalTxList: Map<TransactionData.Uncompiled, TransactionFee> = emptyMap()
|
||||
|
||||
override fun onFeeResult(feeSelectorUM: FeeSelectorUM) {
|
||||
this.feeSelectorUM = feeSelectorUM
|
||||
uiState.update { it.copy(isApproveButtonEnabled = feeSelectorUM.isPrimaryButtonEnabled) }
|
||||
|
|
@ -122,83 +131,127 @@ internal class GiveApprovalModel @Inject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun prepareApprovalTransaction(): Either<Throwable, TransactionData> {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||
?: return Either.Left(IllegalStateException("Currency is not a token"))
|
||||
|
||||
return createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = params.userWalletId,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
return onApprovalTx(
|
||||
onApprove = { approve ->
|
||||
getFeeUseCase(
|
||||
transactionData = approve,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
).onRight { fee ->
|
||||
approvalTxList = mapOf(approve to fee)
|
||||
}
|
||||
},
|
||||
onResetApprove = { (revokeApproval, approve) ->
|
||||
getFeeUseCase(
|
||||
transactionData = revokeApproval,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
).map { revokeFee ->
|
||||
estimateFeeForResetApproval(
|
||||
revokeTransactionFee = revokeFee,
|
||||
revokeApprovalTransaction = revokeApproval,
|
||||
approvalTransaction = approve,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadFee(): Either<GetFeeError, TransactionFee> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
|
||||
return getFeeUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
fun shouldDisableCustomFee(): Boolean {
|
||||
return approvalTxList.size > 1
|
||||
}
|
||||
|
||||
suspend fun loadFeeExtended(maybeToken: CryptoCurrencyStatus?): Either<GetFeeError, TransactionFeeExtended> {
|
||||
val approvalTransaction = prepareApprovalTransaction()
|
||||
.getOrElse { return GetFeeError.DataError(it).left() }
|
||||
val approve = createApprovalTransactionUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = (params.cryptoCurrencyStatus.currency as CryptoCurrency.Token).contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to create approveTransaction", error)
|
||||
return GetFeeError.DataError(error).left()
|
||||
}
|
||||
|
||||
return if (maybeToken == null) {
|
||||
getFeeForGaslessUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
transactionData = approve,
|
||||
userWallet = userWallet,
|
||||
network = params.cryptoCurrencyStatus.currency.network,
|
||||
)
|
||||
} else {
|
||||
getFeeForTokenUseCase(
|
||||
transactionData = approvalTransaction,
|
||||
transactionData = approve,
|
||||
userWallet = userWallet,
|
||||
token = maybeToken.currency,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun estimateFeeForResetApproval(
|
||||
revokeTransactionFee: TransactionFee,
|
||||
revokeApprovalTransaction: TransactionData.Uncompiled,
|
||||
approvalTransaction: TransactionData.Uncompiled,
|
||||
) = when (revokeTransactionFee) {
|
||||
is TransactionFee.Choosable -> {
|
||||
val approveFee = revokeTransactionFee.copy(
|
||||
minimum = revokeTransactionFee.minimum.increaseEthereumGasLimitBy(2.toBigDecimal()),
|
||||
normal = revokeTransactionFee.normal.increaseEthereumGasLimitBy(2.toBigDecimal()),
|
||||
priority = revokeTransactionFee.priority.increaseEthereumGasLimitBy(2.toBigDecimal()),
|
||||
)
|
||||
approvalTxList = mapOf(
|
||||
revokeApprovalTransaction to revokeTransactionFee,
|
||||
approvalTransaction to approveFee,
|
||||
)
|
||||
revokeTransactionFee.copy(
|
||||
minimum = approveFee.minimum + revokeTransactionFee.minimum,
|
||||
normal = approveFee.normal + revokeTransactionFee.normal,
|
||||
priority = approveFee.priority + revokeTransactionFee.priority,
|
||||
)
|
||||
}
|
||||
is TransactionFee.Single -> {
|
||||
val approveFee = revokeTransactionFee.copy(normal = revokeTransactionFee.normal)
|
||||
approvalTxList = mapOf(
|
||||
revokeApprovalTransaction to revokeTransactionFee,
|
||||
approvalTransaction to approveFee,
|
||||
)
|
||||
revokeTransactionFee.copy(normal = approveFee.normal + revokeTransactionFee.normal)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun sendApprovalTransaction(): Boolean {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return false
|
||||
|
||||
val feeContent = feeSelectorUM as? FeeSelectorUM.Content ?: return false
|
||||
val selectedFee = feeContent.selectedFeeItem.fee
|
||||
val feeExtended = feeContent.feeExtraInfo.transactionFeeExtended
|
||||
|
||||
val isFeeInTokenCurrency = feeExtended?.transactionFee?.normal is Fee.Ethereum.TokenCurrency
|
||||
|
||||
val transactionData = createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
userWalletId = params.userWalletId,
|
||||
amount = getApprovalAmount(),
|
||||
fee = selectedFee,
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to create approval transaction", error)
|
||||
return false
|
||||
val transactions = approvalTxList.map { (tx, fee) ->
|
||||
tx.copy(
|
||||
fee = when (feeContent.selectedFeeItem) {
|
||||
is FeeItem.Fast -> (fee as? TransactionFee.Choosable)?.priority ?: fee.normal
|
||||
is FeeItem.Market -> fee.normal
|
||||
is FeeItem.Slow -> (fee as? TransactionFee.Choosable)?.minimum ?: fee.normal
|
||||
else -> feeContent.selectedFeeItem.fee
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return if (isFeeInTokenCurrency) {
|
||||
createAndSendGaslessTransactionUseCase(
|
||||
userWallet = userWallet,
|
||||
transactionData = transactionData,
|
||||
transactionData = transactions.first(),
|
||||
fee = feeExtended,
|
||||
)
|
||||
} else {
|
||||
sendTransactionUseCase(
|
||||
txData = transactionData,
|
||||
txsData = transactions,
|
||||
userWallet = userWallet,
|
||||
network = tokenCurrency.network,
|
||||
sendMode = MultipleTransactionSendMode.DEFAULT,
|
||||
)
|
||||
}.fold(
|
||||
ifLeft = { error ->
|
||||
|
|
@ -241,4 +294,88 @@ internal class GiveApprovalModel @Inject constructor(
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> onApprovalTx(
|
||||
onApprove: suspend (TransactionData.Uncompiled) -> Either<GetFeeError, T>,
|
||||
onResetApprove:
|
||||
suspend (Pair<TransactionData.Uncompiled, TransactionData.Uncompiled>) -> Either<GetFeeError, T>,
|
||||
): Either<GetFeeError, T> {
|
||||
val cryptoCurrencyStatus = params.cryptoCurrencyStatus
|
||||
val tokenCurrency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token
|
||||
?: return GetFeeError.DataError(IllegalStateException("Currency is not a token")).left()
|
||||
|
||||
val amount = params.amount.toBigDecimalOrNull()
|
||||
?: return GetFeeError.DataError(IllegalArgumentException("Invalid amount format")).left()
|
||||
|
||||
val allowance = getAllowanceInfoUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrency = cryptoCurrencyStatus.currency,
|
||||
spenderAddress = params.spenderAddress,
|
||||
requiredAmount = amount,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to get allowance info", error)
|
||||
return GetFeeError.DataError(error).left()
|
||||
}
|
||||
|
||||
val approve = createApprovalTransactionUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amount = getApprovalAmount(),
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to create approveTransaction", error)
|
||||
return GetFeeError.DataError(error).left()
|
||||
}
|
||||
|
||||
return if (allowance is AllowanceInfo.ResetNeeded) {
|
||||
val revokeApproval = createApprovalTransactionUseCase(
|
||||
userWalletId = params.userWalletId,
|
||||
cryptoCurrencyStatus = cryptoCurrencyStatus,
|
||||
amount = BigDecimal.ZERO,
|
||||
contractAddress = tokenCurrency.contractAddress,
|
||||
spenderAddress = params.spenderAddress,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to create revoke approveTransaction", error)
|
||||
return GetFeeError.DataError(error).left()
|
||||
}
|
||||
onResetApprove(revokeApproval to approve)
|
||||
} else {
|
||||
onApprove(approve)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Fee.increaseEthereumGasLimitBy(multiplier: BigDecimal): Fee {
|
||||
if (this !is Fee.Ethereum) return this
|
||||
val increasedGasPrice = amount.value?.movePointRight(amount.decimals)
|
||||
?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP)
|
||||
val increasedGasLimit = gasLimit
|
||||
.multiply(multiplier.toBigInteger())
|
||||
val increasedAmount = amount.copy(
|
||||
value = increasedGasPrice?.multiply(
|
||||
increasedGasLimit.toBigDecimal().movePointLeft(amount.decimals),
|
||||
),
|
||||
)
|
||||
return when (this) {
|
||||
is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
|
||||
is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
|
||||
is Fee.Ethereum.TokenCurrency -> error("handle in [REDACTED_TASK_KEY]")
|
||||
}
|
||||
}
|
||||
|
||||
private operator fun Fee.plus(otherFee: Fee): Fee {
|
||||
if (this !is Fee.Ethereum || otherFee !is Fee.Ethereum) return this
|
||||
val gasLimit = this.gasLimit
|
||||
val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals)
|
||||
?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP)
|
||||
val increasedGasLimit = gasLimit + otherFee.gasLimit
|
||||
val increasedAmount = this.amount.copy(
|
||||
value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals),
|
||||
)
|
||||
return when (this) {
|
||||
is Fee.Ethereum.EIP1559 -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
|
||||
is Fee.Ethereum.Legacy -> copy(amount = increasedAmount, gasLimit = increasedGasLimit)
|
||||
is Fee.Ethereum.TokenCurrency -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,4 +12,5 @@ internal data class GiveApprovalUM(
|
|||
val isApproveButtonEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
val isHoldToConfirm: Boolean = false,
|
||||
val isResetApproval: Boolean = false,
|
||||
)
|
||||
|
|
@ -27,28 +27,24 @@ import androidx.compose.ui.unit.DpOffset
|
|||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.window.PopupProperties
|
||||
import com.tangem.common.ui.bottomsheet.permission.state.ApproveType
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.*
|
||||
import com.tangem.core.ui.components.containers.FooterContainer
|
||||
import com.tangem.core.ui.extensions.*
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.features.approval.impl.model.GiveApprovalUM
|
||||
import com.tangem.features.send.v2.api.FeeSelectorBlockComponent
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import com.tangem.common.ui.R as CommonUiR
|
||||
|
||||
@Composable
|
||||
@Suppress("LongParameterList")
|
||||
internal fun GiveApprovalContent(
|
||||
currency: String,
|
||||
uiState: GiveApprovalUM,
|
||||
subtitle: TextReference,
|
||||
approveType: ApproveType,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
walletInteractionIcon: Int?,
|
||||
isApproveEnabled: Boolean,
|
||||
isApproveLoading: Boolean,
|
||||
isHoldToConfirm: Boolean,
|
||||
onApproveClick: () -> Unit,
|
||||
onCancelClick: () -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
|
|
@ -73,20 +69,21 @@ internal fun GiveApprovalContent(
|
|||
|
||||
ApprovalInfo(
|
||||
currency = currency,
|
||||
approveType = approveType,
|
||||
approveItems = approveItems,
|
||||
approveType = uiState.approveType,
|
||||
approveItems = uiState.approveItems,
|
||||
onChangeApproveType = onChangeApproveType,
|
||||
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
|
||||
feeSelectorBlockComponent = feeSelectorBlockComponent,
|
||||
isResetApproval = uiState.isResetApproval,
|
||||
)
|
||||
|
||||
SpacerH(height = TangemTheme.dimens.spacing20)
|
||||
|
||||
if (isHoldToConfirm) {
|
||||
if (uiState.isHoldToConfirm) {
|
||||
HoldToConfirmButton(
|
||||
text = stringResourceSafe(id = CommonUiR.string.common_approve),
|
||||
enabled = isApproveEnabled,
|
||||
isLoading = isApproveLoading,
|
||||
text = stringResourceSafe(id = R.string.common_approve),
|
||||
enabled = uiState.isApproveButtonEnabled,
|
||||
isLoading = uiState.isApproveLoading,
|
||||
onConfirm = onApproveClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -94,21 +91,21 @@ internal fun GiveApprovalContent(
|
|||
)
|
||||
} else {
|
||||
PrimaryButtonIconEnd(
|
||||
text = stringResourceSafe(id = CommonUiR.string.common_approve),
|
||||
iconResId = walletInteractionIcon,
|
||||
showProgress = isApproveLoading,
|
||||
text = stringResourceSafe(id = R.string.common_approve),
|
||||
iconResId = uiState.walletInteractionIcon,
|
||||
showProgress = uiState.isApproveLoading,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
onClick = onApproveClick,
|
||||
enabled = isApproveEnabled,
|
||||
enabled = uiState.isApproveButtonEnabled,
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH12()
|
||||
|
||||
SecondaryButton(
|
||||
text = stringResourceSafe(id = CommonUiR.string.common_cancel),
|
||||
text = stringResourceSafe(id = R.string.common_cancel),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
|
|
@ -124,6 +121,7 @@ internal fun GiveApprovalContent(
|
|||
private fun ApprovalInfo(
|
||||
currency: String,
|
||||
approveType: ApproveType,
|
||||
isResetApproval: Boolean,
|
||||
approveItems: ImmutableList<ApproveType>,
|
||||
onChangeApproveType: (ApproveType) -> Unit,
|
||||
onOpenLearnMoreAboutApproveClick: () -> Unit,
|
||||
|
|
@ -131,7 +129,7 @@ private fun ApprovalInfo(
|
|||
) {
|
||||
FooterContainer(
|
||||
footer = annotatedReference {
|
||||
append(stringResourceSafe(CommonUiR.string.swap_approve_description))
|
||||
append(stringResourceSafe(R.string.swap_approve_description))
|
||||
append(" ")
|
||||
withLink(
|
||||
link = LinkAnnotation.Clickable(
|
||||
|
|
@ -140,7 +138,7 @@ private fun ApprovalInfo(
|
|||
),
|
||||
block = {
|
||||
appendColored(
|
||||
text = stringResourceSafe(CommonUiR.string.common_learn_more),
|
||||
text = stringResourceSafe(R.string.common_learn_more),
|
||||
color = TangemTheme.colors.text.accent,
|
||||
)
|
||||
},
|
||||
|
|
@ -157,7 +155,13 @@ private fun ApprovalInfo(
|
|||
}
|
||||
SpacerH16()
|
||||
FooterContainer(
|
||||
footer = resourceReference(CommonUiR.string.give_permission_policy_type_footer),
|
||||
footer = resourceReference(
|
||||
if (isResetApproval) {
|
||||
R.string.update_approval_permission_fee_note
|
||||
} else {
|
||||
R.string.give_permission_policy_type_footer
|
||||
},
|
||||
),
|
||||
modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
feeSelectorBlockComponent.Content(
|
||||
|
|
@ -202,7 +206,7 @@ private fun AmountItem(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResourceSafe(id = CommonUiR.string.give_permission_rows_amount, currency),
|
||||
text = stringResourceSafe(id = R.string.give_permission_rows_amount, currency),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
maxLines = 1,
|
||||
|
|
@ -215,7 +219,7 @@ private fun AmountItem(
|
|||
maxLines = 1,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(id = CommonUiR.drawable.ic_chevron_24)),
|
||||
painter = rememberVectorPainter(ImageVector.vectorResource(id = R.drawable.ic_chevron_24)),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing2),
|
||||
|
|
@ -275,10 +279,10 @@ private fun DropdownSelector(
|
|||
Text(
|
||||
text = when (item) {
|
||||
ApproveType.LIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_current_transaction,
|
||||
id = R.string.give_permission_current_transaction,
|
||||
)
|
||||
ApproveType.UNLIMITED -> stringResourceSafe(
|
||||
id = CommonUiR.string.give_permission_unlimited,
|
||||
id = R.string.give_permission_unlimited,
|
||||
)
|
||||
},
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
|
|
@ -288,7 +292,7 @@ private fun DropdownSelector(
|
|||
SpacerWMax()
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(id = CommonUiR.drawable.ic_check_24),
|
||||
image = ImageVector.vectorResource(id = R.drawable.ic_check_24),
|
||||
),
|
||||
tint = color,
|
||||
contentDescription = null,
|
||||
|
|
@ -316,13 +320,8 @@ private fun GiveApprovalContentPreview(
|
|||
GiveApprovalContent(
|
||||
currency = params.currency,
|
||||
subtitle = params.subtitle,
|
||||
approveType = params.approveType,
|
||||
approveItems = params.approveItems,
|
||||
uiState = params.uiState,
|
||||
onChangeApproveType = {},
|
||||
walletInteractionIcon = params.walletInteractionIcon,
|
||||
isApproveEnabled = params.isApproveEnabled,
|
||||
isApproveLoading = params.isApproveLoading,
|
||||
isHoldToConfirm = false,
|
||||
onApproveClick = {},
|
||||
onCancelClick = {},
|
||||
onOpenLearnMoreAboutApproveClick = {},
|
||||
|
|
@ -334,11 +333,7 @@ private fun GiveApprovalContentPreview(
|
|||
private data class GiveApprovalPreviewParams(
|
||||
val currency: String,
|
||||
val subtitle: TextReference,
|
||||
val approveType: ApproveType,
|
||||
val approveItems: ImmutableList<ApproveType>,
|
||||
val walletInteractionIcon: Int?,
|
||||
val isApproveEnabled: Boolean,
|
||||
val isApproveLoading: Boolean,
|
||||
val uiState: GiveApprovalUM,
|
||||
)
|
||||
|
||||
private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<GiveApprovalPreviewParams> {
|
||||
|
|
@ -347,20 +342,36 @@ private class GiveApprovalContentPreviewProvider : PreviewParameterProvider<Give
|
|||
GiveApprovalPreviewParams(
|
||||
currency = "USDT",
|
||||
subtitle = stringReference("Allow this app to access your USDT"),
|
||||
approveType = ApproveType.LIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
|
||||
isApproveEnabled = true,
|
||||
isApproveLoading = false,
|
||||
uiState = GiveApprovalUM(
|
||||
approveType = ApproveType.LIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = R.drawable.ic_tangem_24,
|
||||
isApproveButtonEnabled = true,
|
||||
isApproveLoading = false,
|
||||
),
|
||||
),
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDC",
|
||||
subtitle = stringReference("Allow this app to access your USDC"),
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = CommonUiR.drawable.ic_tangem_24,
|
||||
isApproveEnabled = false,
|
||||
isApproveLoading = true,
|
||||
uiState = GiveApprovalUM(
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = R.drawable.ic_tangem_24,
|
||||
isApproveButtonEnabled = false,
|
||||
isApproveLoading = true,
|
||||
),
|
||||
),
|
||||
GiveApprovalPreviewParams(
|
||||
currency = "USDC",
|
||||
subtitle = stringReference("Allow this app to access your USDC"),
|
||||
uiState = GiveApprovalUM(
|
||||
approveType = ApproveType.UNLIMITED,
|
||||
approveItems = persistentListOf(ApproveType.LIMITED, ApproveType.UNLIMITED),
|
||||
walletInteractionIcon = R.drawable.ic_tangem_24,
|
||||
isApproveButtonEnabled = true,
|
||||
isApproveLoading = false,
|
||||
isResetApproval = true,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ sealed class FeeSelectorParams {
|
|||
abstract val userWalletId: UserWalletId
|
||||
abstract val onLoadFeeExtended: (suspend (CryptoCurrencyStatus?) -> Either<GetFeeError, TransactionFeeExtended>)?
|
||||
abstract val onLoadFee: suspend () -> Either<GetFeeError, TransactionFee>
|
||||
abstract val onDisableCustomFee: () -> Boolean
|
||||
abstract val cryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val feeCryptoCurrencyStatus: CryptoCurrencyStatus
|
||||
abstract val feeStateConfiguration: FeeStateConfiguration
|
||||
|
|
@ -39,6 +40,7 @@ sealed class FeeSelectorParams {
|
|||
override val analyticsCategoryName: String,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val shouldShowOnlySpeedOption: Boolean = false,
|
||||
override val onDisableCustomFee: () -> Boolean = { false },
|
||||
val bottomSheetShown: (Boolean) -> Unit = {},
|
||||
) : FeeSelectorParams()
|
||||
|
||||
|
|
@ -56,6 +58,7 @@ sealed class FeeSelectorParams {
|
|||
override val analyticsCategoryName: String,
|
||||
override val analyticsSendSource: CommonSendAnalyticEvents.CommonSendSource,
|
||||
override val shouldShowOnlySpeedOption: Boolean = false,
|
||||
override val onDisableCustomFee: () -> Boolean = { false },
|
||||
val callback: FeeSelectorModelCallback,
|
||||
) : FeeSelectorParams()
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ internal class DefaultFeeSelectorBlockComponent @AssistedInject constructor(
|
|||
params = FeeSelectorParams.FeeSelectorDetailsParams(
|
||||
state = model.uiState.value,
|
||||
onLoadFee = params.onLoadFee,
|
||||
onDisableCustomFee = params.onDisableCustomFee,
|
||||
onLoadFeeExtended = params.onLoadFeeExtended,
|
||||
feeCryptoCurrencyStatus = params.feeCryptoCurrencyStatus,
|
||||
cryptoCurrencyStatus = params.cryptoCurrencyStatus,
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ internal class FeeSelectorLogic @AssistedInject constructor(
|
|||
feeStateConfiguration = params.feeStateConfiguration,
|
||||
isFeeApproximate = isFeeApproximate(fee.transactionFee.normal.amount.type),
|
||||
feeSelectorIntents = this@FeeSelectorLogic,
|
||||
shouldDisableCustomFee = params.onDisableCustomFee(),
|
||||
),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ internal class FeeItemConverter(
|
|||
private val feeSelectorIntents: FeeSelectorIntents,
|
||||
private val appCurrency: AppCurrency,
|
||||
cryptoCurrencyStatus: CryptoCurrencyStatus,
|
||||
private val shouldDisableCustomFee: Boolean,
|
||||
) : Converter<FeeItemConverter.Input, ImmutableList<FeeItem>> {
|
||||
|
||||
private val customFeeFieldConverter = FeeSelectorCustomFieldConverter(
|
||||
|
|
@ -54,8 +55,10 @@ internal class FeeItemConverter(
|
|||
add(FeeItem.Market(fee = value.transactionFee.normal))
|
||||
}
|
||||
}
|
||||
val customFee = value.customFee ?: constructCustomFee()
|
||||
customFee?.let(::add)
|
||||
if (!shouldDisableCustomFee) {
|
||||
val customFee = value.customFee ?: constructCustomFee()
|
||||
customFee?.let(::add)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableList<FeeItem>.addFeeItemsLimited(value: Input) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ internal class FeeSelectorLoadedTransformer(
|
|||
private val feeStateConfiguration: FeeSelectorParams.FeeStateConfiguration,
|
||||
private val isFeeApproximate: Boolean,
|
||||
private val feeSelectorIntents: FeeSelectorIntents,
|
||||
private val shouldDisableCustomFee: Boolean,
|
||||
) : Transformer<FeeSelectorUM> {
|
||||
|
||||
private val feeItemsConverter = FeeItemConverter(
|
||||
|
|
@ -30,6 +31,7 @@ internal class FeeSelectorLoadedTransformer(
|
|||
feeSelectorIntents = feeSelectorIntents,
|
||||
appCurrency = appCurrency,
|
||||
cryptoCurrencyStatus = feeCryptoCurrencyStatus,
|
||||
shouldDisableCustomFee = shouldDisableCustomFee,
|
||||
)
|
||||
|
||||
override fun transform(prevState: FeeSelectorUM): FeeSelectorUM {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ internal class ShowApprovalBottomSheetTransformer(
|
|||
footerText = resourceReference(R.string.staking_give_permission_fee_footer),
|
||||
onChangeApproveType = prevState.clickIntents::onApproveTypeChange,
|
||||
onOpenLearnMoreAboutApproveClick = {},
|
||||
isResetApproval = false,
|
||||
),
|
||||
walletInteractionIcon = walletInterationIcon(userWallet),
|
||||
onCancel = onDismiss,
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import com.tangem.blockchain.common.Amount
|
|||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.TransactionData
|
||||
import com.tangem.blockchain.common.TransactionExtras
|
||||
import com.tangem.blockchain.common.smartcontract.SmartContractCallDataProviderFactory
|
||||
import com.tangem.blockchain.common.transaction.Fee
|
||||
import com.tangem.blockchain.common.transaction.TransactionFee
|
||||
import com.tangem.blockchain.yieldsupply.providers.ethereum.yield.EthereumYieldSupplySendCallData
|
||||
|
|
@ -2025,35 +2024,31 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
// setting up amount for approve with given amount for swap [SwapApproveType.Limited]
|
||||
val callData = SmartContractCallDataProviderFactory.getApprovalCallData(
|
||||
spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" },
|
||||
amount = swapAmount.value.convertToSdkAmount(fromTokenStatus),
|
||||
blockchain = fromToken.network.toBlockchain(),
|
||||
)
|
||||
val feeData = try {
|
||||
val extras = createTransactionExtrasUseCase(
|
||||
callData = callData,
|
||||
network = fromToken.network,
|
||||
).getOrNull() ?: error("unable to create extras")
|
||||
val fromAddress = requireNotNull(
|
||||
fromTokenStatus.value.networkAddress?.defaultAddress?.value,
|
||||
) { "networkAddress cant be null" }
|
||||
|
||||
val fromAddress = requireNotNull(
|
||||
fromTokenStatus.value.networkAddress?.defaultAddress?.value,
|
||||
) { "networkAddress cant be null" }
|
||||
val transactionData = TransactionData.Uncompiled(
|
||||
amount = createNativeAmountForDex("0", fromToken.network),
|
||||
destinationAddress = fromToken.getContractAddress(),
|
||||
fee = null,
|
||||
sourceAddress = fromAddress,
|
||||
extras = extras,
|
||||
)
|
||||
getFeeUseCase(
|
||||
transactionData = transactionData,
|
||||
network = fromToken.network,
|
||||
userWallet = userWallet,
|
||||
).getOrNull() ?: error("unable to calculate fee")
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Failed to get fee", e)
|
||||
// it's impossible next steps without fee
|
||||
val allowanceInfo = getAllowanceInfoUseCase(
|
||||
userWalletId = userWalletId,
|
||||
cryptoCurrency = fromToken,
|
||||
spenderAddress = requireNotNull(spenderAddress) { "spenderAddress cant be null" },
|
||||
requiredAmount = swapAmount.value,
|
||||
).getOrNull()
|
||||
|
||||
val amount = if (allowanceInfo is AllowanceInfo.ResetNeeded) {
|
||||
BigDecimal.ZERO
|
||||
} else {
|
||||
swapAmount.value
|
||||
}
|
||||
|
||||
val approveTransaction = createApprovalTransactionUseCase(
|
||||
cryptoCurrencyStatus = fromTokenStatus,
|
||||
userWalletId = userWalletId,
|
||||
amount = amount,
|
||||
contractAddress = fromToken.getContractAddress(),
|
||||
spenderAddress = spenderAddress,
|
||||
).getOrElse { error ->
|
||||
TangemLogger.e("Failed to create approveTransaction", error)
|
||||
return createSwapErrorWith(
|
||||
fromToken = fromTokenStatus,
|
||||
fromAccount = fromAccount,
|
||||
|
|
@ -2063,6 +2058,12 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
)
|
||||
}
|
||||
|
||||
val feeData = getFeeUseCase(
|
||||
transactionData = approveTransaction,
|
||||
network = fromToken.network,
|
||||
userWallet = userWallet,
|
||||
).getOrNull() ?: error("unable to calculate fee")
|
||||
|
||||
val feeState = feeData
|
||||
.patchTransactionFeeForSwap(INCREASE_GAS_LIMIT_FOR_DEX)
|
||||
.toTxFeeState(fromToken, null)
|
||||
|
|
@ -2084,6 +2085,7 @@ internal class SwapInteractorImpl @AssistedInject constructor(
|
|||
amount = INFINITY_SYMBOL,
|
||||
walletAddress = getWalletAddress(fromToken.network),
|
||||
spenderAddress = getTokenAddress(fromToken),
|
||||
isResetApproval = allowanceInfo is AllowanceInfo.ResetNeeded,
|
||||
requestApproveData = RequestApproveStateData(
|
||||
fee = feeState,
|
||||
fromTokenAmount = swapAmount,
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ sealed class PermissionDataState {
|
|||
val walletAddress: String,
|
||||
val spenderAddress: String,
|
||||
val requestApproveData: RequestApproveStateData,
|
||||
val isResetApproval: Boolean,
|
||||
) : PermissionDataState()
|
||||
|
||||
object PermissionFailed : PermissionDataState()
|
||||
|
|
|
|||
|
|
@ -206,10 +206,15 @@ internal class DefaultSwapComponent @AssistedInject constructor(
|
|||
feeCryptoCurrencyStatus = feeCryptoCurrency,
|
||||
amount = model.dataState.amount.orEmpty(),
|
||||
spenderAddress = requireNotNull(model.dataState.approveDataModel).spenderAddress,
|
||||
subtitle = resourceReference(
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
formatArgs = wrappedList(providerName, permissionState.currency),
|
||||
),
|
||||
subtitle = if (permissionState.isResetApproval) {
|
||||
resourceReference(R.string.update_approval_permission_subtitle)
|
||||
} else {
|
||||
resourceReference(
|
||||
id = R.string.give_permission_swap_subtitle,
|
||||
formatArgs = wrappedList(providerName, permissionState.currency),
|
||||
)
|
||||
},
|
||||
isResetApproval = permissionState.isResetApproval,
|
||||
isHoldToConfirm = model.isHoldToConfirmEnabled,
|
||||
callback = model.approvalCallback,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -975,6 +975,7 @@ internal class StateBuilder(
|
|||
dialogText = resourceReference(R.string.swapping_approve_information_text),
|
||||
footerText = resourceReference(R.string.swap_give_permission_fee_footer),
|
||||
onOpenLearnMoreAboutApproveClick = onOpenLearnMoreAboutApproveClick,
|
||||
isResetApproval = permissionDataState.isResetApproval,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue