Updated on 2026-08-14

This commit is contained in:
Tangem 2023-12-26 17:19:34 +03:00
commit eeaf9e775a
32 changed files with 309 additions and 184 deletions

View file

@ -48,6 +48,9 @@ enum class ExchangeStatus {
@Json(name = "verifying")
VERIFYING,
@Json(name = "cancelled")
CANCELLED,
}
data class ExchangeStatusError(

View file

@ -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 {

View file

@ -189,8 +189,6 @@
<string name="disclaimer_title">Условия использования</string>
<string name="error_update_app">К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений</string>
<string name="error_wrong_wallet_tapped">Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком.</string>
<string name="exchange_receive_view_header">Вы получаете</string>
<string name="exchange_send_view_header">Вы отправляете</string>
<string name="exchange_tokens_available_tokens_header">Мои токены</string>
<string name="exchange_tokens_empty_tokens">У вас нет добавленных токенов. Добавьте токены для обмена</string>
<string name="express_token_list_empty_search">Токены не найдены. Пожалуйста, попробуйте другой запрос</string>
@ -205,6 +203,7 @@
<string name="express_exchange_notification_failed_title">Операция не выполнена провайдером</string>
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
<string name="express_exchange_notification_verification_title">Провайдер запрашивает прохождение верификации</string>
<string name="express_exchange_status_canceled">Отменен</string>
<string name="express_exchange_status_confirmed">Подтверждено</string>
<string name="express_exchange_status_confirming">Подтверждение</string>
<string name="express_exchange_status_confirming_active">Подтверждение...</string>
@ -523,6 +522,7 @@
<string name="swapping_approve_information_text">Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен.</string>
<string name="swapping_approve_information_title">Подтвердить</string>
<string name="swapping_error_wrapper">Ошибка: %s</string>
<string name="swapping_from_title">Вы отправляете</string>
<string name="swapping_generic_error">Произошла ошибка. Пожалуйста, попробуйте еще раз.</string>
<string name="swapping_give_permission">Дать разрешение</string>
<string name="swapping_high_price_impact">Сильные колебания цены!</string>
@ -541,13 +541,12 @@
<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_from_title">Вы отправляете</string>
<string name="swapping_success_to_title">Вы получите</string>
<string name="swapping_success_view_explorer_button_title">Открыть в обозревателе</string>
<string name="swapping_success_view_title">В процессе</string>
<string name="swapping_swap_action">Обменять</string>
<string name="swapping_swap_of_to">Обмен %s на</string>
<string name="swapping_tangem_fee_disclaimer">Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт.</string>
<string name="swapping_to_title">Вы получите</string>
<string name="swapping_token_list_other_tokens">Другие токены</string>
<string name="swapping_token_list_title">Выберите токен</string>
<string name="swapping_token_list_your_tokens">Ваши токены</string>
@ -732,4 +731,7 @@
<string name="welcome_unlock_card">Сканировать карту</string>
<string name="welcome_unlock_description">Используйте %s или отсканируйте карту для входа в приложение</string>
<string name="welcome_unlock_title">C возвращением!</string>
<string name="express_exchange_by">Обмен через %s</string>
<string name="express_exchange_status_subtitle">Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий.</string>
<string name="express_exchange_status_title">Статус обмена</string>
</resources>

View file

@ -137,8 +137,6 @@
<string name="disclaimer_title">服務條款</string>
<string name="error_update_app">糟糕,當前版本的應用程序無法使用此卡,請檢查更新</string>
<string name="error_wrong_wallet_tapped">您使用了另一個錢包中的卡。點按與此錢包關聯的卡片</string>
<string name="exchange_receive_view_header">您收到</string>
<string name="exchange_send_view_header">您發送</string>
<string name="feedback_data_collection_message">以下信息是可選的。如果不想共享,可以將其刪除</string>
<string name="feedback_preface_rate_negative">告訴我們您缺少哪些功能,我們會盡力幫助您</string>
<string name="feedback_preface_scan_failed">請告訴我們你有什麼卡</string>

View file

@ -188,8 +188,6 @@
<string name="disclaimer_title">Terms of Service</string>
<string name="error_update_app">Oops, the current version of the application is not ready to work with this card, please check for updates.</string>
<string name="error_wrong_wallet_tapped">You have used a card from another wallet. Tap the card associated with this wallet</string>
<string name="exchange_receive_view_header">You receive</string>
<string name="exchange_send_view_header">You send</string>
<string name="exchange_tokens_available_tokens_header">My tokens</string>
<string name="exchange_tokens_empty_tokens">You haven\'t added any tokens yet. Add tokens via Market to swap</string>
<string name="express_token_list_empty_search">No tokens found. Please try another request</string>
@ -205,6 +203,7 @@
<string name="express_exchange_notification_failed_title">Operation failed by provider</string>
<string name="express_exchange_notification_verification_text">Visit providers website for verification</string>
<string name="express_exchange_notification_verification_title">KYC verification required by provider</string>
<string name="express_exchange_status_canceled">Canceled</string>
<string name="express_exchange_status_confirmed">Confirmed</string>
<string name="express_exchange_status_confirming">Confirming</string>
<string name="express_exchange_status_confirming_active">Confirming…</string>
@ -219,7 +218,7 @@
<string name="express_exchange_status_sending">Sending to you</string>
<string name="express_exchange_status_sending_active">Sending to you…</string>
<string name="express_exchange_status_sent">Sent</string>
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change.</string>
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change due to market conditions.</string>
<string name="express_exchange_status_title">Exchange status</string>
<string name="express_exchange_status_verified">Verified</string>
<string name="express_exchange_status_verifying">Verification required</string>
@ -534,6 +533,7 @@
<string name="swapping_approve_information_text">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.</string>
<string name="swapping_approve_information_title">Approve</string>
<string name="swapping_error_wrapper">Error: %s</string>
<string name="swapping_from_title">You swap</string>
<string name="swapping_generic_error">There was an error. Please try again.</string>
<string name="swapping_give_permission">Give Permission</string>
<string name="swapping_high_price_impact">High price impact!</string>
@ -552,13 +552,12 @@
<string name="swapping_permission_rows_your_wallet">Your wallet</string>
<string name="swapping_permission_subheader">To continue, grant 1inch smart contracts permission to use your %s</string>
<string name="swapping_permission_unlimited">Unlimited</string>
<string name="swapping_success_from_title">You swap</string>
<string name="swapping_success_to_title">You receive</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_action">Swap</string>
<string name="swapping_swap_of_to">Swap %s for</string>
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
<string name="swapping_to_title">You receive</string>
<string name="swapping_token_list_other_tokens">Other tokens</string>
<string name="swapping_token_list_title">Choose token</string>
<string name="swapping_token_list_your_tokens">Your tokens</string>

View file

@ -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(

View file

@ -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

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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,
)
}
}

View file

@ -17,4 +17,5 @@ enum class ExchangeStatus {
Sending,
Finished,
Refunded,
Cancelled,
}

View file

@ -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<Boolean>,
private val appCurrencyProvider: Provider<AppCurrency>,
) : Converter<CurrenciesGroup, SwapSelectTokenStateHolder> {
) : Converter<CurrenciesGroupWithFromCurrency, SwapSelectTokenStateHolder> {
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,
)
}

View file

@ -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,
)

View file

@ -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
}

View file

@ -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,
)
}

View file

@ -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<SwapWarning>,
) {
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")

View file

@ -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 = {},
),

View file

@ -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,

View file

@ -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,

View file

@ -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, SwapState>): 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<SwapProvider, SwapState>.consideredProvidersStates(): Map<SwapProvider, SwapState> {
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

View file

@ -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,
)

View file

@ -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,

View file

@ -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<ExchangeStatusState> {
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 {

View file

@ -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

View file

@ -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(

View file

@ -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
}

View file

@ -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<AppCurrency>,
private val analyticsEventsHandlerProvider: Provider<AnalyticsEventHandler>,
private val currentStateProvider: Provider<TokenDetailsState>,
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<SwapTransactionsState>) = 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) {

View file

@ -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()
}

View file

@ -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,
)

View file

@ -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,

View file

@ -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) }
}

View file

@ -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 ^