Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-19 13:19:08 +03:00
parent 93db763fe8
commit 43a7201ac8
11 changed files with 223 additions and 72 deletions

View file

@ -369,11 +369,15 @@
<string name="swapping_pending_transaction_subtitle">Транзакция в процессе…</string>
<string name="swapping_pending_transaction_title">Подождите</string>
<string name="swapping_permission_buttons_approve">Подтвердить</string>
<string name="swapping_permission_current_transaction">Текущая транзакция</string>
<string name="swapping_permission_fee_footer">Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена.</string>
<string name="swapping_permission_header">Дать разрешение</string>
<string name="swapping_permission_policy_type_footer">Укажите лимит доступа к выбранному токену</string>
<string name="swapping_permission_rows_amount">Количество %s</string>
<string name="swapping_permission_rows_spender">Отправитель</string>
<string name="swapping_permission_rows_your_wallet">Ваш кошелек</string>
<string name="swapping_permission_subheader">Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s</string>
<string name="swapping_permission_unlimited">Безлимитно</string>
<string name="swapping_success_view_explorer_button_title">Открыть в обозревателе</string>
<string name="swapping_success_view_title">В процессе</string>
<string name="swapping_swap">Обмен</string>

View file

@ -365,11 +365,15 @@
<string name="swapping_pending_transaction_subtitle">Transaction in progress...</string>
<string name="swapping_pending_transaction_title">Waiting</string>
<string name="swapping_permission_buttons_approve">Approve</string>
<string name="swapping_permission_current_transaction">Current transaction</string>
<string name="swapping_permission_fee_footer">The token approval network fee will be charged to confirm that you are the one allowing your token to be used for the exchange.</string>
<string name="swapping_permission_header">Give Permission</string>
<string name="swapping_permission_policy_type_footer">Specify the approve limit for the selected token</string>
<string name="swapping_permission_rows_amount">Amount %s</string>
<string name="swapping_permission_rows_spender">Spender</string>
<string name="swapping_permission_rows_your_wallet">Your Wallet</string>
<string name="swapping_permission_subheader">To continue you need to allow 1inch smart contracts to use your %s</string>
<string name="swapping_permission_unlimited">Unlimited</string>
<string name="swapping_success_view_explorer_button_title">View in Explorer</string>
<string name="swapping_success_view_title">In progress</string>
<string name="swapping_swap">Swap</string>

View file

@ -2,12 +2,8 @@ package com.tangem.feature.swap.domain
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TokensDataState
import com.tangem.feature.swap.domain.models.ui.TxState
import com.tangem.feature.swap.domain.models.domain.SwapApproveType
import com.tangem.feature.swap.domain.models.ui.*
interface SwapInteractor {
@ -46,12 +42,16 @@ interface SwapInteractor {
* @param networkId network in which selected token
* @param approveData tx data to give approve, it loaded from 1inch in findBestQuote if needed
* @param forTokenContractAddress token contract address for which needs permission
* @param fromToken which token will be swapping
* @param approveType unlimited or tx amount approve
*/
@Throws(IllegalStateException::class)
suspend fun givePermissionToSwap(
networkId: String,
approveData: RequestApproveStateData,
forTokenContractAddress: String,
fromToken: Currency,
approveType: SwapApproveType,
): TxState
/**

View file

@ -6,19 +6,9 @@ import com.tangem.feature.swap.domain.models.DataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState
import com.tangem.feature.swap.domain.models.domain.SwapApproveType
import com.tangem.feature.swap.domain.models.toStringWithRightOffset
import com.tangem.feature.swap.domain.models.ui.AmountFormatter
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.PreselectTokens
import com.tangem.feature.swap.domain.models.ui.RequestApproveStateData
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TokenBalanceData
import com.tangem.feature.swap.domain.models.ui.TokenSwapInfo
import com.tangem.feature.swap.domain.models.ui.TokenWithBalance
import com.tangem.feature.swap.domain.models.ui.TokensDataState
import com.tangem.feature.swap.domain.models.ui.TxState
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.lib.crypto.TransactionManager
import com.tangem.lib.crypto.UserWalletManager
import com.tangem.lib.crypto.models.ProxyFiatCurrency
@ -120,13 +110,20 @@ internal class SwapInteractorImpl @Inject constructor(
networkId: String,
approveData: RequestApproveStateData,
forTokenContractAddress: String,
fromToken: Currency,
approveType: SwapApproveType,
): TxState {
val dataToSign = if (approveType == SwapApproveType.UNLIMITED) {
repository.dataToApprove(networkId, getTokenAddress(fromToken)).data
} else {
approveData.approveModel.data
}
val result = transactionManager.sendApproveTransaction(
networkId = networkId,
feeAmount = approveData.fee,
gasLimit = approveData.gasLimit,
destinationAddress = approveData.approveModel.toAddress,
dataToSign = approveData.approveModel.data,
dataToSign = dataToSign,
derivationPath = derivationPath,
)
return when (result) {
@ -354,6 +351,7 @@ internal class SwapInteractorImpl @Inject constructor(
val quotesState = updatePermissionState(
networkId = networkId,
fromToken = fromToken,
swapAmount = amount,
quotesLoadedState = swapState,
)
return quotesState.copy(
@ -503,6 +501,7 @@ internal class SwapInteractorImpl @Inject constructor(
private suspend fun updatePermissionState(
networkId: String,
fromToken: Currency,
swapAmount: SwapAmount,
quotesLoadedState: SwapState.QuotesLoadedState,
): SwapState.QuotesLoadedState {
// if token balance ZERO not show permission state to avoid user to spend money for fee
@ -517,7 +516,12 @@ internal class SwapInteractorImpl @Inject constructor(
permissionState = PermissionDataState.PermissionLoading,
)
}
val transactionData = repository.dataToApprove(networkId, getTokenAddress(fromToken))
// setting up amount for approve with given amount for swap [SwapApproveType.Limited]
val transactionData = repository.dataToApprove(
networkId = networkId,
tokenAddress = getTokenAddress(fromToken),
amount = swapAmount.toStringWithRightOffset(),
)
val feeData = transactionManager.getFee(
networkId = networkId,
amountToSend = BigDecimal.ZERO,

View file

@ -0,0 +1,5 @@
package com.tangem.feature.swap.domain.models.domain
enum class SwapApproveType {
LIMITED, UNLIMITED
}

View file

@ -37,6 +37,7 @@ dependencies {
/** Other libraries */
implementation(deps.compose.shimmer)
implementation(deps.kotlin.serialization)
implementation(deps.kotlin.immutable.collections)
implementation(deps.timber)
/** DI */

View file

@ -1,5 +1,9 @@
package com.tangem.feature.swap.models
import com.tangem.feature.swap.domain.models.domain.SwapApproveType
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
sealed class SwapPermissionState {
object InProgress : SwapPermissionState()
@ -12,11 +16,25 @@ sealed class SwapPermissionState {
val walletAddress: String,
val spenderAddress: String,
val fee: String,
val approveType: ApproveType,
val approveItems: ImmutableList<ApproveType> = ApproveType.values().toList().toImmutableList(),
val approveButton: ApprovePermissionButton,
val cancelButton: CancelPermissionButton,
val onChangeApproveType: (ApproveType) -> Unit,
) : SwapPermissionState()
}
enum class ApproveType {
LIMITED, UNLIMITED
}
fun ApproveType.toDomainApproveType(): SwapApproveType {
return when (this) {
ApproveType.LIMITED -> SwapApproveType.LIMITED
ApproveType.UNLIMITED -> SwapApproveType.UNLIMITED
}
}
data class ApprovePermissionButton(
val enabled: Boolean,
val loading: Boolean = false,

View file

@ -13,4 +13,5 @@ data class UiActions(
val onMaxAmountSelected: () -> Unit,
val openPermissionBottomSheet: () -> Unit,
val hidePermissionBottomSheet: () -> Unit,
val onChangeApproveType: (ApproveType) -> Unit,
)

View file

@ -12,18 +12,7 @@ import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.TxState
import com.tangem.feature.swap.models.ApprovePermissionButton
import com.tangem.feature.swap.models.CancelPermissionButton
import com.tangem.feature.swap.models.FeeState
import com.tangem.feature.swap.models.GenericWarningType
import com.tangem.feature.swap.models.SwapButton
import com.tangem.feature.swap.models.SwapCardData
import com.tangem.feature.swap.models.SwapPermissionState
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.SwapSuccessStateHolder
import com.tangem.feature.swap.models.SwapWarning
import com.tangem.feature.swap.models.TransactionCardType
import com.tangem.feature.swap.models.UiActions
import com.tangem.feature.swap.models.*
/**
* State builder creates a specific states for SwapScreen
@ -160,7 +149,12 @@ internal class StateBuilder(val actions: UiActions) {
),
networkCurrency = quoteModel.networkCurrency,
warnings = warnings,
permissionState = convertPermissionState(quoteModel.permissionState, actions.onGivePermissionClick),
permissionState = convertPermissionState(
lastPermissionState = uiStateHolder.permissionState,
permissionDataState = quoteModel.permissionState,
onGivePermissionClick = actions.onGivePermissionClick,
onChangeApproveType = actions.onChangeApproveType,
),
fee = feeState,
swapButton = SwapButton(
enabled = quoteModel.preparedSwapConfigState.isAllowedToSpend &&
@ -234,9 +228,16 @@ internal class StateBuilder(val actions: UiActions) {
}
private fun convertPermissionState(
lastPermissionState: SwapPermissionState,
permissionDataState: PermissionDataState,
onGivePermissionClick: () -> Unit,
onChangeApproveType: (ApproveType) -> Unit,
): SwapPermissionState {
val approveType = if (lastPermissionState is SwapPermissionState.ReadyForRequest) {
lastPermissionState.approveType
} else {
ApproveType.UNLIMITED
}
return when (permissionDataState) {
PermissionDataState.Empty -> SwapPermissionState.Empty
PermissionDataState.PermissionFailed -> SwapPermissionState.Empty
@ -244,6 +245,7 @@ internal class StateBuilder(val actions: UiActions) {
is PermissionDataState.PermissionReadyForRequest -> SwapPermissionState.ReadyForRequest(
currency = permissionDataState.currency,
amount = permissionDataState.amount,
approveType = approveType,
walletAddress = getShortAddressValue(permissionDataState.walletAddress),
spenderAddress = getShortAddressValue(permissionDataState.spenderAddress),
fee = permissionDataState.fee,
@ -254,6 +256,7 @@ internal class StateBuilder(val actions: UiActions) {
cancelButton = CancelPermissionButton(
enabled = true,
),
onChangeApproveType = onChangeApproveType,
)
}
}
@ -269,6 +272,18 @@ internal class StateBuilder(val actions: UiActions) {
)
}
fun updateApproveType(uiState: SwapStateHolder, approveType: ApproveType): SwapStateHolder {
return if (uiState.permissionState is SwapPermissionState.ReadyForRequest) {
uiState.copy(
permissionState = uiState.permissionState.copy(
approveType = approveType,
),
)
} else {
uiState
}
}
fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder {
return uiState.copy(
permissionState = SwapPermissionState.InProgress,

View file

@ -1,11 +1,9 @@
package com.tangem.feature.swap.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -17,9 +15,11 @@ import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.atoms.Hand
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.ApprovePermissionButton
import com.tangem.feature.swap.models.ApproveType
import com.tangem.feature.swap.models.CancelPermissionButton
import com.tangem.feature.swap.models.SwapPermissionState
import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.ImmutableList
@Composable
fun SwapPermissionBottomSheetContent(data: SwapPermissionState.ReadyForRequest, onCancel: () -> Unit) {
@ -108,20 +108,26 @@ private fun ApprovalBottomSheetInfo(data: SwapPermissionState.ReadyForRequest) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth()
.padding(
start = TangemTheme.dimens.spacing12,
end = TangemTheme.dimens.spacing16,
),
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
AmountItem(currency = data.currency, amount = data.amount)
DividerBottomSheet()
WalletAddressItem(walletAddress = data.walletAddress)
DividerBottomSheet()
SpenderItem(spenderAddress = data.spenderAddress)
AmountItem(
currency = data.currency,
approveType = data.approveType,
onChangeApproveType = data.onChangeApproveType,
approveItems = data.approveItems,
)
SubtitleItem(
subtitle = stringResource(id = R.string.swapping_permission_policy_type_footer),
modifier = Modifier.fillMaxWidth(),
)
SpacerH24()
DividerBottomSheet()
FeeItem(fee = data.fee)
SubtitleItem(
subtitle = stringResource(id = R.string.swapping_permission_fee_footer),
modifier = Modifier.fillMaxWidth(),
)
}
}
@ -138,7 +144,7 @@ private fun InformationItem(subtitle: String, value: String) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(TangemTheme.dimens.spacing16),
.padding(vertical = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@ -159,27 +165,95 @@ private fun InformationItem(subtitle: String, value: String) {
}
@Composable
private fun AmountItem(currency: String, amount: String) {
InformationItem(
subtitle = stringResource(id = R.string.swapping_permission_rows_amount, currency),
value = amount,
)
private fun AmountItem(
currency: String,
approveType: ApproveType,
approveItems: ImmutableList<ApproveType>,
onChangeApproveType: (ApproveType) -> Unit,
) {
var isExpandSelector by remember {
mutableStateOf(false)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = TangemTheme.dimens.spacing16),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = stringResource(id = R.string.swapping_permission_rows_amount, currency),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.subtitle1,
maxLines = 1,
)
Box {
SelectorItem(
getTitleForApproveType(approveType = approveType),
) {
isExpandSelector = true
}
DropdownSelector(
isExpanded = isExpandSelector,
onDismiss = { isExpandSelector = false },
onItemClick = { approveType ->
isExpandSelector = false
onChangeApproveType.invoke(approveType)
},
items = approveItems,
)
}
}
}
@Composable
private fun WalletAddressItem(walletAddress: String) {
InformationItem(
subtitle = stringResource(id = R.string.swapping_permission_rows_your_wallet),
value = walletAddress,
)
private fun SelectorItem(title: String, onClick: () -> Unit) {
Row(
modifier = Modifier.clickable { onClick() },
) {
Text(
text = title,
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body1,
maxLines = 1,
)
Icon(
painter = painterResource(id = R.drawable.ic_chevron_24),
tint = TangemTheme.colors.icon.primary1,
contentDescription = null,
)
}
}
@Composable
private fun SpenderItem(spenderAddress: String) {
InformationItem(
subtitle = stringResource(id = R.string.swapping_permission_rows_spender),
value = spenderAddress,
)
private fun DropdownSelector(
isExpanded: Boolean,
onDismiss: () -> Unit,
onItemClick: (ApproveType) -> Unit,
items: ImmutableList<ApproveType>,
) {
DropdownMenu(
expanded = isExpanded,
onDismissRequest = onDismiss,
modifier = Modifier
.wrapContentSize()
.background(TangemTheme.colors.background.secondary),
) {
items.forEach { item ->
DropdownMenuItem(
onClick = {
onItemClick.invoke(item)
},
) {
Text(
text = getTitleForApproveType(approveType = item),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.body1,
maxLines = 1,
)
}
}
}
}
@Composable
@ -190,6 +264,22 @@ private fun FeeItem(fee: String) {
)
}
@Composable
private fun SubtitleItem(subtitle: String, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = subtitle,
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
)
}
@Composable
private fun getTitleForApproveType(approveType: ApproveType): String = when (approveType) {
ApproveType.LIMITED -> stringResource(id = R.string.swapping_permission_current_transaction)
ApproveType.UNLIMITED -> stringResource(id = R.string.swapping_permission_unlimited)
}
// region preview
@Preview
@ -214,8 +304,10 @@ private val previewData = SwapPermissionState.ReadyForRequest(
walletAddress = "",
spenderAddress = "",
fee = "2,14$",
approveType = ApproveType.UNLIMITED,
approveButton = ApprovePermissionButton(true) {},
cancelButton = CancelPermissionButton(true),
onChangeApproveType = { ApproveType.UNLIMITED },
)
//endregion preview

View file

@ -13,13 +13,11 @@ import com.tangem.feature.swap.domain.BlockchainInteractor
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.feature.swap.domain.models.domain.Currency
import com.tangem.feature.swap.domain.models.formatToUIRepresentation
import com.tangem.feature.swap.domain.models.ui.FoundTokensState
import com.tangem.feature.swap.domain.models.ui.PermissionDataState
import com.tangem.feature.swap.domain.models.ui.SwapState
import com.tangem.feature.swap.domain.models.ui.SwapStateData
import com.tangem.feature.swap.domain.models.ui.TxState
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.models.SwapPermissionState
import com.tangem.feature.swap.models.SwapStateHolder
import com.tangem.feature.swap.models.UiActions
import com.tangem.feature.swap.models.toDomainApproveType
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.feature.swap.router.SwapNavScreen
import com.tangem.feature.swap.router.SwapRouter
@ -272,9 +270,17 @@ internal class SwapViewModel @Inject constructor(
runCatching(dispatchers.io) {
swapInteractor.givePermissionToSwap(
networkId = dataState.networkId,
approveData = dataState.approveDataModel!!,
approveData = requireNotNull(dataState.approveDataModel) {
Timber.e("dataState.approveDataModel might not be null")
},
forTokenContractAddress = (dataState.fromCurrency as? Currency.NonNativeToken)?.contractAddress
?: "",
fromToken = requireNotNull(dataState.fromCurrency) {
Timber.e("dataState.fromCurrency might not be null")
},
approveType = requireNotNull(uiState.permissionState as? SwapPermissionState.ReadyForRequest) {
Timber.e("uiState.permissionState should be SwapPermissionState.ReadyForRequest")
}.approveType.toDomainApproveType(),
)
}
.onSuccess {
@ -282,9 +288,7 @@ internal class SwapViewModel @Inject constructor(
is TxState.TxSent -> {
uiState = stateBuilder.loadingPermissionState(uiState)
}
is TxState.UserCancelled -> {
/* no-op */
}
is TxState.UserCancelled -> Unit
else -> {
uiState = stateBuilder.createErrorTransaction(uiState, it) {
uiState = stateBuilder.clearAlert(uiState)
@ -429,6 +433,9 @@ internal class SwapViewModel @Inject constructor(
analyticsEventHandler.send(SwapEvents.SearchTokenClicked)
}
},
onChangeApproveType = { approveType ->
uiState = stateBuilder.updateApproveType(uiState, approveType)
},
)
}