diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index 77fd07304f..194da3dfb2 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -66,6 +66,7 @@
Одобрение
Внимание
Баланс: %s
+ Баланс
биометрическую аутентификацию
биометрией
Купить
@@ -85,9 +86,11 @@
Включено
Ошибка
Обменять
- Посмотреть историю транзакций
Обозреватель
+ Посмотреть историю
+ Посмотреть историю транзакций
Обозреватель
+ Комиссия
Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции.
Свое
Быстро
@@ -126,7 +129,6 @@
Отправить
Успешно
Обмен
- Комиссия
условия участия
Ошибка транзакции
Транзакции
@@ -192,14 +194,18 @@
Мои токены
У вас нет добавленных токенов. Добавьте токены для обмена
Недоступен для обмена с %s
+ Статус
Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами
Выберите провайдера
Чтобы узнать причину, посетите сайт провайдера
Операция не выполнена провайдером
Посетите сайт провайдера для проверки
Провайдер: требуется верификация
+ Получение наилучших курсов...
Провайдер
Лучший курс
+ Доступно с %s
+ Недоступно для этой пары
Требуется разрешение
Информация ниже не является обязательной. Вы можете стереть её, если хотите.
Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.
@@ -273,7 +279,6 @@
Введенные коды доступа не совпадают
Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?
Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.
- Баланс
Добавить резервную карту
Сканировать карту #%d
Создать резервную копию
@@ -634,12 +639,18 @@
Нравится
Понятно!
Очень круто!
+ Обновить
Вы находитесь в режиме демо
Демо режим включен
Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька.
Не для пользователя!
Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены.
Для работы с сетью необходим депозит
+ У вас в списке нет монет доступных для обмена с %s
+ Нет доступных токенов для обмена
+ Cервис временно недоступен
+ Пожалуйста, измените сумму для обмена
+ Сумма для обмена должна быть не менее %s
Возможно, данная карта - образец или подделка
Ошибка проверки подлинности
На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index ffc1858e33..8d4ff0b882 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -64,6 +64,7 @@
Approval
Attention
Balance: %s
+ Balance
biometric authentication
biometrics
Buy
@@ -84,8 +85,10 @@
Error
Exchange
Explore
+ Explore history
Explore transaction history
Explorer
+ Fee
Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority.
Custom
Fast
@@ -125,7 +128,6 @@
Submit
Success
Swap
- Fee
terms and conditions
Transaction failed
Transactions
@@ -186,14 +188,16 @@
Terms of Service
Oops, the current version of the application is not ready to work with this card, please check for updates.
You have used a card from another wallet. Tap the card associated with this wallet
- You Receive
- You Send
+ You receive
+ You send
My tokens
- You don\'t have any added tokens yet. Add tokens via Market to swap
- Unavailable for swap from %s
- Providers facilitate transactions, ensuring smooth and efficient token exchanges
- Choose Provider
+ You haven\'t added any tokens yet. Add tokens via Market to swap
+ Cannot be swapped for %s
+ Status
+ Providers facilitate transactions, ensuring smooth and efficient token swaps
+ Choose provider
Estimated amount
+ Exchange by %s
Visit provider’s website to see why
Operation failed by provider
Visit provider’s website for verification
@@ -216,7 +220,8 @@
Go to provider
Provider
Best rate
- Exchange by %s
+ Available from %s
+ Unavailable for this pair
Permission Needed
The following information is optional. You can erase it if you don\'t want to share it.
Tell us what functions you are missing, and we will try to help you.
@@ -291,7 +296,6 @@
Entered access code didn\'t match the initial access code
You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?
The backup process is partly complete. You can\'t exit it now.
- Balance
Add a backup card
Scan the card #%d
Backup now
@@ -437,7 +441,6 @@
Invalid Memo. It won\'t be added to the transaction.
Tag
Memo
- Insufficient funds for transfer
Include fee
Low
Normal
@@ -513,7 +516,7 @@
Meet Tangem
Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services
Web 3.0 Compatible
- Approvals are considered an industry standard across all decentralized exchanges and protect your wallet from being accessed by a smart contract without your permission. By design, smart contracts can\'t access your tokens unless you approve access from your end. By \"unlocking\" your tokens, you are give permission to the 1inch smart contract to spend your assets. The miners of the network are compensated with a gas fee (paid by you) to record this action on the blockchain. Once permission has been granted you will be able to swap your token.
+ All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval.
Approve
Error: %s
There was an error. Please try again.
@@ -521,25 +524,25 @@
High price impact!
Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.
Insufficient funds
- Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first.
+ Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first.
Transaction in progress...
Waiting
Approve
Current transaction
- The token approval network fee will be charged to confirm that you are the one allowing your token to be used for the exchange.
+ The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.
Give Permission
Specify the approve limit for the selected token
Amount %s
Spender
- Your Wallet
- To continue you need to allow 1inch smart contracts to use your %s
+ Your wallet
+ To continue, grant 1inch smart contracts permission to use your %s
Unlimited
- View in Explorer
- In progress
You swap
You receive
+ View in Explorer
+ In progress
Swap
- Swap of %s to
+ Swap %s for
Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.
Other tokens
Choose token
@@ -661,12 +664,20 @@
Like it
Ok, Got it!
Really cool!
+ Refresh
You are currently in the Demo mode
Demo mode active
The card you scanned is a developer card. Do not use it to create your wallet.
Not for users!
%1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed.
Network requires Existential Deposit
+ You do not have any %s exchangeable coins in your list
+ No available tokens to swap
+ To make a transaction you need to deposit some %1$s %2$s
+ Unable to cover %s fee
+ Service temporary unavailable
+ Please change the amount to swap
+ The amount to swap must be at least %s
This card might be a production sample or counterfeit
Authenticity check failed
Only %s signatures are left on this card. You must withdraw all of your funds.
diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt
index f5a1bf13df..ae8d8d9373 100644
--- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt
+++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkOperations.kt
@@ -26,6 +26,7 @@ internal fun getNetwork(
name = blockchain.fullName,
isTestnet = blockchain.isTestnet(),
derivationPath = getNetworkDerivationPath(blockchain, extraDerivationPath, derivationStyleProvider),
+ currencySymbol = blockchain.currency,
standardType = getNetworkStandardType(blockchain),
)
}
diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt
index c57d5beee8..17274cfbe9 100644
--- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt
+++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Network.kt
@@ -22,6 +22,7 @@ data class Network(
val id: ID,
val backendId: String,
val name: String,
+ val currencySymbol: String,
val derivationPath: DerivationPath,
val isTestnet: Boolean,
val standardType: StandardType,
diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt
index 979479e010..7b61592137 100644
--- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt
+++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt
@@ -18,6 +18,8 @@ internal object MockNetworks {
name = "Network One",
isTestnet = false,
standardType = Network.StandardType.ERC20,
+ backendId = "network1",
+ currencySymbol = "ETH",
derivationPath = Network.DerivationPath.None,
)
@@ -26,6 +28,8 @@ internal object MockNetworks {
name = "Network Two",
isTestnet = false,
standardType = Network.StandardType.ERC20,
+ backendId = "network1",
+ currencySymbol = "ETH",
derivationPath = Network.DerivationPath.None,
)
@@ -34,6 +38,8 @@ internal object MockNetworks {
name = "Network Three",
isTestnet = false,
standardType = Network.StandardType.ERC20,
+ backendId = "network1",
+ currencySymbol = "ETH",
derivationPath = Network.DerivationPath.None,
)
diff --git a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
index 4b027a624d..9ab26bd83d 100644
--- a/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
+++ b/features/send/impl/src/main/java/com/tangem/features/send/impl/presentation/state/fields/SendAmountFieldConverter.kt
@@ -24,7 +24,7 @@ internal class SendAmountFieldConverter(
),
placeholder = TextReference.Str(DEFAULT_VALUE),
isError = false,
- error = TextReference.Res(R.string.send_insufficient_funds),
+ error = TextReference.Res(R.string.swapping_insufficient_funds),
)
}
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
index 61f1089d60..37ef409ce5 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt
@@ -121,6 +121,8 @@ sealed interface SwapWarning {
* @property priceImpact in format = 10 (means 10%)
*/
data class HighPriceImpact(val priceImpact: Int, val notificationConfig: NotificationConfig) : SwapWarning
+ data class TooSmallAmountWarning(val notificationConfig: NotificationConfig) : SwapWarning
+ data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning
}
enum class GenericWarningType {
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt
index 76844c8cd2..a4fa64ab24 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/UiActions.kt
@@ -19,4 +19,5 @@ data class UiActions(
val onSelectFeeType: (TxFee) -> Unit,
val onProviderClick: (String) -> Unit,
val onProviderSelect: (String) -> Unit,
+ val onBuyClick: () -> Unit,
)
\ No newline at end of file
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt
index 78feaf99a9..2d1d66ef31 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/ProviderState.kt
@@ -1,5 +1,7 @@
package com.tangem.feature.swap.models.states
+import com.tangem.core.ui.extensions.TextReference
+
sealed class ProviderState {
abstract val onProviderClick: ((String) -> Unit)?
@@ -32,7 +34,8 @@ sealed class ProviderState {
val name: String,
val type: String,
val iconUrl: String,
- val alertText: String,
+ val alertText: TextReference,
+ val selectionType: SelectionType,
override val onProviderClick: ((String) -> Unit)? = null,
) : ProviderState()
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt
index 4556f2d785..d0f47a62a2 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseFeeBottomSheet.kt
@@ -7,6 +7,7 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.SpacerH24
@@ -35,7 +36,7 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) {
modifier = Modifier.background(TangemTheme.colors.background.primary),
) {
Text(
- text = "Choose fee", // todo replace with strings
+ text = stringResource(R.string.common_fee_selector_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
@@ -53,9 +54,7 @@ private fun ChooseFeeBottomSheetContent(content: ChooseFeeBottomSheetConfig) {
FeeItemsBlock(content)
}
Text(
- text = "Network transaction fees are small charges paid to support network security, incentivize " +
- "validators," +
- " allocate resources, and determine transaction priority.", // todo replace with strings
+ text = stringResource(R.string.common_fee_selector_footer),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt
index 6d2bab0f4f..f11e395923 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt
@@ -10,13 +10,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
+import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig
import com.tangem.feature.swap.models.states.ProviderState
+import com.tangem.feature.swap.presentation.R
import kotlinx.collections.immutable.toImmutableList
@Composable
@@ -32,7 +35,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC
modifier = Modifier.background(TangemTheme.colors.background.primary),
) {
Text(
- text = "Choose provider",
+ text = stringResource(R.string.express_choose_providers_title),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
modifier = Modifier
@@ -40,7 +43,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC
.align(Alignment.CenterHorizontally),
)
Text(
- text = "Providers facilitate transactions, ensuring smooth and efficient token exchanges",
+ text = stringResource(R.string.express_choose_providers_subtitle),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
modifier = Modifier
@@ -99,7 +102,8 @@ private fun ChooseProviderBottomSheet_Preview() {
name = "1inch",
type = "DEX",
iconUrl = "",
- alertText = "Unavailable",
+ selectionType = ProviderState.SelectionType.SELECT,
+ alertText = stringReference("Unavailable"),
),
)
TangemTheme(isDark = false) {
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt
index 5d75293da0..99595d6398 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt
@@ -13,12 +13,15 @@ import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.R
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.SpacerH24
+import com.tangem.core.ui.extensions.resolveReference
+import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.swap.models.states.ProviderState
@@ -64,6 +67,7 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected
ProviderUnavailableState(
state = state,
modifier = modifier,
+ isSelected = isSelected,
)
}
is ProviderState.Empty -> {
@@ -72,6 +76,7 @@ fun ProviderItem(state: ProviderState, modifier: Modifier = Modifier, isSelected
}
}
+@Suppress("LongMethod")
@Composable
private fun ProviderContentState(
state: ProviderState.Content,
@@ -127,31 +132,42 @@ private fun ProviderContentState(
}
}
Row(
- modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
+ modifier = Modifier.padding(
+ top = TangemTheme.dimens.spacing8,
+ end = TangemTheme.dimens.spacing56,
+ ),
) {
Text(
text = state.rate,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
)
if (state.percentLowerThenBest != null) {
Text(
- text = "${state.percentLowerThenBest}%", // todo add to strings
+ text = "${state.percentLowerThenBest}%",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.warning,
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 1,
)
}
}
}
}
- ProviderChevron(state = state, isSelected = isSelected)
+ ProviderChevron(selectionType = state.selectionType, isSelected = isSelected)
}
}
@Composable
-private fun ProviderUnavailableState(state: ProviderState.Unavailable, modifier: Modifier = Modifier) {
+private fun ProviderUnavailableState(
+ state: ProviderState.Unavailable,
+ isSelected: Boolean,
+ modifier: Modifier = Modifier,
+) {
Box(modifier = modifier.fillMaxWidth()) {
Row {
val (alpha, colorFilter) = GRAY_SCALE_ALPHA to GrayscaleColorFilter
@@ -195,13 +211,15 @@ private fun ProviderUnavailableState(state: ProviderState.Unavailable, modifier:
)
}
Text(
- text = state.alertText,
+ text = state.alertText.resolveReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.tertiary,
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
)
}
}
+
+ ProviderChevron(selectionType = state.selectionType, isSelected = isSelected)
}
}
@@ -247,8 +265,8 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) {
}
@Composable
-private fun BoxScope.ProviderChevron(state: ProviderState.Content, isSelected: Boolean) {
- when (state.selectionType) {
+private fun BoxScope.ProviderChevron(selectionType: ProviderState.SelectionType, isSelected: Boolean) {
+ when (selectionType) {
ProviderState.SelectionType.NONE -> {
/* no-op */
}
@@ -396,7 +414,8 @@ private fun ProviderItem_Unavailable_Preview() {
name = "1inch",
type = "DEX",
iconUrl = "",
- alertText = "Unavailable",
+ selectionType = ProviderState.SelectionType.SELECT,
+ alertText = stringReference("Unavailable"),
)
Column {
TangemTheme(isDark = false) {
@@ -408,5 +427,11 @@ private fun ProviderItem_Unavailable_Preview() {
TangemTheme(isDark = true) {
ProviderItemBlock(state = state)
}
+
+ SpacerH24()
+
+ TangemTheme(isDark = true) {
+ ProviderItem(state = state, isSelected = true)
+ }
}
}
\ No newline at end of file
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
index 8dc00151bb..7d965aa10f 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt
@@ -6,10 +6,7 @@ import com.tangem.common.Provider
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.currency.tokenicon.converter.CryptoCurrencyToIconStateConverter
import com.tangem.core.ui.components.notifications.NotificationConfig
-import com.tangem.core.ui.extensions.TextReference
-import com.tangem.core.ui.extensions.resourceReference
-import com.tangem.core.ui.extensions.stringReference
-import com.tangem.core.ui.extensions.wrappedList
+import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.tokens.model.CryptoCurrency
@@ -123,9 +120,12 @@ internal class StateBuilder(
warnings = listOf(
SwapWarning.NoAvailableTokensToSwap(
notificationConfig = NotificationConfig(
- title = stringReference("No tokens"),
- subtitle = stringReference("Swap tokens not available"),
- iconResId = R.drawable.ic_alert_24,
+ title = resourceReference(R.string.warning_express_no_exchangeable_coins_title),
+ subtitle = resourceReference(
+ id = R.string.warning_express_no_exchangeable_coins_description,
+ formatArgs = wrappedList(fromToken.currency.name),
+ ),
+ iconResId = R.drawable.img_attention_20,
),
),
),
@@ -213,6 +213,19 @@ internal class StateBuilder(
),
)
}
+ if (quoteModel.preparedSwapConfigState.isAllowedToSpend &&
+ !quoteModel.preparedSwapConfigState.isFeeEnough &&
+ quoteModel.preparedSwapConfigState.isBalanceEnough
+ ) {
+ warnings.add(
+ SwapWarning.UnableToCoverFeeWarning(
+ createUnableToCoverFeeNotificationConfig(
+ fromToken = fromToken,
+ onBuyClick = actions.onBuyClick,
+ ),
+ ),
+ )
+ }
if (!quoteModel.preparedSwapConfigState.isBalanceEnough) {
warnings.add(SwapWarning.InsufficientFunds)
}
@@ -430,7 +443,7 @@ internal class StateBuilder(
return FeeItemState.Content(
feeType = feeType,
- title = stringReference("Fee"), // todo replace with string
+ title = resourceReference(R.string.common_fee_label),
amountCrypto = fee.feeCryptoFormatted,
symbolCrypto = fee.cryptoSymbol,
amountFiatFormatted = fee.feeFiatFormatted,
@@ -602,17 +615,27 @@ internal class StateBuilder(
)
}
+ @Suppress("LongParameterList")
fun showSelectProviderBottomSheet(
uiState: SwapStateHolder,
selectedProviderId: String,
+ bestRatedProviderId: String,
providersStates: Map,
+ unavailableProviders: List,
onDismiss: () -> Unit,
): SwapStateHolder {
+ val availableProvidersStates = providersStates.entries.mapNotNull {
+ it.convertToProviderState(bestRatedProviderId, actions.onProviderSelect)
+ }
+ val unavailableProviderStates = unavailableProviders.map {
+ it.convertToUnavailableProviderState(
+ alertText = resourceReference(R.string.express_provider_not_available),
+ selectionType = ProviderState.SelectionType.NONE,
+ )
+ }
val config = ChooseProviderBottomSheetConfig(
selectedProviderId = selectedProviderId,
- providers = providersStates.entries
- .mapNotNull { it.convertToProviderState(actions.onProviderSelect) }
- .toImmutableList(),
+ providers = (availableProvidersStates + unavailableProviderStates).toImmutableList(),
)
return uiState.copy(
bottomSheetConfig = TangemBottomSheetConfig(
@@ -683,7 +706,7 @@ internal class StateBuilder(
return listOf(
FeeItemState.Content(
feeType = this.normalFee.feeType,
- title = stringReference("Fee"), // todo replace with string
+ title = resourceReference(R.string.common_fee_label),
amountCrypto = this.normalFee.feeCryptoFormatted,
symbolCrypto = this.normalFee.cryptoSymbol,
amountFiatFormatted = this.normalFee.feeFiatFormatted,
@@ -692,7 +715,7 @@ internal class StateBuilder(
),
FeeItemState.Content(
feeType = this.priorityFee.feeType,
- title = stringReference("Fee"), // todo replace with string
+ title = resourceReference(R.string.common_fee_label),
amountCrypto = this.priorityFee.feeCryptoFormatted,
symbolCrypto = this.priorityFee.cryptoSymbol,
amountFiatFormatted = this.priorityFee.feeFiatFormatted,
@@ -703,21 +726,28 @@ internal class StateBuilder(
}
private fun Map.Entry.convertToProviderState(
+ bestRatedProviderId: String,
onProviderSelect: (String) -> Unit,
): ProviderState? {
val provider = this.key
return when (val state = this.value) {
is SwapState.EmptyAmountState -> null
- is SwapState.QuotesLoadedState -> provider.convertToContentClickableProviderState(
- fromTokenInfo = state.fromTokenInfo,
- toTokenInfo = state.toTokenInfo,
+ is SwapState.QuotesLoadedState -> provider.convertToContentSelectableProviderState(
+ isBestRate = provider.providerId == bestRatedProviderId,
+ state = state,
onProviderClick = onProviderSelect,
selectionType = ProviderState.SelectionType.SELECT,
)
- is SwapState.SwapError -> null
+ // todo handle error
+ is SwapState.SwapError -> provider.convertToUnavailableProviderState(
+ alertText = resourceReference(R.string.express_provider_min_amount, wrappedList("10")),
+ selectionType = ProviderState.SelectionType.NONE,
+ onProviderClick = onProviderSelect,
+ )
}
}
+ // region warnings
private fun createPermissionNotificationConfig(fromTokenSymbol: String): NotificationConfig {
return NotificationConfig(
title = resourceReference(R.string.swapping_permission_header),
@@ -737,6 +767,28 @@ internal class StateBuilder(
)
}
+ private fun createUnableToCoverFeeNotificationConfig(
+ fromToken: CryptoCurrency,
+ onBuyClick: () -> Unit,
+ ): NotificationConfig {
+ return NotificationConfig(
+ title = resourceReference(
+ R.string.warning_express_not_enough_fee_for_token_tx_title,
+ wrappedList(fromToken.network.name),
+ ),
+ subtitle = resourceReference(
+ R.string.warning_express_not_enough_fee_for_token_tx_description,
+ wrappedList(fromToken.network.name, fromToken.network.currencySymbol),
+ ),
+ iconResId = fromToken.networkIconResId,
+ buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig(
+ text = resourceReference(R.string.common_buy_currency, wrappedList(fromToken.name)),
+ onClick = onBuyClick,
+ ),
+ )
+ }
+ // end region
+
private fun getShortAddressValue(fullAddress: String): String {
check(fullAddress.length > ADDRESS_MIN_LENGTH) { "Invalid address" }
val firstAddressPart = fullAddress.substring(startIndex = 0, endIndex = ADDRESS_FIRST_PART_LENGTH)
@@ -774,6 +826,58 @@ internal class StateBuilder(
)
}
+ private fun SwapProvider.convertToContentSelectableProviderState(
+ isBestRate: Boolean,
+ state: SwapState.QuotesLoadedState,
+ selectionType: ProviderState.SelectionType,
+ onProviderClick: (String) -> Unit,
+ ): ProviderState {
+ val fromTokenInfo = state.fromTokenInfo
+ val toTokenInfo = state.toTokenInfo
+ val rate = toTokenInfo.tokenAmount.value.divide(
+ fromTokenInfo.tokenAmount.value,
+ toTokenInfo.cryptoCurrencyStatus.currency.decimals,
+ RoundingMode.HALF_UP,
+ )
+ val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol
+ val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol
+ val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol"
+ val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) {
+ ProviderState.AdditionalBadge.PermissionRequired
+ } else if (isBestRate) {
+ ProviderState.AdditionalBadge.BestTrade
+ } else {
+ ProviderState.AdditionalBadge.Empty
+ }
+ return ProviderState.Content(
+ id = this.providerId,
+ name = this.name,
+ iconUrl = this.imageLarge,
+ type = this.type.toString(),
+ rate = rateString,
+ additionalBadge = additionalBadge,
+ selectionType = selectionType,
+ percentLowerThenBest = null,
+ onProviderClick = onProviderClick,
+ )
+ }
+
+ private fun SwapProvider.convertToUnavailableProviderState(
+ alertText: TextReference,
+ selectionType: ProviderState.SelectionType,
+ onProviderClick: ((String) -> Unit)? = null,
+ ): ProviderState {
+ return ProviderState.Unavailable(
+ id = this.providerId,
+ name = this.name,
+ iconUrl = this.imageLarge,
+ type = this.type.toString(),
+ selectionType = selectionType,
+ alertText = alertText,
+ onProviderClick = onProviderClick,
+ )
+ }
+
private fun CryptoCurrencyStatus.getFormattedAmount(): String {
val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
index bf382cbc41..95fd4ce997 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt
@@ -256,6 +256,7 @@ private fun SwapWarnings(warnings: List) {
is SwapWarning.HighPriceImpact -> {
Notification(
config = warning.notificationConfig,
+ iconTint = TangemTheme.colors.icon.warning,
)
}
is SwapWarning.PermissionNeeded -> {
@@ -282,6 +283,17 @@ private fun SwapWarnings(warnings: List) {
config = warning.notificationConfig,
)
}
+ is SwapWarning.TooSmallAmountWarning -> {
+ Notification(
+ config = warning.notificationConfig,
+ iconTint = TangemTheme.colors.icon.warning,
+ )
+ }
+ is SwapWarning.UnableToCoverFeeWarning -> {
+ Notification(
+ config = warning.notificationConfig,
+ )
+ }
else -> {}
}
SpacerH8()
@@ -379,7 +391,7 @@ private val state = SwapStateHolder(
notificationConfig = NotificationConfig(
title = stringReference("No tokens"),
subtitle = stringReference("Swap tokens not available"),
- iconResId = R.drawable.ic_alert_24,
+ iconResId = R.drawable.img_attention_20,
),
),
),
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
index b89d4e193d..a577a98dfe 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
@@ -30,11 +30,14 @@ import com.tangem.feature.swap.ui.StateBuilder
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.Debouncer
import com.tangem.utils.coroutines.runCatching
+import com.tangem.utils.isNullOrZero
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber
+import java.math.BigDecimal
+import java.math.RoundingMode
import java.text.DecimalFormat
import java.text.NumberFormat
import java.util.Locale
@@ -286,29 +289,62 @@ internal class SwapViewModel @Inject constructor(
}
is SwapState.SwapError -> {
Timber.e("SwapError when loading quotes ${state.error}")
+ // todo handle when change token and error
uiState = stateBuilder.mapError(uiState, state.error) { startLoadingQuotesFromLastState() }
}
}
}
private fun updateLoadedQuotes(state: Map): Pair {
- val selectedSwapProvider = selectProvider(state)
+ val nonEmptyStates = state.filter { it.value !is SwapState.EmptyAmountState }
+ val selectedSwapProvider = if (nonEmptyStates.isNotEmpty()) {
+ selectProvider(state)
+ } else {
+ null
+ }
dataState = dataState.copy(
selectedProvider = selectedSwapProvider,
lastLoadedSwapStates = state,
)
- return state.entries.first { it.key == selectedSwapProvider }.toPair()
+ selectedSwapProvider?.let {
+ return state.entries.first { it.key == selectedSwapProvider }.toPair()
+ }
+ return state.entries.first().toPair()
}
private fun selectProvider(state: Map): SwapProvider {
- val currentSelected = dataState.selectedProvider
- return if (currentSelected != null && state.keys.contains(currentSelected)) {
- currentSelected
+ val stateSuccess = state
+ .filter { it.value is SwapState.QuotesLoadedState }
+ .mapValues { it.value as SwapState.QuotesLoadedState }
+ return if (stateSuccess.isNotEmpty()) {
+ val currentSelected = dataState.selectedProvider
+ if (currentSelected != null && state.keys.contains(currentSelected)) {
+ currentSelected
+ } else {
+ findBestQuoteProvider(stateSuccess) ?: stateSuccess.keys.first()
+ }
} else {
state.keys.first()
}
}
+ private fun findBestQuoteProvider(state: Map): SwapProvider? {
+ // finding best quotes
+ return state.mapValues {
+ if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() &&
+ !it.value.toTokenInfo.amountFiat.isNullOrZero()
+ ) {
+ it.value.fromTokenInfo.amountFiat.divide(
+ it.value.toTokenInfo.amountFiat,
+ it.value.toTokenInfo.cryptoCurrencyStatus.currency.decimals,
+ RoundingMode.HALF_UP,
+ )
+ } else {
+ BigDecimal.ZERO
+ }
+ }.minByOrNull { it.value }?.key
+ }
+
private fun fillLoadedDataState(
state: SwapState.QuotesLoadedState,
permissionState: PermissionDataState,
@@ -477,7 +513,6 @@ internal class SwapViewModel @Inject constructor(
private fun onTokenSelect(id: String) {
val tokens = dataState.tokensDataState ?: return
-
val foundToken = if (isOrderReversed) {
tokens.fromGroup.available.firstOrNull {
it.currencyStatus.currency.id.value == id
@@ -504,6 +539,7 @@ internal class SwapViewModel @Inject constructor(
dataState = dataState.copy(
fromCryptoCurrency = fromToken,
toCryptoCurrency = toToken,
+ selectedProvider = null,
)
startLoadingQuotes(
fromToken = fromToken,
@@ -674,10 +710,17 @@ internal class SwapViewModel @Inject constructor(
setupLoadedState(selectedProvider, updatedState, fromToken)
}
},
- onProviderClick = {
+ onProviderClick = { providerId ->
+ val states = dataState.lastLoadedSwapStates
+ .filter { it.value is SwapState.QuotesLoadedState }
+ .mapValues { it.value as SwapState.QuotesLoadedState }
+ val bestRatedProviderId = findBestQuoteProvider(states)?.providerId ?: providerId
+ val unavailableProviders = getUnavailableProvidersFor(dataState.lastLoadedSwapStates)
uiState = stateBuilder.showSelectProviderBottomSheet(
uiState = uiState,
- selectedProviderId = it,
+ selectedProviderId = providerId,
+ bestRatedProviderId = bestRatedProviderId,
+ unavailableProviders = unavailableProviders,
providersStates = dataState.lastLoadedSwapStates,
) { uiState = stateBuilder.dismissBottomSheet(uiState) }
},
@@ -694,6 +737,7 @@ internal class SwapViewModel @Inject constructor(
)
}
},
+ onBuyClick = {},
)
}
@@ -735,6 +779,18 @@ internal class SwapViewModel @Inject constructor(
return groupToFind.available.find { idToFind == it.currencyStatus.currency.id.value }?.providers ?: emptyList()
}
+ private fun getAllProviders(): List {
+ dataState.tokensDataState?.let { data ->
+ val allProviders =
+ data.fromGroup.available.flatMap { it.providers } + data.toGroup.available.flatMap { it.providers }
+ return allProviders.distinct()
+ } ?: return emptyList()
+ }
+
+ private fun getUnavailableProvidersFor(state: Map): List {
+ return getAllProviders().filterNot { it in state }
+ }
+
companion object {
private const val loggingTag = "SwapViewModel"
private const val INITIAL_AMOUNT = ""