Updated on 2026-08-14
This commit is contained in:
commit
eeaf9e775a
32 changed files with 309 additions and 184 deletions
|
|
@ -48,6 +48,9 @@ enum class ExchangeStatus {
|
|||
|
||||
@Json(name = "verifying")
|
||||
VERIFYING,
|
||||
|
||||
@Json(name = "cancelled")
|
||||
CANCELLED,
|
||||
}
|
||||
|
||||
data class ExchangeStatusError(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 provider’s 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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -17,4 +17,5 @@ enum class ExchangeStatus {
|
|||
Sending,
|
||||
Finished,
|
||||
Refunded,
|
||||
Cancelled,
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 = {},
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ^
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue