diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index 94cd9cbf1f..3be2a6ebec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -48,6 +48,9 @@ enum class ExchangeStatus { @Json(name = "verifying") VERIFYING, + + @Json(name = "cancelled") + CANCELLED, } data class ExchangeStatusError( diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt index 7a6362efe3..4da4c8b17d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt @@ -36,13 +36,14 @@ class StatusCodeInterceptor : Interceptor { } private fun getBody(): String { - return "\"error\": {\n" + - " \"code\": 2290,\n" + - " \"description\": \"Core: receivedDecimals is not equal to expressDecimals\",\n" + + return "{\n" + + " \"error\": {\n" + + " \"code\": 2250,\n" + + " \"description\": \"Core: exchange too small amount\",\n" + " \"message\": \"Not valid\",\n" + - " \"receivedToDecimals\": 5,\n" + - " \"expressToDecimals\": 5\n" + - " }" + " \"minAmount\": 5\n" + + " }\n" + + "}" } companion object { diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index c2cdf27727..f9f34ddd5f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -189,8 +189,6 @@ Условия использования К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. - Вы получаете - Вы отправляете Мои токены У вас нет добавленных токенов. Добавьте токены для обмена Токены не найдены. Пожалуйста, попробуйте другой запрос @@ -205,6 +203,7 @@ Операция не выполнена провайдером Посетите сайт провайдера для проверки Провайдер запрашивает прохождение верификации + Отменен Подтверждено Подтверждение Подтверждение... @@ -523,6 +522,7 @@ Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. Подтвердить Ошибка: %s + Вы отправляете Произошла ошибка. Пожалуйста, попробуйте еще раз. Дать разрешение Сильные колебания цены! @@ -541,13 +541,12 @@ Ваш кошелек Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s Безлимитно - Вы отправляете - Вы получите Открыть в обозревателе В процессе Обменять Обмен %s на Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт. + Вы получите Другие токены Выберите токен Ваши токены @@ -732,4 +731,7 @@ Сканировать карту Используйте %s или отсканируйте карту для входа в приложение C возвращением! + Обмен через %s + Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. + Статус обмена diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index f595e68773..adb5903417 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -137,8 +137,6 @@ 服務條款 糟糕,當前版本的應用程序無法使用此卡,請檢查更新 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 - 您收到 - 您發送 以下信息是可選的。如果不想共享,可以將其刪除 告訴我們您缺少哪些功能,我們會盡力幫助您 請告訴我們你有什麼卡 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 94f6ed4c2e..bf64538d4b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -188,8 +188,6 @@ Terms of Service Oops, the current version of the application is not ready to work with this card, please check for updates. You have used a card from another wallet. Tap the card associated with this wallet - You receive - You send My tokens You haven\'t added any tokens yet. Add tokens via Market to swap No tokens found. Please try another request @@ -205,6 +203,7 @@ Operation failed by provider Visit provider’s website for verification KYC verification required by provider + Canceled Confirmed Confirming Confirming… @@ -219,7 +218,7 @@ Sending to you Sending to you… Sent - Provider-sourced data. Estimated amount subject to change. + Provider-sourced data. Estimated amount subject to change due to market conditions. Exchange status Verified Verification required @@ -534,6 +533,7 @@ All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve Error: %s + You swap There was an error. Please try again. Give Permission High price impact! @@ -552,13 +552,12 @@ Your wallet To continue, grant 1inch smart contracts permission to use your %s Unlimited - You swap - You receive View in Explorer In progress Swap Swap %s for Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product. + You receive Other tokens Choose token Your tokens diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index 867774adfb..4dfd01d36d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -95,7 +95,6 @@ private fun CollapsedSearchView( modifier = Modifier .background(TangemTheme.colors.background.secondary) .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), verticalAlignment = Alignment.CenterVertically, ) { Icon( diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt index 0370fe0efe..bfae5cb313 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt @@ -111,7 +111,7 @@ internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconP TangemButtonSize.Default, TangemButtonSize.WideAction, TangemButtonSize.TwoLines, - -> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32 + -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16 TangemButtonSize.Text -> when (icon) { is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16 is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing14 to TangemTheme.dimens.spacing16 diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index c64c860e28..e2d0c8dc42 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -22,7 +22,13 @@ object BigDecimalFormatter { roundingMode = RoundingMode.DOWN } - return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency" + return formatter.format(cryptoAmount).let { + if (cryptoCurrency.isEmpty()) { + it + } else { + it + "\u2009$cryptoCurrency" + } + } } fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String { diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt index dd94a2e07c..8ba9fe211e 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt @@ -40,16 +40,23 @@ class DefaultSwapPromoRepository( ) } - private fun checkPromoPeriod(): Boolean { - val currentDate = Calendar.getInstance() - return currentDate.get(Calendar.DATE) in START_DATE_KEY..END_DATE_KEY && - currentDate.get(Calendar.MONTH) == MONTH_KEY && currentDate.get(Calendar.YEAR) == YEAR_KEY + private suspend fun checkPromoPeriod(): Boolean { + val calendar = Calendar.getInstance() + val currentTime = calendar.timeInMillis + calendar.set(END_YEAR_KEY, END_MONTH_KEY, END_DAY_KEY, 0, 0, 0) + val endTime = calendar.timeInMillis + + val shouldShow = endTime - currentTime > 0 + if (!shouldShow) { + setNeverToShowToken() + setNeverToShowWallet() + } + return shouldShow } companion object { - private const val START_DATE_KEY = 15 - private const val END_DATE_KEY = 31 - private const val MONTH_KEY = 11 // December - private const val YEAR_KEY = 2023 // just in case + private const val END_DAY_KEY = 1 + private const val END_MONTH_KEY = 1 // February + private const val END_YEAR_KEY = 2024 } } \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index 6ca55d7f04..fa28cf44f2 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.converters import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.api.express.models.response.ExpressError import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.createFromAmountWithOffset @@ -21,28 +22,47 @@ internal class ErrorsDataConverter( 2220 -> DataError.ExchangeProviderNotActiveError(code = error.code) 2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code) 2240 -> DataError.ExchangeNotPossibleError(code = error.code) - 2250 -> DataError.ExchangeTooSmallAmountError( - code = error.code, - amount = createFromAmountWithOffset( - requireNotNull(error.value?.minAmount), - requireNotNull(error.value?.decimals), - ), - ) - 2260 -> DataError.ExchangeNotEnoughAllowanceError( - code = error.code, - currentAllowance = requireNotNull(error.value?.currentAllowance), - ) + 2250 -> tryParseExchangeTooSmallAmountError(error = error) + 2260 -> tryParseExchangeNotEnoughAllowanceError(error = error) 2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code) 2280 -> DataError.ExchangeInvalidAddressError(code = error.code) - 2290 -> DataError.ExchangeInvalidFromDecimalsError( - code = error.code, - receivedFromDecimals = requireNotNull(error.value?.receivedFromDecimals), - expressFromDecimals = requireNotNull(error.value?.expressFromDecimals), - ) + 2290 -> tryParseExchangeInvalidFromDecimalsError(error = error) else -> DataError.UnknownErrorWithCode(error.code) } } catch (e: Exception) { return DataError.UnknownError } } + + private fun tryParseExchangeTooSmallAmountError(error: ExpressError): DataError { + val minAmount = error.value?.minAmount ?: return DataError.UnknownErrorWithCode(error.code) + val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code) + + return DataError.ExchangeTooSmallAmountError( + code = error.code, + amount = createFromAmountWithOffset(minAmount, decimals), + ) + } + + private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): DataError { + val currentAllowance = error.value?.currentAllowance ?: return DataError.UnknownErrorWithCode(error.code) + + return DataError.ExchangeNotEnoughAllowanceError( + code = error.code, + currentAllowance = currentAllowance, + ) + } + + private fun tryParseExchangeInvalidFromDecimalsError(error: ExpressError): DataError { + val receivedFromDecimals = error.value?.receivedFromDecimals ?: return DataError.UnknownErrorWithCode( + code = error.code, + ) + val expressFromDecimals = error.value?.expressFromDecimals ?: return DataError.UnknownErrorWithCode(error.code) + + return DataError.ExchangeInvalidFromDecimalsError( + code = error.code, + receivedFromDecimals = receivedFromDecimals, + expressFromDecimals = expressFromDecimals, + ) + } } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt index e412c74751..1e3727b208 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt @@ -17,4 +17,5 @@ enum class ExchangeStatus { Sending, Finished, Refunded, + Cancelled, } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index d475bad6f2..936254d3fa 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -8,7 +8,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo -import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup +import com.tangem.feature.swap.models.CurrenciesGroupWithFromCurrency import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState @@ -21,17 +21,21 @@ class TokensDataConverter( private val onTokenSelected: (String) -> Unit, private val isBalanceHiddenProvider: Provider, private val appCurrencyProvider: Provider, -) : Converter { +) : Converter { - override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { + override fun convert(value: CurrenciesGroupWithFromCurrency): SwapSelectTokenStateHolder { + val group = value.group val availableTitle = TokenToSelectState.Title( resourceReference(R.string.exchange_tokens_available_tokens_header), ) val unavailableTitle = TokenToSelectState.Title( - resourceReference(R.string.exchange_tokens_available_tokens_header), + resourceReference( + R.string.exchange_tokens_unavailable_tokens_header, + wrappedList(value.fromCurrency.name), + ), ) return SwapSelectTokenStateHolder( - availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } + availableTokens = group.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() .apply { if (this.isNotEmpty()) { @@ -39,7 +43,7 @@ class TokensDataConverter( } } .toImmutableList(), - unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } + unavailableTokens = group.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } .toMutableList() .apply { if (this.isNotEmpty()) { @@ -49,7 +53,7 @@ class TokensDataConverter( .toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, - afterSearch = value.afterSearch, + afterSearch = group.afterSearch, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/CurrenciesGroupWithFromCurrency.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/CurrenciesGroupWithFromCurrency.kt new file mode 100644 index 0000000000..8251580bb8 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/CurrenciesGroupWithFromCurrency.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.swap.models + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup + +data class CurrenciesGroupWithFromCurrency( + val group: CurrenciesGroup, + val fromCurrency: CryptoCurrency, +) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 577c2e96af..2ccc7c9571 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -105,12 +105,12 @@ sealed interface TransactionCardType { data class Inputtable( val onAmountChanged: ((String) -> Unit), val onFocusChanged: ((Boolean) -> Unit), - @StringRes override val headerResId: Int = R.string.exchange_send_view_header, + @StringRes override val headerResId: Int = R.string.swapping_from_title, ) : TransactionCardType data class ReadOnly( val highPriceImpact: String? = null, - @StringRes override val headerResId: Int = R.string.exchange_receive_view_header, + @StringRes override val headerResId: Int = R.string.swapping_to_title, ) : TransactionCardType } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 0621d0a12a..d30d7591f2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -34,16 +34,16 @@ fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { } } +@Suppress("LongMethod") @Composable private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { - Column { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = stringResource(R.string.express_choose_providers_title), style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing10) - .align(Alignment.CenterHorizontally), + .padding(top = TangemTheme.dimens.spacing10), ) Text( text = stringResource(R.string.express_choose_providers_subtitle), @@ -51,8 +51,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC color = TangemTheme.colors.text.secondary, modifier = Modifier .padding(top = TangemTheme.dimens.spacing10) - .padding(horizontal = TangemTheme.dimens.spacing56) - .align(Alignment.CenterHorizontally), + .padding(horizontal = TangemTheme.dimens.spacing56), textAlign = TextAlign.Center, ) Column( @@ -79,7 +78,11 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC enabled = provider.onProviderClick != null, onClick = { provider.onProviderClick?.invoke(provider.id) }, ) - .padding(TangemTheme.dimens.spacing12), + .padding( + top = TangemTheme.dimens.spacing12, + bottom = TangemTheme.dimens.spacing12, + end = TangemTheme.dimens.spacing12, + ), ) } } @@ -87,8 +90,6 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC painterResource(id = R.drawable.ic_lightning_16), contentDescription = null, tint = TangemTheme.colors.icon.informative, - modifier = Modifier - .align(Alignment.CenterHorizontally), ) Text( text = stringResource(R.string.express_more_providers_soon), @@ -96,8 +97,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC color = TangemTheme.colors.icon.informative, modifier = Modifier .padding(top = TangemTheme.dimens.spacing6, bottom = TangemTheme.dimens.spacing16) - .padding(horizontal = TangemTheme.dimens.spacing56) - .align(Alignment.CenterHorizontally), + .padding(horizontal = TangemTheme.dimens.spacing56), textAlign = TextAlign.Center, ) } 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 b7c7418dae..5246e8ed36 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 @@ -98,12 +98,8 @@ internal class StateBuilder( if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = TransactionCardType.ReadOnly( - headerResId = R.string.exchange_send_view_header, - ), - amountTextFieldValue = TextFieldValue( - text = "0", - ), + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, @@ -111,7 +107,7 @@ internal class StateBuilder( isNotNativeToken = uiStateHolder.sendCardData.isNotNativeToken, tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, - balance = fromToken.getFormattedAmount(), + balance = fromToken.getFormattedAmount(isNeedSymbol = false), networkIconRes = getActiveIconRes(fromToken.currency.network.id.value), isBalanceHidden = isBalanceHiddenProvider(), ), @@ -209,7 +205,7 @@ internal class StateBuilder( fromToken: CryptoCurrency, swapProvider: SwapProvider, bestRatedProviderId: String, - isManyProviders: Boolean, + isNeedBestRateBadge: Boolean, selectedFeeType: FeeType, isReverseSwapPossible: Boolean, ): SwapStateHolder { @@ -231,7 +227,7 @@ internal class StateBuilder( tokenCurrency = uiStateHolder.sendCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.sendCardData.canSelectAnotherToken, networkIconRes = uiStateHolder.sendCardData.networkIconRes, - balance = fromCurrencyStatus.getFormattedAmount(), + balance = fromCurrencyStatus.getFormattedAmount(isNeedSymbol = false), isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.SwapCardData( @@ -245,7 +241,7 @@ internal class StateBuilder( tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, networkIconRes = uiStateHolder.receiveCardData.networkIconRes, - balance = toCurrencyStatus.getFormattedAmount(), + balance = toCurrencyStatus.getFormattedAmount(isNeedSymbol = false), isBalanceHidden = isBalanceHiddenProvider(), ), networkCurrency = quoteModel.networkCurrency, @@ -270,7 +266,7 @@ internal class StateBuilder( isBestRate = bestRatedProviderId == swapProvider.providerId, fromTokenInfo = quoteModel.fromTokenInfo, toTokenInfo = quoteModel.toTokenInfo, - isNeedBadge = isManyProviders, + isNeedBestRateBadge = isNeedBestRateBadge, selectionType = ProviderState.SelectionType.CLICK, onProviderClick = actions.onProviderClick, ), @@ -302,6 +298,7 @@ internal class StateBuilder( ) } + @Suppress("LongMethod", "CyclomaticComplexMethod") private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, @@ -326,18 +323,7 @@ internal class StateBuilder( ) else -> Unit } - if (!quoteModel.preparedSwapConfigState.isFeeEnough && - quoteModel.preparedSwapConfigState.isBalanceEnough - ) { - warnings.add( - SwapWarning.UnableToCoverFeeWarning( - createUnableToCoverFeeNotificationConfig( - fromToken = fromToken, - onBuyClick = actions.onBuyClick, - ), - ), - ) - } + addUnableCoverFeeWarning(quoteModel, fromToken, warnings) // check isBalanceEnough, but for dex includeFeeInAmount always Excluded if (!quoteModel.preparedSwapConfigState.isBalanceEnough && quoteModel.preparedSwapConfigState.includeFeeInAmount !is IncludeFeeInAmount.Included @@ -377,6 +363,26 @@ internal class StateBuilder( return warnings } + private fun addUnableCoverFeeWarning( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + warnings: MutableList, + ) { + if (!quoteModel.preparedSwapConfigState.isFeeEnough && + quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.permissionState !is PermissionDataState.PermissionLoading + ) { + warnings.add( + SwapWarning.UnableToCoverFeeWarning( + createUnableToCoverFeeNotificationConfig( + fromToken = fromToken, + onBuyClick = actions.onBuyClick, + ), + ), + ) + } + } + private fun getSwapButtonEnabled(preparedSwapConfigState: PreparedSwapConfigState): Boolean { if (preparedSwapConfigState.hasOutgoingTransaction) return false return when (preparedSwapConfigState.includeFeeInAmount) { @@ -431,7 +437,7 @@ internal class StateBuilder( tokenCurrency = uiStateHolder.receiveCardData.tokenCurrency, canSelectAnotherToken = uiStateHolder.receiveCardData.canSelectAnotherToken, networkIconRes = uiStateHolder.receiveCardData.networkIconRes, - balance = toToken.getFormattedAmount(), + balance = toToken.getFormattedAmount(isNeedSymbol = false), isBalanceHidden = isBalanceHiddenProvider(), ) } ?: SwapCardState.Empty( @@ -445,6 +451,7 @@ internal class StateBuilder( return uiStateHolder.copy( sendCardData = uiStateHolder.sendCardData.copy( amountEquivalent = getFormattedFiatAmount(fromToken.amountFiat), + balance = fromToken.cryptoCurrencyStatus.getFormattedAmount(isNeedSymbol = false), ), receiveCardData = receiveCardData, warnings = warnings, @@ -583,10 +590,17 @@ internal class StateBuilder( ) } - fun addTokensToState(uiState: SwapStateHolder, tokensDataState: CurrenciesGroup): SwapStateHolder { + fun addTokensToState( + uiState: SwapStateHolder, + fromToken: CryptoCurrency, + tokensDataState: CurrenciesGroup, + ): SwapStateHolder { return uiState.copy( selectTokenState = tokensDataConverter.convert( - value = tokensDataState, + value = CurrenciesGroupWithFromCurrency( + fromCurrency = fromToken, + group = tokensDataState, + ), ), ) } @@ -1121,7 +1135,7 @@ internal class StateBuilder( fromTokenInfo: TokenSwapInfo, toTokenInfo: TokenSwapInfo, selectionType: ProviderState.SelectionType, - isNeedBadge: Boolean, + isNeedBestRateBadge: Boolean, onProviderClick: (String) -> Unit, ): ProviderState { val rate = toTokenInfo.tokenAmount.value.calculateRate( @@ -1131,7 +1145,7 @@ internal class StateBuilder( val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" - val badge = if (isNeedBadge && isBestRate) { + val badge = if (isNeedBestRateBadge && isBestRate) { ProviderState.AdditionalBadge.BestTrade } else { ProviderState.AdditionalBadge.Empty @@ -1214,10 +1228,10 @@ internal class StateBuilder( ) } - private fun CryptoCurrencyStatus.getFormattedAmount(): String { + private fun CryptoCurrencyStatus.getFormattedAmount(isNeedSymbol: Boolean): String { val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN - - return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + val symbol = if (isNeedSymbol) currency.symbol else "" + return BigDecimalFormatter.formatCryptoAmount(amount, symbol, currency.decimals) } @Suppress("UnusedPrivateMember") diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 571a1b51f1..e3c408bde2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -131,7 +131,7 @@ private fun MainInfo(state: SwapStateHolder) { }, onSelectTokenClick = state.onSelectTokenClick, ) - val marginCard = TangemTheme.dimens.spacing16 + val marginCard = TangemTheme.dimens.spacing12 TransactionCardData( priceImpact = priceImpact, swapCardState = state.receiveCardData, @@ -140,7 +140,7 @@ private fun MainInfo(state: SwapStateHolder) { }, onSelectTokenClick = state.onSelectTokenClick, ) - val marginButton = TangemTheme.dimens.spacing32 + val marginButton = TangemTheme.dimens.spacing30 SwapButton( state, modifier = Modifier.constrainAs(button) { @@ -413,7 +413,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U else -> { PrimaryButtonIconEnd( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.common_swap), + text = stringResource(id = R.string.swapping_swap_action), iconResId = R.drawable.ic_tangem_24, enabled = state.swapButton.enabled, onClick = state.swapButton.onClick, @@ -494,12 +494,12 @@ private val state = SwapStateHolder( priceImpact = PriceImpact.Empty(), tosState = TosState( tosLink = LegalState( - title = stringReference("Tangem"), + title = stringReference("Terms of Use"), link = "https://tangem.com", onClick = {}, ), policyLink = LegalState( - title = stringReference("Tangem"), + title = stringReference("Privacy Policy"), link = "https://tangem.com", onClick = {}, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 87a82413f1..930039d697 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -127,8 +127,6 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = .imePadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { - item { SpacerH8() } - tokensToSelectItems(state.availableTokens, state.onTokenSelected) item { SpacerH12() } @@ -182,6 +180,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod Text( text = item.title.resolveReference().uppercase(), style = TangemTheme.typography.overline, + color = TangemTheme.colors.text.tertiary, modifier = Modifier .padding( top = TangemTheme.dimens.spacing16, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index d540514a81..4b44fb9b18 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -60,7 +60,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad TransactionDoneTitle(titleRes = R.string.swapping_success_view_title, date = state.timestamp) SpacerH16() InputRowImage( - title = TextReference.Res(R.string.swapping_success_from_title), + title = TextReference.Res(R.string.swapping_from_title), subtitle = state.fromTokenAmount, caption = state.fromTokenFiatAmount, tokenIconState = state.fromTokenIconState ?: TokenIconState.Loading, @@ -70,7 +70,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad ) SpacerH16() InputRowImage( - title = TextReference.Res(R.string.swapping_success_to_title), + title = TextReference.Res(R.string.swapping_to_title), subtitle = state.toTokenAmount, caption = state.toTokenFiatAmount, tokenIconState = state.toTokenIconState ?: TokenIconState.Loading, 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 955d5e5f74..a18226212a 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 @@ -216,11 +216,12 @@ internal class SwapViewModel @Inject constructor( } } - private fun updateTokensState(dataState: TokensDataStateExpress) { - val tokensDataState = if (isOrderReversed) dataState.fromGroup else dataState.toGroup + private fun updateTokensState(tokenDataState: TokensDataStateExpress) { + val tokensDataState = if (isOrderReversed) tokenDataState.fromGroup else tokenDataState.toGroup uiState = stateBuilder.addTokensToState( uiState = uiState, tokensDataState = tokensDataState, + fromToken = dataState.fromCryptoCurrency?.currency ?: initialCryptoCurrency, ) } @@ -326,7 +327,7 @@ internal class SwapViewModel @Inject constructor( fromToken = fromToken.currency, swapProvider = provider, bestRatedProviderId = bestRatedProviderId, - isManyProviders = dataState.lastLoadedSwapStates.size > 1, + isNeedBestRateBadge = dataState.lastLoadedSwapStates.consideredProvidersStates().size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, isReverseSwapPossible = isReverseSwapPossible(), ) @@ -390,9 +391,7 @@ internal class SwapViewModel @Inject constructor( } private fun selectProvider(state: Map): SwapProvider { - val consideredProviders = state.filter { - it.value is SwapState.QuotesLoadedState || isExchangeTooSmallAmountError(it.value) - } + val consideredProviders = state.consideredProvidersStates() return if (consideredProviders.isNotEmpty()) { val currentSelected = dataState.selectedProvider @@ -420,16 +419,21 @@ internal class SwapViewModel @Inject constructor( } else { dataState.copy( swapDataModel = swapDataModel, - selectedFee = selectDefaultFee(state), + selectedFee = updateOrSelectFee(state), ) } } - private fun selectDefaultFee(state: SwapState.QuotesLoadedState): TxFee? { - return dataState.selectedFee ?: when (val txFee = state.txFee) { + private fun updateOrSelectFee(state: SwapState.QuotesLoadedState): TxFee? { + val selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL + return when (val txFee = state.txFee) { TxFeeState.Empty -> null is TxFeeState.MultipleFeeState -> { - txFee.normalFee + if (selectedFeeType == FeeType.NORMAL) { + txFee.normalFee + } else { + txFee.priorityFee + } } is TxFeeState.SingleFeeState -> { txFee.fee @@ -689,19 +693,22 @@ internal class SwapViewModel @Inject constructor( private fun onAmountChanged(value: String) { val fromToken = dataState.fromCryptoCurrency val toToken = dataState.toCryptoCurrency - if (fromToken != null && toToken != null) { + if (fromToken != null) { val decimals = fromToken.currency.decimals val cutValue = cutAmountWithDecimals(decimals, value) lastAmount.value = cutValue uiState = stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals)) - amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { - startLoadingQuotes( - fromToken = fromToken, - toToken = toToken, - amount = lastAmount.value, - toProvidersList = findSwapProviders(fromToken, toToken), - ) + + if (toToken != null) { + amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) { + startLoadingQuotes( + fromToken = fromToken, + toToken = toToken, + amount = lastAmount.value, + toProvidersList = findSwapProviders(fromToken, toToken), + ) + } } } } @@ -853,14 +860,10 @@ internal class SwapViewModel @Inject constructor( startLoadingQuotesFromLastState() }, onPolicyClick = { - uiState = stateBuilder.showWebViewBottomSheet(uiState, it) { - uiState = stateBuilder.dismissBottomSheet(uiState) - } + swapRouter.openUrl(it) }, onTosClick = { - uiState = stateBuilder.showWebViewBottomSheet(uiState, it) { - uiState = stateBuilder.dismissBottomSheet(uiState) - } + swapRouter.openUrl(it) }, ) } @@ -947,6 +950,12 @@ internal class SwapViewModel @Inject constructor( .mapValues { it.value as SwapState.QuotesLoadedState } } + private fun Map.consideredProvidersStates(): Map { + return this.filter { + it.value is SwapState.QuotesLoadedState || isExchangeTooSmallAmountError(it.value) + } + } + private fun isReverseSwapPossible(): Boolean { val from = dataState.fromCryptoCurrency ?: return false val to = dataState.toCryptoCurrency ?: return false diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index d55821009b..c1e0f67210 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -29,6 +29,7 @@ internal data class SwapTransactionsState( val fromCryptoSymbol: String, val fromFiatAmount: String, val fromCurrencyIcon: TokenIconState, + val showProviderLink: Boolean, val onClick: () -> Unit, val onGoToProviderClick: (String) -> Unit, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index 8af1d13a13..3575d02e0f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -78,7 +78,7 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { */ data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_swap), + text = TextReference.Res(id = R.string.common_exchange), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, enabled = enabled, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 514afc7709..c5069303c7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -62,6 +62,8 @@ internal class TokenDetailsSwapTransactionsStateConverter( val toFiatAmount = toAmount.multiply(toCurrency.value.fiatRate) val fromFiatAmount = fromAmount.multiply(fromCurrency.value.fiatRate) val timestamp = transaction.timestamp + val notifications = getNotification(transaction.status?.status, transaction.status?.txExternalUrl) + val showProviderLink = getShowProviderLink(notifications, transaction.status) result.add( SwapTransactionsState( txId = transaction.txId, @@ -92,6 +94,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( fromCryptoSymbol = fromCurrency.currency.symbol, fromFiatAmount = getFiatAmount(fromFiatAmount), fromCurrencyIcon = iconStateConverter.convert(fromCurrency), + showProviderLink = showProviderLink, onClick = { clickIntents.onSwapTransactionClick(transaction.txId) }, onGoToProviderClick = { url -> analyticsEventsHandlerProvider().send( @@ -109,12 +112,15 @@ internal class TokenDetailsSwapTransactionsStateConverter( fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?): SwapTransactionsState { if (statusModel == null || tx.activeStatus == statusModel.status) return tx val hasFailed = tx.hasFailed || statusModel.status == ExchangeStatus.Failed + val notifications = getNotification(statusModel.status, statusModel.txExternalUrl) + val showProviderLink = getShowProviderLink(notifications, statusModel) return tx.copy( activeStatus = statusModel.status, hasFailed = hasFailed, - notification = getNotification(statusModel.status, statusModel.txExternalUrl), + notification = notifications, statuses = getStatuses(statusModel.status, hasFailed), txUrl = statusModel.txExternalUrl, + showProviderLink = showProviderLink, ) } @@ -149,6 +155,9 @@ internal class TokenDetailsSwapTransactionsStateConverter( } } + private fun getShowProviderLink(notifications: ExchangeStatusNotifications?, status: ExchangeStatusModel?) = + notifications == null && status?.txExternalUrl != null && status.status != ExchangeStatus.Cancelled + private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList { if (status == null) return persistentListOf() val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting @@ -164,26 +173,37 @@ internal class TokenDetailsSwapTransactionsStateConverter( val isExchangingDone = !isExchanging && isConfirmingDone val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone - return listOf( - waitStep(isWaiting, isWaitingDone), - confirmStep(isConfirming, isConfirmingDone), - exchangeStep( - isExchanging = isExchanging, - isExchangingDone = isExchangingDone, - isRefunded = isRefunded, - hasFailed = hasFailed, - isVerifying = isVerifying, - isFailed = isFailed, - ), - sendStep( - isSending = isSending, - isSendingDone = isSendingDone, - isRefunded = isRefunded, - hasFailed = hasFailed, - ), - ).toPersistentList() + return if (status == ExchangeStatus.Cancelled) { + listOf(cancelledStep()) + } else { + listOf( + waitStep(isWaiting, isWaitingDone), + confirmStep(isConfirming, isConfirmingDone), + exchangeStep( + isExchanging = isExchanging, + isExchangingDone = isExchangingDone, + isRefunded = isRefunded, + hasFailed = hasFailed, + isVerifying = isVerifying, + isFailed = isFailed, + ), + sendStep( + isSending = isSending, + isSendingDone = isSendingDone, + isRefunded = isRefunded, + hasFailed = hasFailed, + ), + ) + }.toPersistentList() } + private fun cancelledStep() = ExchangeStatusState( + status = ExchangeStatus.Cancelled, + text = TextReference.Res(R.string.express_exchange_status_canceled), + isActive = true, + isDone = true, + ) + private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState( status = ExchangeStatus.New, text = when { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index 1574bb2db6..ac5edc4fc4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -75,11 +75,15 @@ internal fun ExchangeStatusBlock( } } - statuses.forEachIndexed { index, item -> - ExchangeStatusStep( - stepStatus = item, - isLast = index == statuses.lastIndex, - ) + AnimatedContent(targetState = statuses.lastIndex, label = "Exchange Status List Change") { + Column { + statuses.forEachIndexed { index, item -> + ExchangeStatusStep( + stepStatus = item, + isLast = index == it, + ) + } + } } } } @@ -101,6 +105,11 @@ private fun ExchangeStatusStep( .size(TangemTheme.dimens.size20), ) { when { + it.status == ExchangeStatus.Cancelled -> ExchangeStep( + iconRes = R.drawable.ic_close_24, + color = TangemTheme.colors.icon.warning, + isDone = false, + ) it.status == ExchangeStatus.Failed -> ExchangeStep( iconRes = R.drawable.ic_close_24, color = TangemTheme.colors.icon.warning, @@ -131,6 +140,7 @@ private fun ExchangeStatusStep( @Composable private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) { val textColor = when { + stepStatus.status == ExchangeStatus.Cancelled -> TangemTheme.colors.icon.warning stepStatus.status == ExchangeStatus.Failed && !stepStatus.isDone -> TangemTheme.colors.icon.warning stepStatus.status == ExchangeStatus.Verifying && !stepStatus.isDone -> TangemTheme.colors.icon.attention stepStatus.isDone -> TangemTheme.colors.text.primary1 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index 7cd58e238d..da9113487a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH12 @@ -50,6 +51,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC text = stringResource(id = R.string.express_exchange_status_subtitle), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, modifier = Modifier .align(CenterHorizontally), ) @@ -74,7 +76,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC SpacerH12() ExchangeStatusBlock( statuses = config.statuses, - showLink = config.notification == null && config.txUrl != null, + showLink = config.showProviderLink, onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) }, ) AnimatedContent( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index c32011f436..94c1d12187 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -4,7 +4,9 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -42,7 +44,9 @@ internal fun LazyListScope.swapTransactionsItems( val item = swapTxs[it] val (iconRes, tint) = when (item.activeStatus) { ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention - ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning + ExchangeStatus.Failed, ExchangeStatus.Cancelled -> { + R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning + } else -> null to null } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 4c78d11155..8ff627eaec 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -18,7 +18,9 @@ import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -41,6 +43,7 @@ internal class ExchangeStatusFactory( private val clickIntents: TokenDetailsClickIntents, private val appCurrencyProvider: Provider, private val analyticsEventsHandlerProvider: Provider, + private val currentStateProvider: Provider, private val userWalletId: UserWalletId, private val cryptoCurrency: CryptoCurrency, ) { @@ -64,6 +67,27 @@ internal class ExchangeStatusFactory( ) } + suspend fun removeTransactionOnBottomSheetClosed(): TokenDetailsState { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return state + val selectedTx = bottomSheetConfig.value + + return if (selectedTx.activeStatus.isTerminal()) { + swapTransactionRepository.removeTransaction( + userWalletId = userWalletId, + fromCryptoCurrencyId = selectedTx.fromCryptoCurrencyId, + toCryptoCurrencyId = selectedTx.toCryptoCurrencyId, + txId = selectedTx.txId, + ) + val filteredTxs = state.swapTxs + .filterNot { it.txId == selectedTx.txId } + .toPersistentList() + state.copy(swapTxs = filteredTxs) + } else { + state + } + } + private fun getWalletCryptoCurrencies() = flow { val selectedWallet = getSelectedWalletSyncUseCase().fold( ifLeft = { null }, @@ -80,14 +104,16 @@ internal class ExchangeStatusFactory( suspend fun updateSwapTxStatuses(swapTxList: PersistentList) = withContext(dispatchers.io) { swapTxList.map { tx -> async { - val statusModel = getExchangeStatus(tx.txId) - swapTransactionsStateConverter - .updateTxStatus(tx, statusModel) - .removeIfFinished(statusModel?.status) + if (tx.activeStatus.isTerminal()) { + tx + } else { + val statusModel = getExchangeStatus(tx.txId) + swapTransactionsStateConverter + .updateTxStatus(tx, statusModel) + } } } .awaitAll() - .filterNotNull() .toPersistentList() } @@ -130,21 +156,8 @@ internal class ExchangeStatusFactory( ) } - private suspend fun SwapTransactionsState.removeIfFinished(status: ExchangeStatus?) = when (status) { - null -> null // not found - ExchangeStatus.Refunded, ExchangeStatus.Finished -> { - swapTransactionRepository.removeTransaction( - userWalletId = userWalletId, - fromCryptoCurrencyId = fromCryptoCurrencyId, - toCryptoCurrencyId = toCryptoCurrencyId, - txId = txId, - ) - null - } - else -> { - this - } - } + private fun ExchangeStatus?.isTerminal() = + this == ExchangeStatus.Refunded || this == ExchangeStatus.Finished || this == ExchangeStatus.Cancelled private fun toAnalyticStatus(status: ExchangeStatus?): ExchangeAnalyticsStatus? { return when (status) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1c3f77a105..b2d4ba321e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -125,6 +125,7 @@ internal class TokenDetailsViewModel @Inject constructor( clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, analyticsEventsHandlerProvider = Provider { analyticsEventsHandler }, + currentStateProvider = Provider { uiState }, userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, ) @@ -229,7 +230,7 @@ internal class TokenDetailsViewModel @Inject constructor( delay = EXCHANGE_STATUS_UPDATE_DELAY, task = { runCatching(dispatchers.io) { - exchangeStatusFactory.updateSwapTxStatuses(swapTxs) + exchangeStatusFactory.updateSwapTxStatuses(uiState.swapTxs) } }, onSuccess = ::updateSwapTx, @@ -547,6 +548,11 @@ internal class TokenDetailsViewModel @Inject constructor( } override fun onDismissBottomSheet() { + if (uiState.bottomSheetConfig?.content is ExchangeStatusBottomSheetConfig) { + viewModelScope.launch(dispatchers.main) { + uiState = exchangeStatusFactory.removeTransactionOnBottomSheetClosed() + } + } uiState = stateFactory.getStateWithClosedBottomSheet() } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt index ae4d06b0dc..c5af5a2cf6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt @@ -19,10 +19,7 @@ import org.burnoutcrew.reorderable.detectReorder @Composable internal fun NetworkGroupItem(networkName: String, modifier: Modifier = Modifier) { - InternalNetworkGroupItem( - modifier = modifier, - networkName = networkName, - ) + InternalNetworkGroupItem(modifier = modifier, networkName = networkName) } @Composable @@ -69,12 +66,13 @@ private fun InternalNetworkGroupItem( .background(TangemTheme.colors.background.primary) .padding(horizontal = TangemTheme.dimens.spacing14) .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size48), + .heightIn(min = TangemTheme.dimens.size40), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Text( text = stringResource(id = R.string.wallet_network_group_title, networkName), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing8), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt index fabf18401f..2628469375 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt @@ -88,7 +88,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Swap(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_swap), + text = TextReference.Res(id = R.string.common_exchange), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, enabled = enabled, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt index df989ef9b8..7a452a255c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -78,7 +78,7 @@ internal class TokenActionsProvider( action = { clickIntents.onMultiCurrencySendClick(cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Swap -> { - title = resourceReference(R.string.common_swap) + title = resourceReference(R.string.common_exchange) icon = R.drawable.ic_exchange_horizontal_24 action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } } diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 237bd3e4a9..805056d1a5 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-423" +tangemBlockchainSdk = "release-app_5.4-434" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.4-315" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^