diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9bfec2938a..f95eb69b21 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -369,11 +369,15 @@ Транзакция в процессе… Подождите Подтвердить + Текущая транзакция + Комиссия сети за одобрение токена будет взиматься за подтверждение того, что именно вы разрешаете использовать ваш токен для обмена. Дать разрешение + Укажите лимит доступа к выбранному токену Количество %s Отправитель Ваш кошелек Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s + Безлимитно Открыть в обозревателе В процессе Обмен diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 66205cf391..ba65e1aa64 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -365,11 +365,15 @@ Transaction in progress... Waiting Approve + Current transaction + The token approval network fee will be charged to confirm that you are the one allowing your token to be used for the exchange. Give Permission + Specify the approve limit for the selected token Amount %s Spender Your Wallet To continue you need to allow 1inch smart contracts to use your %s + Unlimited View in Explorer In progress Swap diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 0c74003585..156bdc1606 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -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 /** diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index cdca70f863..713d18f7c8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -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, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt new file mode 100644 index 0000000000..8690e83395 --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.swap.domain.models.domain + +enum class SwapApproveType { + LIMITED, UNLIMITED +} \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index 873966b94c..f4a0857938 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -37,6 +37,7 @@ dependencies { /** Other libraries */ implementation(deps.compose.shimmer) implementation(deps.kotlin.serialization) + implementation(deps.kotlin.immutable.collections) implementation(deps.timber) /** DI */ diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt index 2ec47f7595..8b6d4dd0c0 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapPermissionStateHolder.kt @@ -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.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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt index 9fea79221a..9469ec1c22 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt @@ -13,4 +13,5 @@ data class UiActions( val onMaxAmountSelected: () -> Unit, val openPermissionBottomSheet: () -> Unit, val hidePermissionBottomSheet: () -> Unit, + val onChangeApproveType: (ApproveType) -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index 11d2617601..37264eae08 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt index 2e268e7bf0..7190919db7 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapPermissionBottomSheetContent.kt @@ -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, + 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, +) { + 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 \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index ffd683f981..63dc12649c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -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) + }, ) }