From 3aa5c4b9266f76a94b02c134f4b2ab81e75b7b37 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 10:48:36 +0300 Subject: [PATCH 001/102] Updated on 2026-08-14 --- .../local/preferences/utils/AppPreferencesStoreExt.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index 974b36147a..862962e88d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -2,6 +2,7 @@ package com.tangem.datasource.local.preferences.utils import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit +import com.squareup.moshi.JsonDataException import com.squareup.moshi.Types import com.tangem.datasource.local.preferences.AppPreferencesStore import kotlinx.coroutines.flow.Flow @@ -23,7 +24,13 @@ inline fun AppPreferencesStore.getObject(key: Preferences.Key AppPreferencesStore.getObject(key: Preferences.Key, default: T): Flow { val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types - return data.map { it[key]?.let(adapter::fromJson) ?: default } + return data.map { + try { + it[key]?.let(adapter::fromJson) ?: default + } catch (e: JsonDataException) { + default + } + } } /** From 3558e81bde77716c672a971b04caae5950d6cad2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 6 Dec 2023 11:39:05 +0300 Subject: [PATCH 002/102] Updated on 2026-08-14 --- .../utils/AppPreferencesStoreExt.kt | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt index 862962e88d..5622eb2351 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/utils/AppPreferencesStoreExt.kt @@ -12,7 +12,15 @@ import kotlinx.coroutines.flow.map /** Get flow of nullable data [T] by string [key] */ inline fun AppPreferencesStore.getObject(key: Preferences.Key): Flow { val adapter = moshi.adapter(T::class.java) - return data.map { it[key]?.let(adapter::fromJson) } + return data.map { preferences -> + preferences[key]?.let { + try { + adapter.fromJson(it) + } catch (e: JsonDataException) { + null + } + } + } } /** @@ -44,7 +52,13 @@ suspend inline fun AppPreferencesStore.getObjectSyncOrNull(key: Pref val adapter = moshi.adapter(T::class.java) // TODO: Support parameterized types return data.firstOrNull() ?.get(key) - ?.let(adapter::fromJson) + ?.let { + try { + adapter.fromJson(it) + } catch (e: JsonDataException) { + null + } + } } /** Get data [T] by string [key]. If data is not found, it returns [default] */ @@ -55,7 +69,13 @@ suspend inline fun AppPreferencesStore.getObjectSyncOrDefault( val adapter = moshi.adapter(T::class.java) return data.firstOrNull() ?.get(key) - ?.let(adapter::fromJson) + ?.let { + try { + adapter.fromJson(it) + } catch (e: JsonDataException) { + default + } + } ?: default } From 0d6be14aa3df07977652adc8df3f70e2e701cdde Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Dec 2023 14:04:43 +0300 Subject: [PATCH 003/102] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6b23060307..edc76744e7 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,9 +88,9 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-411" +tangemBlockchainSdk = "release-app_5.4-412" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-312" +tangemCardSdk = "release-app_5.4-315" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ # endregion Tangem From d74f3191bbfb36e9bfa989fd0e347b84b3bf2b6e Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Dec 2023 18:05:57 +0300 Subject: [PATCH 004/102] Updated on 2026-08-14 --- .../tangem/feature/swap/SwapRepositoryImpl.kt | 2 +- .../swap/converters/SwapPairInfoConverter.kt | 22 ++++++++++++++++--- .../feature/swap/domain/SwapInteractorImpl.kt | 12 +++++----- .../feature/swap/domain/SwapRepository.kt | 2 +- .../models/domain/PairsWithProviders.kt | 6 +++++ .../models/ui/TokensDataStateExpress.kt | 3 +++ .../feature/swap/viewmodels/SwapViewModel.kt | 13 +++++------ 7 files changed, 40 insertions(+), 20 deletions(-) create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 90358d75df..9e41c08329 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -60,7 +60,7 @@ internal class SwapRepositoryImpl @Inject constructor( override suspend fun getPairs( initialCurrency: LeastTokenInfo, currencyList: List, - ): List { + ): PairsWithProviders { return withContext(coroutineDispatcher.io) { val initial = NetworkLeastTokenInfo( contractAddress = initialCurrency.contractAddress, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index ff86a7e049..4ea0faf9cd 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -2,18 +2,20 @@ package com.tangem.feature.swap.converters import com.tangem.datasource.api.express.models.response.* import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo +import com.tangem.feature.swap.domain.models.domain.PairsWithProviders +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.utils.converter.Converter import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType as ExchangeProviderTypeDomain import com.tangem.feature.swap.domain.models.domain.SwapPairLeast as SwapPairDomain import com.tangem.feature.swap.domain.models.domain.SwapProvider as SwapPairProviderDomain -class SwapPairInfoConverter : Converter> { +class SwapPairInfoConverter : Converter { private val rateTypeConverter = RateTypeConverter() - override fun convert(value: SwapPairsWithProviders): List { + override fun convert(value: SwapPairsWithProviders): PairsWithProviders { val providersAdditionalMap = value.providers.associateBy { it.id } - return value.swapPair.map { pair -> + val pairs = value.swapPair.map { pair -> SwapPairDomain( from = LeastTokenInfo( contractAddress = pair.from.contractAddress, @@ -28,6 +30,20 @@ class SwapPairInfoConverter : Converter, - ): List { + ): PairsWithProviders { return repository.getPairs(initialCurrency, currenciesList) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt index 9f90d4dc3d..f68c41e255 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt @@ -9,7 +9,7 @@ import java.math.BigDecimal interface SwapRepository { - suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List): List + suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List): PairsWithProviders suspend fun getRates(currencyId: String, tokenIds: List): Map diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt new file mode 100644 index 0000000000..c642d3f52e --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt @@ -0,0 +1,6 @@ +package com.tangem.feature.swap.domain.models.domain + +data class PairsWithProviders( + val pairs: List, + val allProviders: List, +) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index 1d29633fec..60c8e749bd 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -1,16 +1,19 @@ package com.tangem.feature.swap.domain.models.ui import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo +import com.tangem.feature.swap.domain.models.domain.SwapProvider data class TokensDataStateExpress( val fromGroup: CurrenciesGroup, val toGroup: CurrenciesGroup, + val allProviders: List, ) { companion object { val EMPTY = TokensDataStateExpress( fromGroup = CurrenciesGroup(emptyList(), emptyList()), toGroup = CurrenciesGroup(emptyList(), emptyList()), + allProviders = emptyList(), ) } } 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 b2ba4e8a24..101200169e 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 @@ -791,7 +791,7 @@ internal class SwapViewModel @Inject constructor( val fromToken = dataState.fromCryptoCurrency ?: return@UiActions val amountToSwap = dataState.amount ?: return@UiActions val selectedProvider = dataState.selectedProvider ?: return@UiActions - uiState = stateBuilder.updateSelectedFeeBottomSheet(uiState, it.feeType) + uiState = stateBuilder.dismissBottomSheet(uiState) dataState = dataState.copy(selectedFee = it) viewModelScope.launch(dispatchers.io) { val updatedState = swapInteractor.updateQuotesStateWithSelectedFee( @@ -823,7 +823,7 @@ internal class SwapViewModel @Inject constructor( val fromToken = dataState.fromCryptoCurrency if (provider != null && swapState != null && fromToken != null) { analyticsEventHandler.send(SwapEvents.ProviderChosen(provider)) - uiState = stateBuilder.updateSelectedProvider(uiState, provider.providerId) + uiState = stateBuilder.dismissBottomSheet(uiState) setupLoadedState( provider = provider, state = swapState, @@ -915,15 +915,12 @@ internal class SwapViewModel @Inject constructor( } 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() + return dataState.tokensDataState?.allProviders ?: emptyList() } private fun getUnavailableProvidersFor(state: Map): List { - return getAllProviders().filterNot { it in state } + val availableProviders = state.keys.map { it.providerId } + return getAllProviders().filterNot { availableProviders.contains(it.providerId) } } private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { From 5d7aef4c6bcee98b2535f3609af92a5c06099447 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 13:09:31 +0300 Subject: [PATCH 005/102] Updated on 2026-08-14 --- .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 74a81982cb..bb1842d035 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 @@ -648,7 +648,7 @@ internal class StateBuilder( val fee = requireNotNull(dataState.selectedFee) val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency) - val fromAmount = requireNotNull(dataState.amount?.toBigDecimal()) + val fromAmount = toBigDecimalOrNull(requireNotNull(dataState.amount)) val toAmount = requireNotNull(dataState.swapDataModel?.toTokenAmount?.value) val providerState = uiState.providerState as ProviderState.Content @@ -1136,6 +1136,10 @@ internal class StateBuilder( return this.divide(to, min(decimals, MAX_DECIMALS_TO_SHOW), RoundingMode.HALF_UP) } + private fun toBigDecimalOrNull(text: String): BigDecimal? { + return text.replace(",", ".").toBigDecimalOrNull() + } + private companion object { const val ADDRESS_MIN_LENGTH = 11 const val ADDRESS_FIRST_PART_LENGTH = 7 From bec0253e6f2c54ca03341d5a54b1e4b2b336184d Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 8 Dec 2023 16:54:08 +0200 Subject: [PATCH 006/102] Updated on 2026-08-14 --- .../network/auth/ExpressAuthProviderImpl.kt | 31 ------------------- .../DefaultMarketCryptoCurrencyRepository.kt | 4 +++ .../feature/swap/domain/SwapInteractorImpl.kt | 3 +- 3 files changed, 6 insertions(+), 32 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt diff --git a/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt b/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt deleted file mode 100644 index 2a7f65eee3..0000000000 --- a/app/src/main/java/com/tangem/tap/network/auth/ExpressAuthProviderImpl.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.tangem.tap.network.auth - -import com.tangem.datasource.config.ConfigManager -import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.lib.auth.ExpressAuthProvider -import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator -import java.util.UUID - -class ExpressAuthProviderImpl( - private val userWalletsStore: UserWalletsStore, - private val configManager: ConfigManager, -) : ExpressAuthProvider, ExpressSessionIdGenerator { - - private var uuid = UUID.randomUUID() - - override fun getApiKey(): String { - return configManager.config.tangemExpressApiKey - } - - override fun getUserId(): String { - return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: "" - } - - override fun getSessionId(): String { - return uuid.toString() - } - - override fun generateNewSessionId() { - uuid = UUID.randomUUID() - } -} \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt index 84188ea775..a210d96273 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultMarketCryptoCurrencyRepository.kt @@ -11,6 +11,10 @@ class DefaultMarketCryptoCurrencyRepository( ) : MarketCryptoCurrencyRepository { override suspend fun isExchangeable(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { + return getExchangeableFlag(userWalletId, cryptoCurrency) && !cryptoCurrency.isCustom + } + + private suspend fun getExchangeableFlag(userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency): Boolean { val contractAddress = (cryptoCurrency as? CryptoCurrency.Token)?.contractAddress ?: EMPTY_CONTRACT_ADDRESS_VALUE return assetsStore.getSyncOrNull(userWalletId)?.find { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 96303ba8ad..f1af16641e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -79,7 +79,8 @@ internal class SwapInteractorImpl @Inject constructor( val currencyFilter = it.currency.network.backendId != currency.network.backendId || it.currency.getContractAddress() != currency.getContractAddress() val statusFilter = it.value is CryptoCurrencyStatus.Loaded - statusFilter && currencyFilter + val notCustomTokenFilter = !it.currency.isCustom + statusFilter && currencyFilter && notCustomTokenFilter } if (walletCurrencyStatusesExceptInitial.isEmpty()) { From df9a00188b8830771688a4b9c074edd80229d75e Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 16:26:53 +0300 Subject: [PATCH 007/102] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 101200169e..356dde9be4 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 @@ -384,7 +384,7 @@ internal class SwapViewModel @Inject constructor( val stateSuccess = state.getLastLoadedSuccessStates() return if (stateSuccess.isNotEmpty()) { val currentSelected = dataState.selectedProvider - if (currentSelected != null && state.keys.contains(currentSelected)) { + if (currentSelected != null && stateSuccess.keys.contains(currentSelected)) { currentSelected } else { findBestQuoteProvider(stateSuccess) ?: stateSuccess.keys.first() From 6844163be4c78ed239417695e7c924626135cf9d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 16:56:59 +0300 Subject: [PATCH 008/102] Updated on 2026-08-14 --- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 87dee2d3d9..61962b7ca5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -118,12 +118,12 @@ class GetCryptoCurrencyActionsUseCase( val isMulticurrencyWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() // swap - if (isMulticurrencyWallet && - marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency) - ) { - activeList.add(TokenActionsState.ActionState.Swap(true)) - } else { - disabledList.add(TokenActionsState.ActionState.Swap(false)) + if (isMulticurrencyWallet) { + if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)) { + activeList.add(TokenActionsState.ActionState.Swap(true)) + } else { + disabledList.add(TokenActionsState.ActionState.Swap(false)) + } } // buy @@ -164,11 +164,13 @@ class GetCryptoCurrencyActionsUseCase( return activeList + disabledList } - private fun isSendDisabled(cryptoCurrencyStatus: CryptoCurrencyStatus, coinStatus: CryptoCurrencyStatus?): Boolean = - cryptoCurrencyStatus.value.amount.isNullOrZero() || - coinStatus?.value?.amount.isNullOrZero() || - currenciesRepository.hasPendingTransactions( - cryptoCurrencyStatus = cryptoCurrencyStatus, - coinStatus = coinStatus, - ) + private fun isSendDisabled( + cryptoCurrencyStatus: CryptoCurrencyStatus, + coinStatus: CryptoCurrencyStatus?, + ): Boolean = cryptoCurrencyStatus.value.amount.isNullOrZero() || + coinStatus?.value?.amount.isNullOrZero() || + currenciesRepository.hasPendingTransactions( + cryptoCurrencyStatus = cryptoCurrencyStatus, + coinStatus = coinStatus, + ) } \ No newline at end of file From de5bb054faffbfa79494e4b15d889daf9721e799 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 17:51:03 +0300 Subject: [PATCH 009/102] Updated on 2026-08-14 --- .../com/tangem/core/ui/components/Keyboard.kt | 25 +++---------------- .../com/tangem/feature/swap/ui/SwapScreen.kt | 12 +++++++++ .../feature/swap/ui/SwapScreenContent.kt | 7 ------ .../feature/swap/ui/SwapSelectTokenScreen.kt | 3 ++- 4 files changed, 18 insertions(+), 29 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt index 4baf118b61..a920fda100 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/Keyboard.kt @@ -1,13 +1,10 @@ package com.tangem.core.ui.components import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.ime -import androidx.compose.foundation.layout.navigationBars import androidx.compose.runtime.Composable import androidx.compose.runtime.State -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -25,23 +22,9 @@ sealed interface Keyboard { /** * Allows to subscribe to a soft keyboard to detect when it's open/closed */ -@Deprecated("Use Modifier.imePadding() on pure Compose screens (without XML layouts)") @Composable fun keyboardAsState(): State { - val density = LocalDensity.current - val imeInsets = WindowInsets.ime - .exclude(WindowInsets.navigationBars) - .getBottom(density) - - return remember(imeInsets) { - derivedStateOf { - if (imeInsets > 0) { - Keyboard.Opened( - height = with(density) { imeInsets.toDp() }, - ) - } else { - Keyboard.Closed - } - } - } + val bottom = WindowInsets.ime.getBottom(LocalDensity.current) + val isImeVisible = bottom > 0 + return rememberUpdatedState(if (isImeVisible) Keyboard.Opened(bottom.dp) else Keyboard.Closed) } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index a70b6ce6f2..c25ea64c0b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -2,21 +2,33 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.material.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig +import com.tangem.feature.swap.presentation.R @Composable internal fun SwapScreen(stateHolder: SwapStateHolder) { BackHandler(onBack = stateHolder.onBackClicked) Scaffold( + modifier = Modifier.systemBarsPadding(), + topBar = { + AppBarWithBackButton( + text = stringResource(R.string.common_swap), + onBackClick = stateHolder.onBackClicked, + iconRes = R.drawable.ic_close_24, + ) + }, containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> 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 949ead0f64..ace1d19a31 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 @@ -17,7 +17,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.Strings.STARS import com.tangem.core.ui.components.* -import com.tangem.core.ui.components.appbar.AppBarWithBackButton import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.getActiveIconResByCoinId @@ -40,12 +39,6 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi .background(color = TangemTheme.colors.background.secondary), ) { Column { - AppBarWithBackButton( - text = stringResource(R.string.common_swap), - onBackClick = state.onBackClicked, - iconRes = R.drawable.ic_close_24, - ) - Column( modifier = Modifier .fillMaxWidth() diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 82b05a7c86..4057aba9af 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -97,7 +97,8 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = LazyColumn( modifier = modifier .background(color = screenBackgroundColor) - .fillMaxSize(), + .fillMaxSize() + .imePadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { item { SpacerH8() } From 2516b9e9b9fc1fc7b0f100205be30b59982f97c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 19:07:22 +0300 Subject: [PATCH 010/102] Updated on 2026-08-14 --- .../swap/models/states/ProviderState.kt | 31 ++++++++++++++----- .../swap/ui/ChooseProviderBottomSheet.kt | 3 +- .../tangem/feature/swap/ui/ProviderItem.kt | 9 ++++-- .../tangem/feature/swap/ui/StateBuilder.kt | 19 +++++++----- .../feature/swap/viewmodels/SwapViewModel.kt | 17 +++++----- 5 files changed, 53 insertions(+), 26 deletions(-) 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 84254a5716..265631000e 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 @@ -25,7 +25,7 @@ sealed class ProviderState { val subtitle: TextReference, val selectionType: SelectionType, val additionalBadge: AdditionalBadge, - val percentLowerThenBest: Float = 0f, + val percentLowerThenBest: PercentLowerThanBest = PercentLowerThanBest.Empty, override val onProviderClick: (String) -> Unit, ) : ProviderState() @@ -50,18 +50,35 @@ sealed class ProviderState { } } +sealed class PercentLowerThanBest { + data class Value(val value: Float) : PercentLowerThanBest() + object Empty : PercentLowerThanBest() +} + object ProviderPercentDiffComparator : Comparator { override fun compare(o1: ProviderState, o2: ProviderState): Int { if (o1 is ProviderState.Content && o2 !is ProviderState.Content) { - return 1 - } - if (o1 !is ProviderState.Content && o2 is ProviderState.Content) { return -1 } - return if (o1 is ProviderState.Content && o2 is ProviderState.Content) { - o1.percentLowerThenBest.compareTo(o2.percentLowerThenBest) + if (o1 !is ProviderState.Content && o2 is ProviderState.Content) { + return 1 + } + if (o1 is ProviderState.Content && o2 is ProviderState.Content) { + val o1Percent = o1.percentLowerThenBest + val o2Percent = o2.percentLowerThenBest + if (o1Percent is PercentLowerThanBest.Value && o2Percent !is PercentLowerThanBest.Value) { + return -1 + } + if (o1Percent !is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) { + return 1 + } + return if (o1Percent is PercentLowerThanBest.Value && o2Percent is PercentLowerThanBest.Value) { + o1Percent.value.compareTo(o2Percent.value) + } else { + 0 + } } else { - 0 + return 0 } } } \ No newline at end of file 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 2dd33c473f..363085e36c 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 @@ -17,6 +17,7 @@ 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.PercentLowerThanBest import com.tangem.feature.swap.models.states.ProviderState import com.tangem.feature.swap.presentation.R import kotlinx.collections.immutable.toImmutableList @@ -89,7 +90,7 @@ private fun ChooseProviderBottomSheet_Preview() { iconUrl = "", subtitle = stringReference("1 000 000"), additionalBadge = ProviderState.AdditionalBadge.BestTrade, - percentLowerThenBest = -1.0f, + percentLowerThenBest = PercentLowerThanBest.Value(-1.0f), selectionType = ProviderState.SelectionType.SELECT, onProviderClick = {}, ), 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 e5fe4ef2c9..51f6936885 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 @@ -24,6 +24,7 @@ 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.PercentLowerThanBest import com.tangem.feature.swap.models.states.ProviderState /** @@ -151,8 +152,10 @@ private fun ProviderContentState( maxLines = 1, ) } - if (state.percentLowerThenBest > 0f) { - AnimatedContent(targetState = state.percentLowerThenBest, label = "") { + if (state.percentLowerThenBest is PercentLowerThanBest.Value && + state.percentLowerThenBest.value > 0 + ) { + AnimatedContent(targetState = state.percentLowerThenBest.value, label = "") { Text( text = "-$it%", style = TangemTheme.typography.body2, @@ -404,7 +407,7 @@ private fun ProviderItem_Content_Preview() { iconUrl = "", subtitle = stringReference("1 000 000"), additionalBadge = ProviderState.AdditionalBadge.PermissionRequired, - percentLowerThenBest = -1.0f, + percentLowerThenBest = PercentLowerThanBest.Value(-1.0f), selectionType = ProviderState.SelectionType.SELECT, onProviderClick = {}, ) 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 bb1842d035..af2f4cb3db 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 @@ -789,7 +789,7 @@ internal class StateBuilder( fun showSelectProviderBottomSheet( uiState: SwapStateHolder, selectedProviderId: String, - pricesLowerBest: Map, + pricesLowerBest: Map, providersStates: Map, unavailableProviders: List, onDismiss: () -> Unit, @@ -820,6 +820,7 @@ internal class StateBuilder( fun updateProvidersBottomSheetContent( uiState: SwapStateHolder, + pricesLowerBest: Map, tokenSwapInfoForProviders: Map, ): SwapStateHolder { val config = uiState.bottomSheetConfig?.content as? ChooseProviderBottomSheetConfig @@ -835,6 +836,9 @@ internal class StateBuilder( .getFormattedCryptoAmount(tokenInfo.cryptoCurrencyStatus.currency) it.copy( subtitle = stringReference(rateString), + percentLowerThenBest = pricesLowerBest[it.id]?.let { percent -> + PercentLowerThanBest.Value(percent) + } ?: PercentLowerThanBest.Empty, ) } else { it @@ -928,7 +932,7 @@ internal class StateBuilder( } private fun Map.Entry.convertToProviderBottomSheetState( - pricesLowerBest: Map, + pricesLowerBest: Map, onProviderSelect: (String) -> Unit, ): ProviderState? { val provider = this.key @@ -1040,7 +1044,7 @@ internal class StateBuilder( subtitle = stringReference(rateString), additionalBadge = badge, selectionType = selectionType, - percentLowerThenBest = ZERO_PERCENT, + percentLowerThenBest = PercentLowerThanBest.Empty, onProviderClick = onProviderClick, ) } @@ -1049,7 +1053,7 @@ internal class StateBuilder( isBestRate: Boolean, state: SwapState.QuotesLoadedState, selectionType: ProviderState.SelectionType, - pricesLowerBest: Map, + pricesLowerBest: Map, onProviderClick: (String) -> Unit, ): ProviderState { val toTokenInfo = state.toTokenInfo @@ -1069,7 +1073,9 @@ internal class StateBuilder( subtitle = stringReference(rateString), additionalBadge = additionalBadge, selectionType = selectionType, - percentLowerThenBest = pricesLowerBest[this] ?: ZERO_PERCENT, + percentLowerThenBest = pricesLowerBest[this.providerId]?.let { percent -> + PercentLowerThanBest.Value(percent) + } ?: PercentLowerThanBest.Value(0f), onProviderClick = onProviderClick, ) } @@ -1103,7 +1109,7 @@ internal class StateBuilder( selectionType = selectionType, subtitle = alertText, additionalBadge = ProviderState.AdditionalBadge.Empty, - percentLowerThenBest = ZERO_PERCENT, + percentLowerThenBest = PercentLowerThanBest.Empty, onProviderClick = onProviderClick, ) } @@ -1148,6 +1154,5 @@ internal class StateBuilder( private const val HUNDRED_PERCENTS = 100 private const val UNKNOWN_AMOUNT_SIGN = "—" private const val MAX_DECIMALS_TO_SHOW = 8 - private const val ZERO_PERCENT = 0f } } \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 356dde9be4..2f93839717 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 @@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.getOrElse -import arrow.core.mapNotNull import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter @@ -289,11 +288,13 @@ internal class SwapViewModel @Inject constructor( if (providersState.isNotEmpty()) { val (provider, state) = updateLoadedQuotes(providersState) setupLoadedState(provider, state, fromToken) + val successStates = providersState + .getLastLoadedSuccessStates() + val pricesLowerBest = getPricesLowerBest(successStates) uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, - tokenSwapInfoForProviders = providersState - .getLastLoadedSuccessStates() - .entries + pricesLowerBest = pricesLowerBest, + tokenSwapInfoForProviders = successStates.entries .associate { it.key.providerId to it.value.toTokenInfo }, ) } else { @@ -869,21 +870,21 @@ internal class SwapViewModel @Inject constructor( }?.key } - private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map { + private fun getPricesLowerBest(state: SuccessLoadedSwapData): Map { val bestRateEntry = state.maxByOrNull { it.value.toTokenInfo.tokenAmount.value } ?: return emptyMap() val bestRate = bestRateEntry.value.toTokenInfo.tokenAmount.value val hundredPercent = BigDecimal("100") - return state.mapNotNull { + return state.entries.mapNotNull { if (it.key != bestRateEntry.key) { val amount = it.value.toTokenInfo.tokenAmount.value val percentDiff = BigDecimal.ONE.minus( amount.divide(bestRate, RoundingMode.HALF_UP), ).multiply(hundredPercent) - percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue + it.key.providerId to percentDiff.setScale(2, RoundingMode.HALF_UP).toFloat().absoluteValue } else { null } - } + }.toMap() } private fun createSelectedAppCurrencyFlow(): StateFlow { From 98732196cda2a8068b8e439d241a63dbef56789b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 19:18:30 +0300 Subject: [PATCH 011/102] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index edc76744e7..0df8cabe1b 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-412" +tangemBlockchainSdk = "release-app_5.4-416" #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 ^ From 6ef85396728bf441d2eadde723869fcc2f02e220 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 18:26:02 +0200 Subject: [PATCH 012/102] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 2 +- .../tangem/feature/swap/SwapRepositoryImpl.kt | 72 +++++++++++-------- .../swap/domain/models/ExpressException.kt | 3 + .../tangem/feature/swap/ui/StateBuilder.kt | 10 ++- .../feature/swap/viewmodels/SwapViewModel.kt | 16 +++-- 5 files changed, 63 insertions(+), 40 deletions(-) create mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 345f06a0f6..cbd1bf2dab 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -681,7 +681,7 @@ 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 + Service temporarily 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 diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 9e41c08329..042908ada3 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -9,6 +9,7 @@ import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.Token import com.tangem.blockchain.extensions.Result import com.tangem.data.tokens.utils.CryptoCurrencyFactory +import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi @@ -28,6 +29,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.SwapRepository import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* @@ -62,49 +64,61 @@ internal class SwapRepositoryImpl @Inject constructor( currencyList: List, ): PairsWithProviders { return withContext(coroutineDispatcher.io) { - val initial = NetworkLeastTokenInfo( - contractAddress = initialCurrency.contractAddress, - network = initialCurrency.network, - ) - val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) } - - val pairs = async { - getPairsInternal( - from = arrayListOf(initial), - to = currenciesList, + try { + val initial = NetworkLeastTokenInfo( + contractAddress = initialCurrency.contractAddress, + network = initialCurrency.network, ) + val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) } + + val pairsDeferred = async { + getPairsInternal( + from = arrayListOf(initial), + to = currenciesList, + ) + } + + val reversedPairsDeferred = async { + getPairsInternal( + from = currenciesList, + to = arrayListOf(initial), + ) + } + + val pairs = pairsDeferred.await().getOrThrow() + val reversedPairs = reversedPairsDeferred.await().getOrThrow() + + val allPairs = pairs + reversedPairs + + val providers = tangemExpressApi.getProviders().getOrThrow() + + return@withContext swapPairInfoConverter.convert( + SwapPairsWithProviders( + swapPair = allPairs, + providers = providers, + ), + ) + } catch (exception: Exception) { + if (exception is ApiResponseError.HttpException) { + throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: "")) + } else { + throw exception + } } - val reversedPairs = async { - getPairsInternal( - from = currenciesList, - to = arrayListOf(initial), - ) - } - - val allPairs = pairs.await() + reversedPairs.await() - - val providers = tangemExpressApi.getProviders().getOrThrow() - - return@withContext swapPairInfoConverter.convert( - SwapPairsWithProviders( - swapPair = allPairs, - providers = providers, - ), - ) } } private suspend fun getPairsInternal( from: List, to: List, - ): List { + ): ApiResponse> { return tangemExpressApi.getPairs( PairsRequestBody( from = from, to = to, ), - ).getOrThrow() + ) } override suspend fun getExchangeStatus(txId: String): Either { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt new file mode 100644 index 0000000000..cc416b82ca --- /dev/null +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt @@ -0,0 +1,3 @@ +package com.tangem.feature.swap.domain.models + +class ExpressException(val dataError: DataError) : Exception() \ 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 af2f4cb3db..28d54f59f5 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 @@ -579,14 +579,18 @@ internal class StateBuilder( } } - fun createInitialErrorState(uiState: SwapStateHolder, onRefreshClick: () -> Unit): SwapStateHolder { + fun createInitialErrorState( + uiState: SwapStateHolder, + code: Int, + onRefreshClick: () -> Unit, + ): SwapStateHolder { return uiState.copy( warnings = listOf( SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( title = TextReference.Res(R.string.warning_express_refresh_required_title), - subtitle = TextReference.EMPTY, - iconResId = R.drawable.ic_alert_circle_24, + subtitle = TextReference.Res(R.string.generic_error_code, wrappedList(code)), + iconResId = R.drawable.ic_alert_triangle_20, buttonsState = NotificationConfig.ButtonsState.PrimaryButtonConfig( text = TextReference.Res(R.string.warning_button_refresh), onClick = onRefreshClick, 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 2f93839717..4f736b29f7 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 @@ -18,6 +18,7 @@ import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider @@ -174,13 +175,14 @@ internal class SwapViewModel @Inject constructor( selectedCurrency = null, ) - uiState = stateBuilder.createInitialErrorState(uiState) { - uiState = stateBuilder.createInitialLoadingState( - initialCurrency = initialCryptoCurrency, - networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), - ) - initTokens() - } + uiState = + stateBuilder.createInitialErrorState(uiState, (it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code) { + uiState = stateBuilder.createInitialLoadingState( + initialCurrency = initialCryptoCurrency, + networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), + ) + initTokens() + } } } } From 98ae29a49b930d55628e4d946fd9a752559016b5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 19:31:03 +0300 Subject: [PATCH 013/102] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 4 ++++ .../feature/swap/domain/models/ui/TxState.kt | 4 ++++ .../com/tangem/feature/swap/ui/StateBuilder.kt | 14 ++++++-------- .../feature/swap/viewmodels/SwapViewModel.kt | 2 +- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index f1af16641e..6504c6d53a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -461,10 +461,12 @@ internal class SwapInteractorImpl @Inject constructor( amount, currencyToSend.symbol, ), + fromAmountValue = amount.value, toAmount = amountFormatter.formatSwapAmountToUI( swapData.toTokenAmount, currencyToGet.symbol, ), + toAmountValue = swapData.toTokenAmount.value, txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), timestamp = System.currentTimeMillis(), ) @@ -541,10 +543,12 @@ internal class SwapInteractorImpl @Inject constructor( amount, currencyToSend.currency.symbol, ), + fromAmountValue = amount.value, toAmount = amountFormatter.formatSwapAmountToUI( exchangeData.dataModel.toTokenAmount, currencyToGet.currency.symbol, ), + toAmountValue = exchangeData.dataModel.toTokenAmount.value, txAddress = userWalletManager.getLastTransactionHash( currencyToSend.currency.network.backendId, derivationPath, diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt index a593888610..526b5b2a73 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt @@ -1,10 +1,14 @@ package com.tangem.feature.swap.domain.models.ui +import java.math.BigDecimal + sealed class TxState { data class TxSent( val fromAmount: String? = null, + val fromAmountValue: BigDecimal? = null, val toAmount: String? = null, + val toAmountValue: BigDecimal? = null, val txAddress: String, val txExternalUrl: String? = null, val timestamp: Long, 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 28d54f59f5..1330cb8db3 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 @@ -643,7 +643,7 @@ internal class StateBuilder( @Suppress("LongParameterList") fun createSuccessState( uiState: SwapStateHolder, - timeStamp: Long, + txState: TxState.TxSent, txUrl: String, dataState: SwapProcessDataState, onExploreClick: () -> Unit, @@ -652,18 +652,16 @@ internal class StateBuilder( val fee = requireNotNull(dataState.selectedFee) val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) val toCryptoCurrency = requireNotNull(dataState.toCryptoCurrency) - val fromAmount = toBigDecimalOrNull(requireNotNull(dataState.amount)) - val toAmount = requireNotNull(dataState.swapDataModel?.toTokenAmount?.value) + val fromAmount = txState.fromAmountValue ?: BigDecimal.ZERO + val toAmount = txState.toAmountValue ?: BigDecimal.ZERO val providerState = uiState.providerState as ProviderState.Content - val fromCryptoAmount = BigDecimalFormatter.formatCryptoAmount(fromAmount, fromCryptoCurrency.currency) - val toCryptoAmount = BigDecimalFormatter.formatCryptoAmount(toAmount, toCryptoCurrency.currency) val fromFiatAmount = getFormattedFiatAmount(fromCryptoCurrency.value.fiatRate?.multiply(fromAmount)) val toFiatAmount = getFormattedFiatAmount(toCryptoCurrency.value.fiatRate?.multiply(toAmount)) return uiState.copy( successState = SwapSuccessStateHolder( - timestamp = timeStamp, + timestamp = txState.timestamp, txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), @@ -671,8 +669,8 @@ internal class StateBuilder( providerIcon = providerState.iconUrl, rate = providerState.subtitle, fee = stringReference("${fee.feeCryptoFormatted} (${fee.feeFiatFormatted})"), - fromTokenAmount = stringReference(fromCryptoAmount), - toTokenAmount = stringReference(toCryptoAmount), + fromTokenAmount = stringReference(txState.fromAmount.orEmpty()), + toTokenAmount = stringReference(txState.toAmount.orEmpty()), fromTokenFiatAmount = stringReference(fromFiatAmount), toTokenFiatAmount = stringReference(toFiatAmount), fromTokenIconState = iconStateConverter.convert(fromCryptoCurrency), 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 4f736b29f7..acf3fff110 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 @@ -460,7 +460,7 @@ internal class SwapViewModel @Inject constructor( ) uiState = stateBuilder.createSuccessState( uiState = uiState, - timeStamp = it.timestamp, + txState = it, dataState = dataState, txUrl = url, onExploreClick = { From 45b77429141402cf076a6b9febbecb3ff5f749ad Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 20:07:34 +0300 Subject: [PATCH 014/102] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/SwapRepositoryImpl.kt | 1 - .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 6 +----- .../com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 9 +++++++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 042908ada3..26ac346f90 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -105,7 +105,6 @@ internal class SwapRepositoryImpl @Inject constructor( throw exception } } - } } 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 1330cb8db3..6881df60df 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 @@ -579,11 +579,7 @@ internal class StateBuilder( } } - fun createInitialErrorState( - uiState: SwapStateHolder, - code: Int, - onRefreshClick: () -> Unit, - ): SwapStateHolder { + fun createInitialErrorState(uiState: SwapStateHolder, code: Int, onRefreshClick: () -> Unit): SwapStateHolder { return uiState.copy( warnings = listOf( SwapWarning.GeneralWarning( 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 acf3fff110..ac172210c3 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 @@ -176,10 +176,15 @@ internal class SwapViewModel @Inject constructor( ) uiState = - stateBuilder.createInitialErrorState(uiState, (it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code) { + stateBuilder.createInitialErrorState( + uiState, + (it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code, + ) { uiState = stateBuilder.createInitialLoadingState( initialCurrency = initialCryptoCurrency, - networkInfo = blockchainInteractor.getBlockchainInfo(initialCryptoCurrency.network.backendId), + networkInfo = blockchainInteractor.getBlockchainInfo( + initialCryptoCurrency.network.backendId, + ), ) initTokens() } From c19ca08957ef3a9a1e2f4adf9699af9b753f2d82 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Dec 2023 12:15:00 +0300 Subject: [PATCH 015/102] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 2 ++ core/res/src/main/res/values/strings.xml | 2 ++ features/swap/domain/build.gradle.kts | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 9 ++++-- .../models/domain/PreparedSwapConfigState.kt | 1 + .../swap/domain/models/ui/SwapState.kt | 1 + .../feature/swap/models/SwapStateHolder.kt | 2 ++ .../tangem/feature/swap/ui/ProviderItem.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 20 +++++++++++++ .../feature/swap/ui/SwapScreenContent.kt | 29 ++++++++++--------- 10 files changed, 52 insertions(+), 16 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 6fe946df8b..391fe57950 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -647,6 +647,8 @@ Не для пользователя! Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены. Для работы с сетью необходим депозит + Обмен будет доступен после завершения %s транзакции + У вас есть активная транзакция У вас в списке нет монет доступных для обмена с %s Нет доступных токенов для обмена Cервис временно недоступен diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index cbd1bf2dab..340dc79988 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -677,6 +677,8 @@ 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 + Swap will be available after the %s transaction is complete + You have active transaction 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 diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index f3841b400d..b1a50d2773 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.domain.demo) implementation(projects.domain.card) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.txhistory.models) /** Core modules */ implementation(projects.core.utils) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 6504c6d53a..f15db9df11 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -420,7 +420,6 @@ internal class SwapInteractorImpl @Inject constructor( permissionState = PermissionDataState.Empty, preparedSwapConfigState = state.preparedSwapConfigState.copy( isBalanceEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, - isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, includeFeeInAmount = includeFeeInAmount, ), ) @@ -798,6 +797,7 @@ internal class SwapInteractorImpl @Inject constructor( isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, isAllowedToSpend = isAllowedToSpend, isBalanceEnough = isBalanceWithoutFeeEnough, + hasOutgoingTransaction = hasOutgoingTransaction(fromToken), includeFeeInAmount = includeFeeInAmount, ), ) @@ -873,7 +873,7 @@ internal class SwapInteractorImpl @Inject constructor( /** * Load swap data calls only if spend is allowed for token contract address */ - @Suppress("LongParameterList") + @Suppress("LongParameterList", "LongMethod") private suspend fun loadDexSwapData( provider: SwapProvider, networkId: String, @@ -932,6 +932,7 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = true, isBalanceEnough = isBalanceIncludeFeeEnough, isFeeEnough = isFeeEnough, + hasOutgoingTransaction = hasOutgoingTransaction(fromToken), includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex ), ) @@ -1226,6 +1227,10 @@ internal class SwapInteractorImpl @Inject constructor( } } + private fun hasOutgoingTransaction(cryptoCurrencyStatuses: CryptoCurrencyStatus): Boolean { + return cryptoCurrencyStatuses.value.pendingTransactions.any { it.isOutgoing } + } + private fun Fee.getGasLimit(): Int { return when (this) { is Fee.Common -> 0 diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt index 53cfae4ded..ca44dc046e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt @@ -14,6 +14,7 @@ data class PreparedSwapConfigState( val isAllowedToSpend: Boolean, val isBalanceEnough: Boolean, val isFeeEnough: Boolean, + val hasOutgoingTransaction: Boolean, val includeFeeInAmount: IncludeFeeInAmount, ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index d031891ba3..d2788d35a4 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -19,6 +19,7 @@ sealed interface SwapState { isAllowedToSpend = false, isBalanceEnough = false, isFeeEnough = false, + hasOutgoingTransaction = false, includeFeeInAmount = IncludeFeeInAmount.Excluded, ), val permissionState: PermissionDataState = PermissionDataState.Empty, 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 e30eff225c..ae4d9ba3cf 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 @@ -6,6 +6,7 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState +import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.ui.TxFee import com.tangem.feature.swap.models.states.FeeItemState @@ -125,6 +126,7 @@ sealed interface SwapWarning { data class TooSmallAmountWarning(val notificationConfig: NotificationConfig) : SwapWarning data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning + data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning } enum class GenericWarningType { 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 51f6936885..9c3b990219 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 @@ -261,6 +261,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { CircularProgressIndicator( modifier = Modifier.size(TangemTheme.dimens.size16), color = TangemTheme.colors.icon.informative, + strokeWidth = TangemTheme.dimens.size2, ) Text( text = "Fetching best rates ...", 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 6881df60df..7042da2c25 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 @@ -320,6 +320,26 @@ internal class StateBuilder( ), ) } + if (quoteModel.permissionState is PermissionDataState.PermissionLoading) { + warnings.add( + SwapWarning.TransactionInProgressWarning( + title = resourceReference(R.string.swapping_pending_transaction_title), + description = resourceReference(R.string.swapping_pending_transaction_subtitle), + ), + ) + } else if (quoteModel.preparedSwapConfigState.hasOutgoingTransaction) { + warnings.add( + SwapWarning.TransactionInProgressWarning( + title = resourceReference(R.string.warning_express_active_transaction_title), + description = resourceReference( + id = R.string.warning_express_active_transaction_message, + formatArgs = wrappedList( + quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.network.currencySymbol, + ), + ), + ), + ) + } return warnings } 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 ace1d19a31..8d1fe2e6df 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 @@ -20,6 +20,7 @@ import com.tangem.core.ui.components.* import com.tangem.core.ui.components.notifications.Notification import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.extensions.getActiveIconResByCoinId +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.domain.models.ui.FeeType @@ -67,20 +68,6 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) - AnimatedVisibility(visible = state.permissionState is SwapPermissionState.InProgress) { - CardWithIcon( - title = stringResource(id = R.string.swapping_pending_transaction_title), - description = stringResource(id = R.string.swapping_pending_transaction_subtitle), - icon = { - CircularProgressIndicator( - modifier = Modifier - .size(TangemTheme.dimens.size16), - color = TangemTheme.colors.icon.primary1, - strokeWidth = TangemTheme.dimens.size2, - ) - }, - ) - } MainButton(state = state, onPermissionWarningClick = state.onShowPermissionBottomSheet) } } @@ -285,6 +272,20 @@ private fun SwapWarnings(warnings: List) { config = warning.notificationConfig, ) } + is SwapWarning.TransactionInProgressWarning -> { + CardWithIcon( + title = warning.title.resolveReference(), + description = warning.description.resolveReference(), + icon = { + CircularProgressIndicator( + modifier = Modifier + .size(TangemTheme.dimens.size16), + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + }, + ) + } else -> {} } SpacerH8() From 30fbc7a45afa375f04b781a4e313a75142f19a25 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Dec 2023 13:42:13 +0300 Subject: [PATCH 016/102] Updated on 2026-08-14 --- core/res/src/main/res/values/strings.xml | 2 +- .../src/main/java/com/tangem/feature/swap/ui/ProviderItem.kt | 3 ++- .../src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 340dc79988..08c1787e9b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -228,7 +228,7 @@ Best rate Available from %s Unavailable for this pair - Permission Needed + Permission Required 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. Please tell us what card do you have 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 9c3b990219..41adc2a361 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 @@ -14,6 +14,7 @@ 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.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import coil.compose.SubcomposeAsyncImage @@ -374,7 +375,7 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) { ), ) { Text( - text = "Permission required", + text = stringResource(id = R.string.express_provider_permission_needed), style = TangemTheme.typography.caption1, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), 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 7042da2c25..f2adfb4a9d 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 @@ -976,7 +976,7 @@ internal class StateBuilder( // region warnings private fun createPermissionNotificationConfig(fromTokenSymbol: String): NotificationConfig { return NotificationConfig( - title = resourceReference(R.string.swapping_permission_header), + title = resourceReference(R.string.express_provider_permission_needed), subtitle = resourceReference( id = R.string.swapping_permission_subheader, formatArgs = wrappedList(fromTokenSymbol), From 0f2b4464ef9fa022ec806901732ccda2ada32ebc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 21:37:28 +0200 Subject: [PATCH 017/102] Updated on 2026-08-14 --- .../com/tangem/feature/swap/ui/StateBuilder.kt | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) 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 f2adfb4a9d..472810b335 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 @@ -450,10 +450,14 @@ internal class StateBuilder( iconResId = R.drawable.ic_alert_circle_24, ), ) - is DataError.UnknownError -> SwapWarning.GeneralWarning( + else -> SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( title = resourceReference(R.string.common_error), - subtitle = resourceReference(R.string.swapping_generic_error), + subtitle = if (dataError.code == DataError.UnknownError.code){ + resourceReference(R.string.swapping_generic_error) + } else { + resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())) + }, iconResId = R.drawable.img_attention_20, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference(R.string.warning_button_refresh), @@ -461,13 +465,6 @@ internal class StateBuilder( ), ), ) - else -> SwapWarning.GeneralWarning( - notificationConfig = NotificationConfig( - title = resourceReference(R.string.common_error), - subtitle = resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())), - iconResId = R.drawable.img_attention_20, - ), - ) } } From 38b64433c05062f7c0121c75b6c2a4f7f692967b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 11 Dec 2023 21:40:15 +0200 Subject: [PATCH 018/102] Updated on 2026-08-14 --- .../src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 472810b335..38e2ac9240 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 @@ -453,7 +453,7 @@ internal class StateBuilder( else -> SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( title = resourceReference(R.string.common_error), - subtitle = if (dataError.code == DataError.UnknownError.code){ + subtitle = if (dataError.code == DataError.UnknownError.code) { resourceReference(R.string.swapping_generic_error) } else { resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())) From c9fee330ef024560d5ea6fd6f5ce488f432194f0 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Dec 2023 11:56:07 +0200 Subject: [PATCH 019/102] Updated on 2026-08-14 --- .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 8 ++++++-- .../com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 4 +--- 2 files changed, 7 insertions(+), 5 deletions(-) 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 38e2ac9240..2837f48f2a 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 @@ -452,8 +452,12 @@ internal class StateBuilder( ) else -> SwapWarning.GeneralWarning( notificationConfig = NotificationConfig( - title = resourceReference(R.string.common_error), - subtitle = if (dataError.code == DataError.UnknownError.code) { + title = if (dataError is DataError.UnknownError) { + resourceReference(R.string.common_error) + } else { + resourceReference(R.string.warning_express_refresh_required_title) + }, + subtitle = if (dataError is DataError.UnknownError) { resourceReference(R.string.swapping_generic_error) } else { resourceReference(R.string.generic_error_code, wrappedList(dataError.code.toString())) 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 ac172210c3..b17c398d2e 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 @@ -346,9 +346,7 @@ internal class SwapViewModel @Inject constructor( ) } is SwapState.SwapError -> { - if (state.error is DataError.UnknownError) { - singleTaskScheduler.cancelTask() - } + singleTaskScheduler.cancelTask() uiState = stateBuilder.createQuotesErrorState( uiStateHolder = uiState, swapProvider = provider, From b8fc09d6bf2d667ef8e12e8bcf969dfd3b6c8d82 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Dec 2023 17:38:17 +0300 Subject: [PATCH 020/102] Updated on 2026-08-14 --- .../features/demo/DemoTransactionSender.kt | 4 ++ .../DefaultWalletManagersFacade.kt | 16 ++++- .../walletmanager/WalletManagersFacade.kt | 10 +++ .../transaction/usecase/EstimateFeeUseCase.kt | 63 +++++++++++++++++++ .../feature/swap/domain/SwapInteractorImpl.kt | 9 ++- gradle/dependencies.toml | 2 +- 6 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt index 7e24b61c6d..17714e011b 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt @@ -21,6 +21,10 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact ) } + override suspend fun estimateFee(amount: Amount): Result { + return getFee(amount, walletManager.wallet.address) + } + override suspend fun send(transactionData: TransactionData, signer: TransactionSigner): SimpleResult { val signerResponse = signer.sign( hash = getDataToSign(), diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index d1a57e7726..cdffab603a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -38,7 +38,7 @@ import timber.log.Timber import java.math.BigDecimal import java.util.EnumSet -@Suppress("LargeClass") +@Suppress("LargeClass", "TooManyFunctions") // FIXME: Move to its own module and make internal @Deprecated("Inject the WalletManagerFacade interface using DI instead") class DefaultWalletManagersFacade( @@ -433,6 +433,20 @@ class DefaultWalletManagersFacade( ) } + override suspend fun estimateFee( + amount: Amount, + userWalletId: UserWalletId, + network: Network, + ): Result? { + val blockchain = Blockchain.fromId(network.id.value) + val walletManager = getOrCreateWalletManager( + userWalletId = userWalletId, + blockchain = blockchain, + derivationPath = network.derivationPath.value, + ) + return (walletManager as? TransactionSender)?.estimateFee(amount = amount) + } + override suspend fun validateTransaction( amount: Amount, fee: Amount?, diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt index dba5d089eb..548668c5a7 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/WalletManagersFacade.kt @@ -25,6 +25,7 @@ import java.util.EnumSet /** * A facade for managing wallets. */ +@Suppress("TooManyFunctions") interface WalletManagersFacade { /** @@ -166,6 +167,15 @@ interface WalletManagersFacade { network: Network, ): Result? + /** + * Returns estimated fee for transaction + * + * @param amount of transaction + * @param userWalletId selected wallet id + * @param network network of currency + */ + suspend fun estimateFee(amount: Amount, userWalletId: UserWalletId, network: Network): Result? + /** * Validates transaction * diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt new file mode 100644 index 0000000000..28f48358a0 --- /dev/null +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt @@ -0,0 +1,63 @@ +package com.tangem.domain.transaction.usecase + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.tangem.blockchain.common.Amount +import com.tangem.blockchain.common.AmountType +import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.transaction.TransactionFee +import com.tangem.blockchain.extensions.Result +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.transaction.error.GetFeeError +import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn +import java.math.BigDecimal + +/** + * Use case to estimate transaction fee + */ +class EstimateFeeUseCase( + private val walletManagersFacade: WalletManagersFacade, + private val dispatcher: CoroutineDispatcherProvider, +) { + suspend operator fun invoke( + amount: BigDecimal, + userWalletId: UserWalletId, + cryptoCurrency: CryptoCurrency, + ): Flow> { + return flow { + val result = walletManagersFacade.estimateFee( + amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), + userWalletId = userWalletId, + network = cryptoCurrency.network, + ) + + val maybeFee = when (result) { + is Result.Success -> result.data.right() + else -> GetFeeError.DataError.left() + } + emit(maybeFee) + }.flowOn(dispatcher.io) + } + + private fun convertCryptoCurrencyToAmount(cryptoCurrency: CryptoCurrency, amount: BigDecimal) = Amount( + currencySymbol = cryptoCurrency.symbol, + value = amount, + decimals = cryptoCurrency.decimals, + type = when (cryptoCurrency) { + is CryptoCurrency.Coin -> AmountType.Coin + is CryptoCurrency.Token -> AmountType.Token( + token = Token( + symbol = cryptoCurrency.symbol, + contractAddress = cryptoCurrency.contractAddress, + decimals = cryptoCurrency.decimals, + ), + ) + }, + ) +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index f15db9df11..3b32d69aec 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -13,7 +13,7 @@ import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount import com.tangem.domain.transaction.error.SendTransactionError -import com.tangem.domain.transaction.usecase.GetFeeUseCase +import com.tangem.domain.transaction.usecase.EstimateFeeUseCase import com.tangem.domain.transaction.usecase.SendTransactionUseCase import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet @@ -54,8 +54,8 @@ internal class SwapInteractorImpl @Inject constructor( private val initialToCurrencyResolver: InitialToCurrencyResolver, ) : SwapInteractor { - private val getFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { - GetFeeUseCase(walletManagersFacade, dispatcher) + private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { + EstimateFeeUseCase(walletManagersFacade, dispatcher) } private val swapCurrencyConverter = SwapCurrencyConverter() @@ -999,9 +999,8 @@ internal class SwapInteractorImpl @Inject constructor( networkId: String, ): TxFeeState { getSelectedWalletSyncUseCase().getOrNull()?.walletId?.let { userWalletId -> - val txFeeResult = getFeeUseCase( + val txFeeResult = estimateFeeUseCase( amount = amount.value, - destination = fromToken.value.networkAddress?.defaultAddress?.value ?: "", userWalletId = userWalletId, cryptoCurrency = fromToken.currency, ).firstOrNull() diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 0df8cabe1b..5557fb257d 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-416" +tangemBlockchainSdk = "release-app_5.4-417" #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 ^ From 6ea62cc4c6ab1156ed7b5084d4983c7d0a7fb36b Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Dec 2023 18:30:33 +0300 Subject: [PATCH 021/102] Updated on 2026-08-14 --- app/build.gradle.kts | 1 + features/swap/data/build.gradle.kts | 2 + .../tangem/feature/swap/SwapRepositoryImpl.kt | 29 ++-- .../tangem/feature/swap/di/SwapDataModule.kt | 2 +- features/swap/domain/api/.gitignore | 1 + features/swap/domain/api/build.gradle.kts | 18 +++ .../swap/domain/api}/SwapRepository.kt | 8 +- features/swap/domain/build.gradle.kts | 3 +- features/swap/domain/models/.gitignore | 1 + features/swap/domain/models/build.gradle.kts | 23 +++ .../feature/swap/domain/models/DataError.kt | 0 .../swap/domain/models/ExpressException.kt | 0 .../feature/swap/domain/models/SwapAmount.kt | 0 .../swap/domain/models/domain/Currency.kt | 0 .../domain/models/domain/ExchangeQuote.kt | 0 .../domain/models/domain/ExchangeStatus.kt | 0 .../models/domain/ExpressTransactionModel.kt | 0 .../domain/models/domain/LeastTokenInfo.kt | 0 .../swap/domain/models/domain/NetworkInfo.kt | 0 .../models/domain/PairsWithProviders.kt | 0 .../domain/models/domain/PermissionOptions.kt | 0 .../models/domain/PreparedSwapConfigState.kt | 0 .../swap/domain/models/domain/QuoteModel.kt | 0 .../domain/SavedLastSwappedCryptoCurrency.kt | 0 .../domain/SavedSwapTransactionListModel.kt | 0 .../domain/models/domain/SwapApproveType.kt | 0 .../domain/models/domain/SwapDataModel.kt | 0 .../domain/models/domain/SwapPairLeast.kt | 0 .../domain/models/domain/TransactionModel.kt | 0 .../swap/domain/models/ui/AmountFormatter.kt | 0 .../swap/domain/models/ui/SwapState.kt | 0 .../swap/domain/models/ui/TokensDataState.kt | 0 .../models/ui/TokensDataStateExpress.kt | 0 .../feature/swap/domain/models/ui/TxState.kt | 0 .../feature/swap/domain/SwapInteractor.kt | 5 +- .../feature/swap/domain/SwapInteractorImpl.kt | 153 +++++++++--------- .../swap/domain/di/SwapDomainModule.kt | 1 + .../models/data/AggregatedSwapDataModel.kt | 15 -- features/swap/presentation/build.gradle.kts | 9 +- .../tangem/feature/swap/ui/StateBuilder.kt | 4 +- .../feature/swap/viewmodels/SwapViewModel.kt | 2 +- features/tokendetails/impl/build.gradle.kts | 3 +- .../viewmodels/ExchangeStatusFactory.kt | 2 +- .../viewmodels/TokenDetailsViewModel.kt | 2 +- settings.gradle.kts | 4 + 45 files changed, 165 insertions(+), 123 deletions(-) create mode 100644 features/swap/domain/api/.gitignore create mode 100644 features/swap/domain/api/build.gradle.kts rename features/swap/domain/{src/main/java/com/tangem/feature/swap/domain => api/src/main/java/com/tangem/feature/swap/domain/api}/SwapRepository.kt (91%) create mode 100644 features/swap/domain/models/.gitignore create mode 100644 features/swap/domain/models/build.gradle.kts rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt (100%) rename features/swap/domain/{ => models}/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt (100%) delete mode 100644 features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index de5864d8b1..2791ba88f4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -87,6 +87,7 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.swap.presentation) implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.api) implementation(projects.features.swap.data) implementation(projects.features.tester.api) implementation(projects.features.tester.impl) diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 53dddfc5a9..a2b37ee319 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -15,6 +15,8 @@ dependencies { implementation(projects.core.datasource) implementation(projects.core.utils) implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.models) + implementation(projects.features.swap.domain.api) /** Network */ implementation(deps.retrofit) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 26ac346f90..ffd12827ba 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -1,8 +1,10 @@ package com.tangem.feature.swap import arrow.core.Either +import arrow.core.left import arrow.core.raise.catch import arrow.core.raise.either +import arrow.core.right import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.Approver import com.tangem.blockchain.common.Blockchain @@ -27,12 +29,11 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* -import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.createFromAmountWithOffset -import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel -import com.tangem.feature.swap.domain.models.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext @@ -184,7 +185,7 @@ internal class SwapRepositoryImpl @Inject constructor( toDecimals: Int, providerId: String, rateType: RateType, - ): AggregatedSwapDataModel { + ): Either { return withContext(coroutineDispatcher.io) { try { val response = tangemExpressApi.getExchangeQuote( @@ -198,14 +199,12 @@ internal class SwapRepositoryImpl @Inject constructor( providerId = providerId, rateType = rateType.name.lowercase(), ).getOrThrow() - AggregatedSwapDataModel( - dataModel = QuoteModel( - toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), - allowanceContract = response.allowanceContract, - ), - ) + QuoteModel( + toTokenAmount = createFromAmountWithOffset(response.toAmount, response.toDecimals), + allowanceContract = response.allowanceContract, + ).right() } catch (ex: Exception) { - AggregatedSwapDataModel(null, getDataError(ex)) + getDataError(ex).left() } } } @@ -227,7 +226,7 @@ internal class SwapRepositoryImpl @Inject constructor( providerId: String, rateType: RateType, toAddress: String, - ): AggregatedSwapDataModel { + ): Either { return withContext(coroutineDispatcher.io) { try { val response = tangemExpressApi.getExchangeData( @@ -242,11 +241,9 @@ internal class SwapRepositoryImpl @Inject constructor( rateType = rateType.name.lowercase(), toAddress = toAddress, ).getOrThrow() - AggregatedSwapDataModel( - dataModel = expressDataConverter.convert(response), - ) + expressDataConverter.convert(response).right() } catch (ex: Exception) { - AggregatedSwapDataModel(null, getDataError(ex)) + getDataError(ex).left() } } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index b30b20f817..83ddab6b62 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -13,7 +13,7 @@ import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.feature.swap.SwapRepositoryImpl import com.tangem.feature.swap.converters.ErrorsDataConverter import com.tangem.feature.swap.DefaultSwapTransactionRepository -import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module diff --git a/features/swap/domain/api/.gitignore b/features/swap/domain/api/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap/domain/api/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap/domain/api/build.gradle.kts b/features/swap/domain/api/build.gradle.kts new file mode 100644 index 0000000000..38ecfe752e --- /dev/null +++ b/features/swap/domain/api/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + id("configuration") +} + +android { + namespace = "com.tangem.feature.swap.domain.api" +} + +dependencies { + implementation(projects.features.swap.domain.models) + implementation(projects.domain.tokens.models) + implementation(projects.domain.wallets.models) + + implementation(deps.arrow.core) + +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt similarity index 91% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt rename to features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index f68c41e255..ea82fe6495 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -1,10 +1,10 @@ -package com.tangem.feature.swap.domain +package com.tangem.feature.swap.domain.api import arrow.core.Either import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.swap.domain.models.DataError import java.math.BigDecimal interface SwapRepository { @@ -28,7 +28,7 @@ interface SwapRepository { toDecimals: Int, providerId: String, rateType: RateType, - ): AggregatedSwapDataModel + ): Either /** * Returns address of 1inch router that must be trusted @@ -77,7 +77,7 @@ interface SwapRepository { providerId: String, rateType: RateType, toAddress: String, - ): AggregatedSwapDataModel + ): Either fun getNativeTokenForNetwork(networkId: String): CryptoCurrency } \ No newline at end of file diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index b1a50d2773..5fa5dfec86 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -29,6 +29,8 @@ dependencies { implementation(projects.domain.card) implementation(projects.domain.appCurrency.models) implementation(projects.domain.txhistory.models) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** Core modules */ implementation(projects.core.utils) @@ -38,7 +40,6 @@ dependencies { implementation(projects.features.wallet.api) /** Other Libraries **/ - implementation(deps.kotlin.serialization) implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.timber) diff --git a/features/swap/domain/models/.gitignore b/features/swap/domain/models/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/features/swap/domain/models/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/features/swap/domain/models/build.gradle.kts b/features/swap/domain/models/build.gradle.kts new file mode 100644 index 0000000000..96958add9b --- /dev/null +++ b/features/swap/domain/models/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + id("configuration") +} + +android { + namespace = "com.tangem.feature.swap.domain.models" +} + +dependencies { + /** Domain */ + implementation(projects.domain.tokens.models) + + /** Core modules */ + implementation(projects.core.utils) + + /** Other Libraries **/ + implementation(deps.kotlin.serialization) + implementation(deps.arrow.core) + +} \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ExpressException.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/SwapAmount.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Currency.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeQuote.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/LeastTokenInfo.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/NetworkInfo.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PairsWithProviders.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PermissionOptions.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/PreparedSwapConfigState.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/QuoteModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedLastSwappedCryptoCurrency.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SavedSwapTransactionListModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapApproveType.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapDataModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/SwapPairLeast.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/TransactionModel.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/AmountFormatter.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataState.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt similarity index 100% rename from features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt rename to features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index d36610f673..765647a01e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -5,7 +5,10 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount +import com.tangem.feature.swap.domain.models.domain.PermissionOptions +import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.* interface SwapInteractor { diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 3b32d69aec..3754e0c835 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -1,5 +1,6 @@ package com.tangem.feature.swap.domain +import arrow.core.Either import arrow.core.getOrElse import com.tangem.blockchain.common.Amount import com.tangem.blockchain.common.AmountType @@ -19,9 +20,10 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.converters.SwapCurrencyConverter +import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.data.AggregatedSwapDataModel import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.toStringWithRightOffset import com.tangem.feature.swap.domain.models.ui.* @@ -297,13 +299,14 @@ internal class SwapInteractorImpl @Inject constructor( ) val fromTokenAddress = getTokenAddress(fromToken.currency) - val isAllowedToSpend = if (quotes.dataModel != null) { - quotes.dataModel.allowanceContract?.let { - isAllowedToSpend(networkId, fromToken.currency, amount, it) - } ?: true - } else { - false - } + val isAllowedToSpend = quotes.fold( + ifRight = { + it.allowanceContract?.let { + isAllowedToSpend(networkId, fromToken.currency, amount, it) + } ?: true + }, + ifLeft = { false }, + ) if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) @@ -497,13 +500,13 @@ internal class SwapInteractorImpl @Inject constructor( providerId = swapProvider.providerId, rateType = RateType.FLOAT, toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", - ) + ).getOrNull() val txData = walletManagersFacade.createTransaction( amount = amount.value.convertToAmount(currencyToSend.currency), fee = getFeeForTransaction(txFee), memo = null, - destination = (exchangeData.dataModel?.transaction as ExpressTransactionModel.CEX).txTo, + destination = (exchangeData?.transaction as ExpressTransactionModel.CEX).txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, ) @@ -514,7 +517,7 @@ internal class SwapInteractorImpl @Inject constructor( network = currencyToSend.currency.network, ) - val externalUrl = (exchangeData.dataModel.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl + val externalUrl = (exchangeData.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl return result.fold( ifLeft = { @@ -533,7 +536,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet = currencyToGet, amount = amount, swapProvider = swapProvider, - swapDataModel = exchangeData.dataModel, + swapDataModel = exchangeData, timestamp = timestamp, ) storeLastCryptoCurrencyId(currencyToGet.currency) @@ -544,10 +547,10 @@ internal class SwapInteractorImpl @Inject constructor( ), fromAmountValue = amount.value, toAmount = amountFormatter.formatSwapAmountToUI( - exchangeData.dataModel.toTokenAmount, + exchangeData.toTokenAmount, currencyToGet.currency.symbol, ), - toAmountValue = exchangeData.dataModel.toTokenAmount.value, + toAmountValue = exchangeData.toTokenAmount.value, txAddress = userWalletManager.getLastTransactionHash( currencyToSend.currency.network.backendId, derivationPath, @@ -751,7 +754,7 @@ internal class SwapInteractorImpl @Inject constructor( private suspend fun getQuotesState( exchangeProviderType: ExchangeProviderType, - quoteDataModel: AggregatedSwapDataModel, + quoteDataModel: Either, amount: SwapAmount, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, @@ -761,58 +764,60 @@ internal class SwapInteractorImpl @Inject constructor( txFee: TxFeeState, includeFeeInAmount: IncludeFeeInAmount, ): SwapState { - val quoteModel = quoteDataModel.dataModel - if (quoteModel != null) { - val swapState = updateBalances( - networkId = networkId, - fromTokenStatus = fromToken, - toTokenStatus = toToken, - fromTokenAmount = amount, - toTokenAmount = quoteModel.toTokenAmount, - swapData = null, - txFeeState = txFee, - ) + return quoteDataModel.fold( + ifRight = { quoteModel -> + val swapState = updateBalances( + networkId = networkId, + fromTokenStatus = fromToken, + toTokenStatus = toToken, + fromTokenAmount = amount, + toTokenAmount = quoteModel.toTokenAmount, + swapData = null, + txFeeState = txFee, + ) - return when (exchangeProviderType) { - ExchangeProviderType.DEX -> { - val state = updatePermissionState( - networkId = networkId, - fromTokenStatus = fromToken, - swapAmount = amount, - quotesLoadedState = swapState, - isAllowedToSpend = isAllowedToSpend, - spenderAddress = quoteModel.allowanceContract, - ) - state.copy( - preparedSwapConfigState = state.preparedSwapConfigState.copy( + when (exchangeProviderType) { + ExchangeProviderType.DEX -> { + val state = updatePermissionState( + networkId = networkId, + fromTokenStatus = fromToken, + swapAmount = amount, + quotesLoadedState = swapState, isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceWithoutFeeEnough, - ), - ) + spenderAddress = quoteModel.allowanceContract, + ) + state.copy( + preparedSwapConfigState = state.preparedSwapConfigState.copy( + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + ), + ) + } + ExchangeProviderType.CEX -> { + swapState.copy( + permissionState = PermissionDataState.Empty, + preparedSwapConfigState = PreparedSwapConfigState( + isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, + isAllowedToSpend = isAllowedToSpend, + isBalanceEnough = isBalanceWithoutFeeEnough, + hasOutgoingTransaction = hasOutgoingTransaction(fromToken), + includeFeeInAmount = includeFeeInAmount, + ), + ) + } } - ExchangeProviderType.CEX -> { - swapState.copy( - permissionState = PermissionDataState.Empty, - preparedSwapConfigState = PreparedSwapConfigState( - isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, - isAllowedToSpend = isAllowedToSpend, - isBalanceEnough = isBalanceWithoutFeeEnough, - hasOutgoingTransaction = hasOutgoingTransaction(fromToken), - includeFeeInAmount = includeFeeInAmount, - ), - ) - } - } - } else { - val rates = getQuotes(fromToken.currency.id) - val fromTokenSwapInfo = TokenSwapInfo( - tokenAmount = amount, - amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) - ?: BigDecimal.ZERO, - cryptoCurrencyStatus = fromToken, - ) - return SwapState.SwapError(fromTokenSwapInfo, quoteDataModel.error) - } + }, + ifLeft = { error -> + val rates = getQuotes(fromToken.currency.id) + val fromTokenSwapInfo = TokenSwapInfo( + tokenAmount = amount, + amountFiat = rates[fromToken.currency.id]?.fiatRate?.multiply(amount.value) + ?: BigDecimal.ZERO, + cryptoCurrencyStatus = fromToken, + ) + return SwapState.SwapError(fromTokenSwapInfo, error) + }, + ) } private suspend fun getIncludeFeeInAmount( @@ -882,7 +887,7 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, selectedFee: FeeType, ): SwapState { - repository.getExchangeData( + return repository.getExchangeData( fromContractAddress = fromToken.currency.getContractAddress(), fromNetwork = fromToken.currency.network.backendId, toContractAddress = toToken.currency.getContractAddress(), @@ -893,9 +898,8 @@ internal class SwapInteractorImpl @Inject constructor( providerId = provider.providerId, rateType = RateType.FLOAT, toAddress = toToken.value.networkAddress?.defaultAddress?.value ?: "", - ).let { - val swapData = it.dataModel - if (swapData != null) { + ).fold( + ifRight = { swapData -> val feeData = transactionManager.getFee( networkId = networkId, amountToSend = amount.value, @@ -926,7 +930,7 @@ internal class SwapInteractorImpl @Inject constructor( swapData = swapData, txFeeState = txFeeState, ) - return swapState.copy( + swapState.copy( permissionState = PermissionDataState.Empty, preparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = true, @@ -936,7 +940,8 @@ internal class SwapInteractorImpl @Inject constructor( includeFeeInAmount = IncludeFeeInAmount.Excluded, // exclude for dex ), ) - } else { + }, + ifLeft = { error -> val rates = getQuotes(fromToken.currency.id) val fromTokenSwapInfo = TokenSwapInfo( tokenAmount = amount, @@ -944,12 +949,12 @@ internal class SwapInteractorImpl @Inject constructor( ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, ) - return SwapState.SwapError( + SwapState.SwapError( fromTokenSwapInfo, - it.error, + error, ) - } - } + }, + ) } @Suppress("LongParameterList") diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index f6bf9093c9..d299f7824e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -13,6 +13,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.feature.swap.domain.* +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.UserWalletManager import com.tangem.utils.coroutines.CoroutineDispatcherProvider diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt deleted file mode 100644 index 45a895181e..0000000000 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/models/data/AggregatedSwapDataModel.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.tangem.feature.swap.domain.models.data - -import com.tangem.feature.swap.domain.models.DataError - -/** - * Model that aggregate data model from repository return with error [DataError] if it exists - * - * @param T model type - * @property dataModel - * @property error possible from repository [DataError] - */ -data class AggregatedSwapDataModel( - val dataModel: T?, - val error: DataError = DataError.UnknownError, -) \ No newline at end of file diff --git a/features/swap/presentation/build.gradle.kts b/features/swap/presentation/build.gradle.kts index b2ea5cda48..db0e737edf 100644 --- a/features/swap/presentation/build.gradle.kts +++ b/features/swap/presentation/build.gradle.kts @@ -26,6 +26,10 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.settings) + implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** AndroidX */ implementation(deps.androidx.activity.compose) @@ -48,11 +52,6 @@ dependencies { implementation(projects.features.swap.api) implementation(projects.features.tokendetails.api) - /** Domain */ - implementation(projects.features.swap.domain) - implementation(projects.domain.tokens.models) - implementation(projects.domain.settings) - /** Other libraries */ implementation(deps.compose.shimmer) implementation(deps.kotlin.serialization) 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 2837f48f2a..6f674f14e9 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -12,11 +12,11 @@ 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.converters.TokensDataConverter +import com.tangem.feature.swap.domain.models.domain.* +import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount -import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation -import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R 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 b17c398d2e..944e9ff75d 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 @@ -22,8 +22,8 @@ import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider -import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.domain.models.ui.* +import com.tangem.feature.swap.domain.models.formatToUIRepresentation import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 8c4c106fa0..860b64eec4 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -70,9 +70,10 @@ dependencies { /** Temp dependency to swap domain */ implementation(projects.features.swap.domain) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** Feature Apis */ implementation(projects.features.tokendetails.api) implementation(projects.features.send.api) - implementation(projects.features.swap.domain) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 13445dc76d..e1f2ca8766 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -12,7 +12,7 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1a718c9846..8b10c5c379 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -35,7 +35,7 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.feature.swap.domain.SwapRepository +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState diff --git a/settings.gradle.kts b/settings.gradle.kts index 3ab3775f15..2d7d4f7efc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -79,12 +79,16 @@ include(":libs:auth") // region Feature modules include(":features:onboarding") + include(":features:referral:data") include(":features:referral:domain") include(":features:referral:presentation") + include(":features:swap:api") include(":features:swap:data") include(":features:swap:domain") +include(":features:swap:domain:models") +include(":features:swap:domain:api") include(":features:swap:presentation") include(":features:tester:api") From 64423bdf6fa4a4d7a2ea79bd0bdce78830540257 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Dec 2023 14:03:31 +0300 Subject: [PATCH 022/102] Updated on 2026-08-14 --- .../tangem/core/ui/components/transactions/TransactionList.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt index 202930549a..6b4cc1bd3d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionList.kt @@ -71,7 +71,6 @@ fun LazyListScope.txHistoryItems( } } -@OptIn(ExperimentalFoundationApi::class) private fun LazyListScope.contentItems( txHistoryItems: LazyPagingItems, isBalanceHidden: Boolean, @@ -93,7 +92,6 @@ private fun LazyListScope.contentItems( state = item, isBalanceHidden = isBalanceHidden, modifier = modifier - .animateItemPlacement() .roundedShapeItemDecoration( currentIndex = index, lastIndex = txHistoryItems.itemSnapshotList.lastIndex, From 203b66f6ed84d83ad37935806e2b34bde8449c94 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 12:06:36 +0300 Subject: [PATCH 023/102] Updated on 2026-08-14 --- .../com/tangem/feature/swap/ui/StateBuilder.kt | 14 +++++++++++--- .../feature/swap/viewmodels/SwapViewModel.kt | 1 + 2 files changed, 12 insertions(+), 3 deletions(-) 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 6f674f14e9..18933ba236 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -12,11 +12,11 @@ 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.converters.TokensDataConverter -import com.tangem.feature.swap.domain.models.domain.* -import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.SwapAmount +import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation +import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.* import com.tangem.feature.swap.presentation.R @@ -651,9 +651,17 @@ internal class StateBuilder( } fun loadingPermissionState(uiState: SwapStateHolder): SwapStateHolder { + val warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded }.toMutableList() + warnings.add( + 0, + SwapWarning.TransactionInProgressWarning( + title = resourceReference(R.string.swapping_pending_transaction_title), + description = resourceReference(R.string.swapping_pending_transaction_subtitle), + ), + ) return uiState.copy( permissionState = SwapPermissionState.InProgress, - warnings = uiState.warnings.filterNot { it is SwapWarning.PermissionNeeded }, + warnings = warnings, ) } 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 944e9ff75d..a36badc51e 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 @@ -560,6 +560,7 @@ internal class SwapViewModel @Inject constructor( is TxState.TxSent -> { uiState = stateBuilder.loadingPermissionState(uiState) uiState = stateBuilder.dismissBottomSheet(uiState) + startLoadingQuotesFromLastState(isSilent = true) } is TxState.UserCancelled -> Unit else -> { From 9aa840f892b5a3671e756e821632001b17563982 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Dec 2023 20:42:39 +0300 Subject: [PATCH 024/102] Updated on 2026-08-14 --- .../tap/di/domain/SettingsDomainModule.kt | 17 ++ .../local/preferences/PreferencesKeys.kt | 4 + core/res/src/main/res/values-ru/strings.xml | 5 + core/res/src/main/res/values/strings.xml | 5 + .../notifications/NotificationConfig.kt | 20 ++- .../NotificationWithBackground.kt | 170 ++++++++++++++++++ .../main/res/drawable/ic_swap_promo_34.xml | 20 +++ .../img_swap_promo_banner_background.webp | Bin 0 -> 12392 bytes .../settings/DefaultSwapPromoRepository.kt | 54 ++++++ .../data/settings/di/SettingsDataModule.kt | 8 + .../ShouldShowSwapPromoTokenUseCase.kt | 11 ++ .../ShouldShowSwapPromoWalletUseCase.kt | 11 ++ .../repositories/SwapPromoRepository.kt | 13 ++ 13 files changed, 331 insertions(+), 7 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt create mode 100644 core/ui/src/main/res/drawable/ic_swap_promo_34.xml create mode 100644 core/ui/src/main/res/drawable/img_swap_promo_banner_background.webp create mode 100644 data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt create mode 100644 domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt index 1c596a4b4d..7334227d99 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/SettingsDomainModule.kt @@ -8,6 +8,7 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository import com.tangem.domain.settings.* import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.settings.repositories.SwapPromoRepository import com.tangem.tap.domain.TangemSdkManager import com.tangem.tap.domain.settings.DefaultLegacySettingsRepository import dagger.Module @@ -103,4 +104,20 @@ internal object SettingsDomainModule { fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled { return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository) } + + @Provides + @ViewModelScoped + fun provideShouldShowSwapPromoWalletUseCase( + swapPromoRepository: SwapPromoRepository, + ): ShouldShowSwapPromoWalletUseCase { + return ShouldShowSwapPromoWalletUseCase(swapPromoRepository) + } + + @Provides + @ViewModelScoped + fun provideShouldShowSwapPromoTokenUseCase( + swapPromoRepository: SwapPromoRepository, + ): ShouldShowSwapPromoTokenUseCase { + return ShouldShowSwapPromoTokenUseCase(swapPromoRepository) + } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index d10a9dab4f..26d5b3cf08 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -42,6 +42,10 @@ object PreferencesKeys { val WALLETS_BALANCES_STATES_KEY by lazy { stringPreferencesKey(name = "walletsBalancesStates") } val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") } + + val IS_WALLET_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoShown") } + + val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isTokenSwapPromoShown") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 391fe57950..21bec9195d 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -244,6 +244,8 @@ 1INCH токены будут зачислены на адрес вашего кошелька в сети %s в течение 48 часов Чтобы получить доступ ко всем сетям, вам необходимо отсканировать карту Отсканируйте карту + Обменивайте свои цифровые активы между различными сетями + Кроссчейн-своп теперь доступен Токены Добавить Изменить @@ -539,6 +541,9 @@ Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. Невозможно скрыть %s Нет цены + Обменяйте этот токен на другие активы в вашем портфеле + Представляем кроссчейн-своп + Обменять контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 08c1787e9b..3088fc97ea 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -263,6 +263,8 @@ 1INCH tokens will be credited to your %s wallet address within 48 hours To access all the networks you need to scan the card Scan your card + Swap multiple currencies across several blockchains + Cross-chain swaps are now available Tokens Add Edit @@ -569,6 +571,9 @@ The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list. Unable to hide %s No rate + Exchange this token for other assets in your portfolio + Introducing cross-chain swaps + Exchange now contract: %s You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt index 1992043a9e..c6180da951 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationConfig.kt @@ -6,12 +6,13 @@ import com.tangem.core.ui.extensions.TextReference /** * Notification component state * - * @property title title - * @property subtitle subtitle - * @property iconResId icon resource id - * @property buttonsState buttons state - * @property onClick lambda be invoked when notification is clicked - * @property onCloseClick lambda be invoked when close button is clicked + * @property title title + * @property subtitle subtitle + * @property iconResId icon resource id + * @property backgroundResId background resource id + * @property buttonsState buttons state + * @property onClick lambda be invoked when notification is clicked + * @property onCloseClick lambda be invoked when close button is clicked * [REDACTED_AUTHOR] */ @@ -19,6 +20,7 @@ data class NotificationConfig( val title: TextReference, val subtitle: TextReference, @DrawableRes val iconResId: Int, + @DrawableRes val backgroundResId: Int? = null, val buttonsState: ButtonsState? = null, val onClick: (() -> Unit)? = null, val onCloseClick: (() -> Unit)? = null, @@ -33,7 +35,11 @@ data class NotificationConfig( val onClick: () -> Unit, ) : ButtonsState() - data class SecondaryButtonConfig(val text: TextReference, val onClick: () -> Unit) : ButtonsState() + data class SecondaryButtonConfig( + val text: TextReference, + @DrawableRes val iconResId: Int? = null, + val onClick: () -> Unit, + ) : ButtonsState() data class PairButtonsConfig( val primaryText: TextReference, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt new file mode 100644 index 0000000000..072a7389f6 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -0,0 +1,170 @@ +package com.tangem.core.ui.components.notifications + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material.Icon +import androidx.compose.material.Text +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import androidx.constraintlayout.compose.Visibility +import com.tangem.core.ui.R +import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Custom notification with image background + * @see [Swap Promo](https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?type=design&node-id=8713-6307&mode=dev) + */ +@Suppress("LongMethod", "DestructuringDeclarationWithTooManyEntries") +@Composable +fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = Modifier) { + val button = config.buttonsState as? NotificationConfig.ButtonsState.SecondaryButtonConfig + + ConstraintLayout( + modifier = modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size62) + .fillMaxWidth() + .clip(TangemTheme.shapes.roundedCornersXMedium), + ) { + val (iconRef, titleRef, subtitleRef, closeIconRef, buttonRef, backgroundRef) = createRefs() + + val spacing2 = TangemTheme.dimens.spacing2 + val spacing12 = TangemTheme.dimens.spacing12 + val spacing14 = TangemTheme.dimens.spacing14 + + Image( + painter = painterResource(config.backgroundResId ?: R.drawable.img_swap_promo_banner_background), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.constrainAs(backgroundRef) { + top.linkTo(parent.top) + start.linkTo(parent.start) + end.linkTo(parent.end) + bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints + height = Dimension.fillToConstraints + visibility = if (config.backgroundResId == null) Visibility.Gone else Visibility.Visible + }, + ) + Image( + painter = painterResource(config.iconResId), + contentDescription = null, + modifier = Modifier.constrainAs(iconRef) { + start.linkTo(parent.start, spacing12) + linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f) + }, + ) + Text( + text = config.title.resolveReference(), + style = TangemTheme.typography.button, + color = TangemTheme.colors.text.constantWhite, + modifier = Modifier.constrainAs(titleRef) { + top.linkTo(parent.top, spacing12) + start.linkTo(iconRef.end, spacing12) + end.linkTo(closeIconRef.start, spacing2) + width = Dimension.fillToConstraints + }, + ) + Text( + text = config.subtitle.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.constantWhite, + modifier = Modifier.constrainAs(subtitleRef) { + top.linkTo(titleRef.bottom, spacing2) + start.linkTo(iconRef.end, spacing12) + end.linkTo(parent.end, spacing12) + bottom.linkTo(buttonRef.top, spacing12, spacing14) + width = Dimension.fillToConstraints + }, + ) + Icon( + painter = painterResource(id = R.drawable.ic_close_24), + contentDescription = null, + tint = TangemTheme.colors.text.constantWhite, + modifier = Modifier + .size(TangemTheme.dimens.size16) + .constrainAs(closeIconRef) { + top.linkTo(parent.top, spacing12) + end.linkTo(parent.end, spacing12) + } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = rememberRipple(bounded = false), + ) { + config.onCloseClick?.invoke() + }, + ) + SecondaryButtonIconStart( + text = button?.text?.resolveReference().orEmpty(), + iconResId = button?.iconResId ?: R.drawable.ic_exchange_vertical_24, + onClick = button?.onClick ?: {}, + modifier = Modifier.constrainAs(buttonRef) { + start.linkTo(parent.start, spacing12) + end.linkTo(parent.end, spacing12) + bottom.linkTo(parent.bottom, spacing12) + width = Dimension.fillToConstraints + visibility = if (button == null) { + Visibility.Gone + } else { + Visibility.Visible + } + }, + ) + } +} + +//region preview +@Preview +@Composable +private fun NotificationWithBackgroundPreview( + @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, +) { + TangemTheme { + NotificationWithBackground(config = config) + } +} + +private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + NotificationConfig( + title = resourceReference(id = R.string.main_swap_promotion_title), + subtitle = resourceReference(id = R.string.main_swap_promotion_message), + iconResId = R.drawable.ic_swap_promo_34, + backgroundResId = R.drawable.img_swap_promo_banner_background, + ), + NotificationConfig( + title = resourceReference(id = R.string.token_swap_promotion_title), + subtitle = stringReference( + "Swap multiple currencies between any chains you wish. Swap multiple " + + "currencies between any chains you wish. Swap multiple " + + "currencies between any chains you wish. Commission free period " + + "till Dec 31.", + ), + iconResId = R.drawable.ic_swap_promo_34, + backgroundResId = R.drawable.img_swap_promo_banner_background, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = R.string.token_swap_promotion_button), + onClick = {}, + ), + ), + ) +} +//endregion \ No newline at end of file diff --git a/core/ui/src/main/res/drawable/ic_swap_promo_34.xml b/core/ui/src/main/res/drawable/ic_swap_promo_34.xml new file mode 100644 index 0000000000..3c279910c8 --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_swap_promo_34.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/core/ui/src/main/res/drawable/img_swap_promo_banner_background.webp b/core/ui/src/main/res/drawable/img_swap_promo_banner_background.webp new file mode 100644 index 0000000000000000000000000000000000000000..1e952cd6f75c5d7d25ad418bbf5dc427ebbd5f86 GIT binary patch literal 12392 zcmZvAV{j(ivhEw(wmq@!WHPaB+qO9|Cbn(c*2K1LzV|y-_uhTZKKs`~RqN@eyH_nG zDG3P!CICQPTtq=lfs0t>pZzT}NHz#91b7&T05}K~005nudX43>3q{Op`=gioTlAi) z`+D_Gi4ZsXG4wyOUr^8Zd4cRJ-FIxL-K12}jPyz`j5SEHXwjAie}{xxqk>Auh? z@%st%8JdpWHUG=%f2qi#U;z|kpSAOXlH0OE?&jq5^HJ`lT|av{mh_NGDH&lZ(X;&r zNsK#WytGPM=(C*e`SV15Tpe{;Z}ZK31eK>5E4d<^Wpy6%^FuX8S$7puh`Ia&lOu@z)5 z8>PBtXDkV-{x_QcC0H9dZY2Y;)PlA-z*QGw`6K?p$8z>({yt3MlCzeZL)C*P4Q7V= ze~?AX+YkFBWfPnG?iYCJu)fMuX#hJ2PSqse_^mq??SwwC0IRYf0v{#*A2T_$d&8Lc z?#X}Q=A*bdrt@mJF_c~wcJXBr79%L>0_aj>*Zs-ScY0jVBvYMh{vSa4$Div0C{wG$dIP*!XedTzq-W6aFNkJmP^8D19d-$TK8fJ8-} z;(+GS%zf|sb+GyzSN_{c9zCTKDSmH#-~yRu>tO(6;LvW3#il2RljF$@rPsbCm>emFjVwX-YKYkGO(D; zzo7VjSGq`h-mDotTe1benW7?S`)EkvzS@hHbRWEAU4cw6;$C!n`u=Y| za(;^i8a4KHD1NU{+3D)0V&~uRTMmva^^i{`3z^FI7gm*EKJmdio79srj{jz(2eoOU zPw1ak&=MoL-=XSHc-RYLn2VIo+KK}zOO+o?xxn|aKTgW<&4J!h+ohs7QXRWJcKjI0 zK4ps?Q7wzV$i7;}!giVwoNQ=qEw-?8q6@j?@B!YU+hlD`3!0#X%+-x+Nr|<)dEKb+4xBizUqv6O=W;KCh(G9B&`$jAH zrsJ$aP0Zf)ixo5m7ap@p>Ibl@s)B3KZplBk&qY$8LzXisIjb~Xs=OWgVF*t%F?%2+zS3J4?t!~wajv=d!Fhdt7 zCM|DBgzsjqIeIB{rM^F|Qj{Teqc~Swk^yq+Xfevo+GEy+X&!beMKXI$HuveB^2y5X zk+y(xmKb77kPJ*yY%jk-#Q4@k1UuFMW75V4{$=8tdR zp_ue7e3-&UAY!^!8NImq>UuOqbfQP!SJwi4OIVA#kzr|!*7JJ+ftZF3-groZ$#`$F zcvhKcjDAY*p2s?T+r}ls8!TPqNt~b`p}%N#r`IiY{~KHA_8kG10w`!lujVC8<7X_B zEH!PI1vJJ@HBKGBLY(>E;FNciyQglK9pj!6mFXE|UCjw!%cb+eh0e^Y-lr-hY*-J2 zgR8}cY$z6=7e?9gzuWV4$B^`P$5#Z5jNGIKUz7%I;Vb`p9-}d-@OFyb=sI1HWQZ*B z80$Z()yD|*=^>4~tev+cSYk9Y?efdnDTX^nwf4ogEwDCOz-6o^TRm0&eCFC#ckol= zCdZ)jgInxCxa`6@7O_iK+?4Q}KgW^N^6|M*u|8`|%OYbo*gHczG@ z#PHbM%reAQ*Ukv$q4G{cV)|H$lvz*V$+`i9e<}c&{yCONJ+$AH;3h|^xM#a88%@R# zLAqD|agrplptdowYnVQIbo5mjyN^jk_8u8?EZ649Q|2XvZ^i7uEbP>_z49-!!%?sP zs?#VR@AFi~GO7#&YvmP}P6;%#{dF3Ac>47yhT!+YYoF`Hgs+2@QfbgI_^J}EqX8Uh_}h0U2?{T;#PEMqq^eIirA>UC zk?vt#jwrxp@*tVE_Hg2o0jIbi3j?CIzoz_tie7u*%E(mWMq;siVA?QjBu_Y`zxtJ| zO*+cqjy(U5=~x>h^hAWb&)ky0RLHAJ-iVK(w`$O-zJ|4vcp9+3lrkQ-$!LyLr?HQ* z*Nt0cG_ctPq?dpa)Kx^$(SC_n4>TdG(c*L!K#*JJFaUf5Wk!IQ(;HXs0|Z{ym-B6- zvLa12c>n--oG9P7@u+S0$0=5)!;T~mGwq(W){=ILJEPQu@2MMh@$480cgd_{D<{*n z%nfVdIOnkP9`2`^dWWNpua1-wLYpAJ7%y*u*LX8LJZ1?noj{gK0hJx8D854 zYr37oqLV(U=Z^A^f}n-HOcKXU7BL$WPj&bxg%XUxOPf8HUim zuMNtil4ssODTJoD%Cl9+dwlJnO?hTw>NfFP?kxX#n*#7wTSZoXYGl)EhoD-lUbS9v zp?6!OX7x&1E2tkP6F?zhz*A0D(Rse4{~o00BuTz8`##Pn{en?oW$82QRZnxAi@sI` z)2HXH^0JY2v=<8B;t}l@nofE!f6~frWIPq726E4h1{k|23>e!TAg{nzlX;iK(!{Y3pb;5-|B*OG86HRJ2fp<#Uwc)O` z82|t(yXEFOc%_R6i8ZHfB9oTZUgJ-xmJgj1Q4xaRVD<)7ey{LXrh-jV%!fNvHxXs? zR^XdAI(!oDAN~D!lyYyZje4DT>)kj1o$$Q^c2Ddo)2DNx!A(zza0B~LdzTa^{skuQ zu9mjW$%!B-IjjI1gewX5fETZ0*bg)55DO^Rqzoit?!eCQbKfK~O&={qkQSgm$SEH-#*A$59Xq&=G$?mvCa zbFWAsm3h$KGOe_v`rJ&z2OGYg4T6$nmdAE5yn&X$?i~9xqRJ7%ig8qU;cAcnK>Os! zVZ(1Px1>OOxyNmN7Nh$RahLuC;dkyyW4pvzo}9x@YGBMRIfJ!~Q<}8DG9f9Biw#8S z09X8{My?OVfL+GqRA6}N-p*^|kH}I`+8`E+zpKgZY$x_EPuivA@C$BTuTy68uK9m4 zet+I;nkb}jxmR0-7l@I%wY7bZ-`9m6Mov}D9dDU$86OVR>(W%dTBD|VU$Lh4)XR~P zIbRuU<$Yx9fpJchdWf4iiQ5*{zH?p8b@tWGW1vojDEfc=;?^DS3!M5+bLE5jvyzJ` z>D5rO1fvl^OZNx>z)VW4GcP6f?CVNy)W(xglA3LDcr-GRGB=z&Q01@|QW0))^5##b zs~km)+!dF_(#qcS3$-<$hbM`dEy=|Y4)eVV@H0i+_9FX4wp{X>rrI?KCb~e%7bq0~ zf<1A@C_K#POFWvfm_?P9FC2tFB~X?ol5_|GEIa^i_bC+YF3+TkFW{;3**K2G8)Yd1 z2kLo*T3v8p6waM(3_?8=e>#>8&&=9tm8LW0!+K;V_@+Z@FUT6Ein$V{p^S>St-o!4 z6e_6N_CIjL0uN8V{rFRrr~D~(yG`d#pMu{&aQr*h))Xmg1^HapMh*()Bp7H!=Uu%> zgzDNHW0uFp&>|gyqkj`!p$XdJc&;}Z#sX4na3iwJ+$)6uNz6k#_{c!u3Nxy9$CxfN zw=sl6pKXx{+yS@e_B2B1wa{#lcuX5sfGalt?Q<$i1s)?EE;Tf$5G=C zfkXG?s2_SH7WH<8C5w|wVEfPu5nk~T2-0VKTG!V*9Sq6Ts`u*&FsBn&s-k`* z6kpPA)-R7ob)i+z&}3|d9Ivl;yB0BU0~UOYqukW1LS>}dK)leG~-$5K_V?z&Lgm2PNSlaxvUAf#C)`}4a^Snx#`2|so&&h!-oU=J5Nu&_rIxvFlN{}xxKcAMC^T@ zgR87U!!L#PCce&RDW0Rwe6`?tJgfg;j);=;&7$|FnIfDD`TsZ{S<#B9du-llp`=F? zdW+Pl7(o_txVYIN3Ijjvel$h7V!R`sF6-?JL&O1{kOvvt(04!K@f{c&hm7@!b&clc z5o$4TjrX=b1Baox#P-?X-p6l7qg6+<^(FoMyHe6qCs$(?-SPJ)+s_eYuiL;P?e%pJ z9>gGvY5WzdjqJz5tRzPHWfKszo@w)1pCaaGAC2{{_>th9>Y(s+YrZgS_Ltr_sZIkyk zLd&HVT}-`lr_%|w4yeJv>hjuU)6FRr6;RqQVRpM(zw#^BU1XL6b_czP3KRcp20T}Y zz}0Z}HrK2z=IxvwC?JMZX)CfvYOV6qUoiWv0LQ32n5O+riFY%lGB9CjOoE|LL!bNY z7=EM3YS!*dG!^ri5iASvEGr^UyZ5J+e~C-mWUgOF<+|nr=jw16C&H&Qo>|U7kd^)1 zGhC-(BJ-uYQd>n=w)KHMq#OXMt&+VAfn8-cdsP$^d9_fNafWY`%TSGku>hMWf2fmR z-!1aC_49*1t0OUTAN|qX7>>CG`x-x)a`b3svO$;_1A4gp6~{_giFynDvF~d_uoAR0 zs%IM8PWqiIMTTl7NeYjl2sm~cV3W&;>9z) zZgitRDP}&OHVfK8Yq!^g3y0bAJY|-8hZ31yKkc*NFW$u7=7=vZaQkJ475VJEAB{^< z^7i;+hQECu!gTWJvjWD1ewF)ECju2tXDis^%}p~Quj|8`+BocM*e%7v(NuaU8rq+` z-xILl{79Dm{6Mzh--SQe(1>>`Wd{*OQ*`-zJ=WtMVCdVh9)HKKSZXtrrLLGy9RY=a zvK?Nk&vz00vBgQ+`CubcwQ^JTCiMOx=nKm0Ska*X`?QiMSuO3Q;R?l92QXJ98=(B9wPIk?` z$DNbB4PPsat6sY~CRQfF{)ND`xe>#3Ffvo&ceu79ZeJfu2?HH3C#ZqQm364_U<+s{ zIdAOOxRDPbiys2)!|M37F#gzALfpw!P{?{^8cGs0c;EGYiQ}??@cH(S zwK889t{N3Z+XUE#WL?gC?9&j567~N^I zx-v^a!7qm88?}MF#^g4?wVE^B)z0Hxzw?3?yabc0M+q%D!Y0u?2@Pz!+PrY53*h$@<-5?J*MZFPDj_RXLf5aZS~AeBwAV zt&$?PHuSM#GWq7Q1ez=iA6zH%B2#Z)SQM^W3(TLHv|PC{e-8?SgQ=(E0KM71t{QN3 zBpdIgZr*k&QWP&^b;v3EfrzWks6yAh=S0h!mtI}fwFnC}#_#*lpUN;OD|hu6Ux>`= z#EkjaRNdHf?#p=1c3^p3MZDD?{OaHR0JXa4i;n=MFn2f9GQ;TnEOR(i>?9tMj>X4- z((GX73oS~IX%};7Z2Dr`nXI8s=Ow*bYx2NnoJ20Z zn;2YXlZ147>CL+OJ&IY^x7Waoyd-O5({KKe5176!glGeiK`wub2mqimxmsg013NqV zLI$;eRGYm)M7O&1>N=nX#rbk%Dke|-)G0MhoOYFJt`bOO1^QaX*^UZ163VNo{57(l zi?z&?Vt|?4?n{BBD}clO#IIwx>S3Yp?oe-3$~)12ZhZJ;RwM@I!Zkj%m6}^!NA6!n zvJ>p9TDA4J@HQBuj+`>0r03TQzz4CExG?;fLV=V8lReM;FQQhR^4G`s`$%`hMqm;a z+-fCm_TnZl#OSfV8)sshxyc1g!)Exf2?ITES*g zVUg`;43><`s;WLT`SWzFDRDV6)461Ad{lAK_?E*H#?;WbyV9kd zos2ZgoK^+qhAGoex=Cqp&!3I7w+OYEV-(70=DEFcc0ynaZdLN}i3#1?dGVzpR-X{B z=4V+LA`6{qoQ`O?a$FQ+k1eVr4QqFY$`M!{2=$8PiruHZx%fSepDl(4_B` z%34Z8(E|NwQXooF-tkupXnz*!QWI&+#Xc4<2Qz_gu9pl1{C(*%TB4Umflm+8U7$vA zUz!wxk`&Glxncm5s5A9Rdwg{sKWr}|2pXB(h?FLq8K~_7$Cdfvq`qN(wJVZDGz>#D zf>r1|cX1r9ulfR*G1Qn8(?-&=a6vo|kY?OeFk7TiK6TlMAbDc?C{UPNGaxNWQp1rf z>of;lwSxsq;OmHqNkmU-b>4{E z8q7}9w4dU;|Gc8ObVo&16*J#Q;*r3f-2k^3au{>hA z3C%OlUO(MUzCUPZ$&kUIq@%SB-sUv>?I(WL??T9`pHF*%(a2`hxBRj^)&k)mpkcve?x@^sS zMDtlK8&CRTC$X&HGc3VTl1xUje;Qs$HS!hNcV6WB=~;$c5uZPGvx>xn8#Z=Ld* zF?tn4l@re+vGX>6vS;sOl-Tg9TjlgG?3ugkP;CgJm<~uKxAD|KkUlh@x`9-RlGCz? z4@o{dx`ssDEq7Q;b@jPg>RWWdJ)Aj(3S(w|20+n|MRMx?y=<_m3OrzxF&7D3MkhC) zSo3u_ex2*#%u7qTGqS+*eK=aPg|W%8R!9&5%dc*>*no&Li~W6KZaPOR&x9aZkq`CQ z%VJ(j@Po%{6H?b6*KuH}Vm*mAcJvwxk^gYFq=&~}9Pc6WnMH4T*Mt*o40Xs*2%py| z2`eTAPm8Uuh>Ie9G9TeGMR7*D5QdTf+&l6%cq8^GFiNGFReNpuc?So_QrX_!xjxf4Qg%uTYYbDnv!5o z884tJ`%Dz;0oLVA$`FDv$@=2R@~FJwEJXdpdO)wBgNm5k1=B+(4UvnPsGJ3D<`t7O zXa+{>)k`RN-PbWOuBZru{6MiaubR!400P@zT>bk+o3zEeOf(fKIk^7iL2_UB1K3!O zH@6WowQNIhKFuP-N~}|d>u9C*kV+wc0IW{t=!YwXJmn~DZE`Ph#P0$5-AR?y?8>>t zcHcb-|Gwb(P5Tn^x-6f!-E=A@LT_<3lCnQCy;K*oTJ$nd9e0I?~%F^z!+p;#xj z7^5K=_0KPbkXLw3Hl4Sx^6({WO^a7PmX!7mm(k3!Ixg*H<85b26Ugb3O^MvGVrl9v zm(F5383hn#r>kUYKq)Oxf|dSl+2`%YFR2b)$1km~X>JO<_``fjA@|y}9S2FjD=)k! zFiD(Wkajo(gl<9>pn%1Oj4wze90>3OaIjItw8#VtSr~ApJ}a4UPzd>~J9EPdi_+J( zB6%@5OD$9UbpyLtfK&6}Dv{@GZX5nY%5qfV^HozZDc zlOi2@U2rnc)!ljlRi#-y$G(Yy{BsM)3i=<;&2wTc)_5LKIL#9Gtk zrYUpG>b1Xf4&yl64~L(p9;@irxruU3&MNJ`^n)@N?WuQdeDk>Kt01c#mWBZRYk$}= zQB0V#-FWNzUo{GAH!BwMq+lRvMy7u(g#2Cbu>(gD99Rei!~ z-XX2r_Gp%V1x-=}`Z|o$7K($b>EekS;M3nrDhGe<19f0fN6LGtxjI>a7}IV!(90kX z4q%EUgj1q$^-!*_>SZYK`xt}(ARO4aRH{0NfyzS}sJAi+2kI&It`^hF%>lG1 zd+vD=bM?S_e%Q3%Z2m#gd1?bbG8u7qC>tCPiKq<7b`mgb_)u}={d%yEsz9Bo^2lo6yxwo_DJJ=P&-O`^CcKmXlJudD z6E54WJAC*L8f}|s3zU+jh`1tzbZrY{T958{UXJKJ=m?o8#*hkeE>` zc9`rPLjD@-n7|i7sK8RpgV}a*3_eH4{7pBq7(;U}Y~7@Tq^;D`{PQpiw#7zSH*VbW zrp<3pWAj~dZzI@@Y{j97NxL<1n6Sg1X=POvJ#xktq%&vRJJUtID&7T#;WX29zSC4z zFy3_Oc=EX;`Y9ZTt>5;vWi}ID~Ce2Ael`;o`Hr(Nq@G3BeuahQs6c#`~AA*lI zneY&a3vF`BO__}xu&q_X;AXEgspe}I09mrotrHFLxhH`EI~2)5$nHDC;K&^L=Oc(j zZsSW?zxx4X_t%ZZ6f{59EuiN2P5A5-;c8LbkvK`QcDe{cLy(^%v@4cicr}qB`kPp_ z#e}tq+td+`gI}U$sL$e#sX5&*1Ooto!~Dkv58I=j8=n|M336%E?}3uv^H=tAgmb0+ zk*Uj~Y~A!8^st_)zvmN4gMI#fc?dgs=`gwo;h?7??H|nCIiWB>VJi1+1l7lovK39i zG8~-aGA2v~^%?n?!u)1CR;j{)$=`~QyZh6b$oTM$m1pWQ7y=JAcU|9EQc&c8xw?(s zeCa8q%9D+8Esj77cs_yhkcdm6jW}9Av5q4s61LqlHd(Lw(DM;4z{SUtxknpm$UFOD zWXf^Yu{$g4U2R9KD%2A230Y89bBrhO+(PB=W z9K382c{WFe;cw5`S6Ml!Vg2yaeg>Hcv|d`3qatX2orvPBPk-;xX_me%jFoQ(2m5*x8y~Gxc%}4 zbXv8+2(h9~=K-bUacpkn&SK(^0H-GP7b70@oeRO{I#K_k)nN%~=*eQCo;5a;dAVeO zj;Fw1Px%xLq5ARCIySDT7DM>9yV>VIJPgJH$xeU33n-i?Y;8J@Hp0tqh6;3Ux{RO? z{*H7z$kbW-+RsS)g^-xzU+d|R8@zO8UQSclCip1C7HsMiPoY)TJuWh`9kasA#UnC9 znxAIzorRcHME;VvNBtAm`CjIeKhGk+1yWdP#4RHO=ZwEkhxXPMo#&g&N;8J8Re|pkohh_KZyHXn4j7e znbzQN@Y(xKVt+#x{>Xf!9nUxdSt_=ladjwF9X??|ZEsqL-U?9$qNzq-8BH~SrDe8? z7_)S@fg}&wS7!3j{3%dD*wv}93jh{spdW|MWzV$B@$B6%pPrtVjBT3gQh$ljX4ZIFU4T-aO7y-&XS^UQ-ThMX9fHNY>N*|hOlW=P1;veY+Vux z>b8pIWHBfqTfzaJa@t#6|2c2L3XF%M7aM73HG-NBvK@u41_%*OPZjHT;C z5%D#Bjc?(%76O};;gGFsjwS1wGNz#-2f@J1sPXH95h%jaWI^6Jyn){!Bct2ierEEVm*W3%kAi3Y^tUhc?# z^ZN|AW$F~xV}2MsezBFGZBZ@Tymk*5c?;mAJh7`n#MPw5TxCUgB0I=Mn=bu7Md6P> zU98B@s7Q1*(Qpc!GgD;R)mlvzB4 z58#hd8L-@iZq|BXlP^29@sg4uBvn_o-=h#ZH#$kyT1^~p1vm|BCi0j}O}=1e_J6gH z!@|Y|KTG}QS+>y8dW^|S2H<7;V=x)ZG!(4@(3E2xZ(7KxqP#gVOR}PVvvs(!Bvdy?W%#IHF%D4~6UXK4WJ#)HtRRNU; z4xuLjP65Nk2*)T_>>V`bhIh!v_B&k|n?HaSjqC0XXA8byUJepXfyQ^4m?v$c4o`qn)b7Zh(Nwsq6rH5R_!EsR-mx1NEY19An-?8GW?DPR{_0SRH9-1-%4j(X2!MY@DOG#?hzArNq5yy=;8b`XTpqk}wU#6g5}lnQ zr~nACYP!qLhe1EFC}=LzqN49p4pBqJkCzoHi&3@g%C-D&^L?@j7sEy%63`KmX^3UY zSCh}l5MEpXm)w;BL}DWmW2R#f((UH!Wg=(1{63^<7s@^-mtksPE>6M(bL^97ix{QZ z$G3Kxdd=tNfCdw+qWmw$M<6b_0if%%Az+(GTX27Dq^9un_4KBQF66hN^LPx9@_4MV zbWlMO6=511ajbZ$=_gQB9eggpUk_WzdpVJ&2bhKw;OZsQB(20WMQ=8nqNFE!MGh{% z>t+*?D>+j(Fl`B&pJ>87c9rwSmz%7g%1!sISXz*}5#+FdMzvW;wCL#9K6@X1KdBub z*6*t5l9<8<`O{@VuqG_fHn8v-fb(k)WGoH~5UH3^PkTSZom&JGlG>@!P-QcsOrEtu zXmq>1_5;`yyiuH&U2Sk76Vy2+jU3%fsrQ$~s@9X8 zV{`ogi~C+B$t53_5J<-8rfmSyn#*h@S$4rug_>ObjP5K*dy9m8-f?5hruxi%i7Ob* zyWA9tO5;gX&0X@>rj>3iv=c&P7mBH*09Wx9xJv%R@dsOxFLPCmgxFXmg`MADL)h-e>E z+79OLOUUmURoW?1sM)@Tb#4SSa%jUXrt=UBS+7F-=il#qvXDH?qt;N zD;rvC14WO~V8y4)U$@wzyaBs*o2CHPc|NVB0`v4}pwnU(VG-g1e$=T-ox+a*fNo|k z-QM(EA_<3LfUQy&#$uaOd){3|%)F}n!CDkKJd2A?N`?Wa7HvrVAG=>iTLhVuaNLuR zJQ2lYlQJB`?kWCI%tA+F?(oj|s+p>eg?TY{*Ejxjjh}e<3aMl`!^T!3x)iq8wqudw z15v!8qk=h}sy|*gLVTZ4wzcG{NXL(SE2beP8F6DiyO)3RxdjcP;`|}u$LOPIIE+G# zJIyPk5&HH1wvCG~bkIi-uUJHtu!%nztTzYMWj3YEMbc1%%RsvREjtvjMt^y*o-q2p z`zTyFhXJF+G(?(waiSAYQ$U>OO?mGX@bp-(C7;R1qJ3nuNus9Rl7Z!?re?{!-9xMR zn?NtG>@=Sv-Ql1wrTkH8Wn8$qI))G@v7sIgbk%o;P@ekbMiGHW|B$i+Ycxr(yKqq9 z{^h{l2@B12)~}dxpT7j3Z!*loHi=Gz*hrXFNwVt}iL(A~FExUR<1JrbBE6zCCXXt> z@gyDe3(KIm)ikIi)y*YfN8raGQD0mtCmOUo*;FnuzYx1XpW}fVc2wOL( zIl$xn1I5H$AVK&}1z{H-YF#WFNf#t!9r9=cbM0&j{qabKyJE=Lm(a`i Z*e;_)pz6qSe5LFduL2cHmK6Z-e*iH`Ae{gJ literal 0 HcmV?d00001 diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt new file mode 100644 index 0000000000..5b4611572e --- /dev/null +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt @@ -0,0 +1,54 @@ +package com.tangem.data.settings + +import com.tangem.datasource.local.preferences.AppPreferencesStore +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_TOKEN_SWAP_PROMO_SHOW_KEY +import com.tangem.datasource.local.preferences.PreferencesKeys.IS_WALLET_SWAP_PROMO_SHOW_KEY +import com.tangem.datasource.local.preferences.utils.get +import com.tangem.datasource.local.preferences.utils.store +import com.tangem.domain.settings.repositories.SwapPromoRepository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import java.util.Calendar + +/** + * Repository for showing swap promo notification. + */ +class DefaultSwapPromoRepository( + private val appPreferencesStore: AppPreferencesStore, +) : SwapPromoRepository { + override fun isReadyToShowWallet(): Flow { + return appPreferencesStore.get(IS_WALLET_SWAP_PROMO_SHOW_KEY, true) + .map { it && checkPromoPeriod() } + } + + override fun isReadyToShowToken(): Flow { + return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_SHOW_KEY, true) + .map { it && checkPromoPeriod() } + } + + override suspend fun setNeverToShowWallet() { + appPreferencesStore.store( + key = IS_WALLET_SWAP_PROMO_SHOW_KEY, + value = false, + ) + } + + override suspend fun setNeverToShowToken() { + appPreferencesStore.store( + key = IS_TOKEN_SWAP_PROMO_SHOW_KEY, + value = false, + ) + } + + 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 + } + + companion object { + private const val START_DATE_KEY = 15 + private const val END_DATE_KEY = 31 + private const val MONTH_KEY = 11 // December + } +} \ No newline at end of file diff --git a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt index ba83b09d4d..43d5796d15 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/di/SettingsDataModule.kt @@ -2,10 +2,12 @@ package com.tangem.data.settings.di import com.tangem.data.settings.DefaultAppRatingRepository import com.tangem.data.settings.DefaultSettingsRepository +import com.tangem.data.settings.DefaultSwapPromoRepository import com.tangem.data.source.preferences.PreferencesDataSource import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.settings.repositories.AppRatingRepository import com.tangem.domain.settings.repositories.SettingsRepository +import com.tangem.domain.settings.repositories.SwapPromoRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -36,4 +38,10 @@ internal object SettingsDataModule { fun provideAppRatingRepository(appPreferencesStore: AppPreferencesStore): AppRatingRepository { return DefaultAppRatingRepository(appPreferencesStore = appPreferencesStore) } + + @Provides + @Singleton + fun provideSwapPromoRepository(appPreferencesStore: AppPreferencesStore): SwapPromoRepository { + return DefaultSwapPromoRepository(appPreferencesStore = appPreferencesStore) + } } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt new file mode 100644 index 0000000000..0c75a4f52a --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SwapPromoRepository +import kotlinx.coroutines.flow.Flow + +class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) { + + operator fun invoke(): Flow = swapPromoRepository.isReadyToShowToken() + + suspend fun neverToShow() = swapPromoRepository.setNeverToShowToken() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt new file mode 100644 index 0000000000..573a5ea94b --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoWalletUseCase.kt @@ -0,0 +1,11 @@ +package com.tangem.domain.settings + +import com.tangem.domain.settings.repositories.SwapPromoRepository +import kotlinx.coroutines.flow.Flow + +class ShouldShowSwapPromoWalletUseCase(private val swapPromoRepository: SwapPromoRepository) { + + operator fun invoke(): Flow = swapPromoRepository.isReadyToShowWallet() + + suspend fun neverToShow() = swapPromoRepository.setNeverToShowWallet() +} \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt new file mode 100644 index 0000000000..95d31f65d8 --- /dev/null +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt @@ -0,0 +1,13 @@ +package com.tangem.domain.settings.repositories + +import kotlinx.coroutines.flow.Flow + +interface SwapPromoRepository { + fun isReadyToShowWallet(): Flow + + fun isReadyToShowToken(): Flow + + suspend fun setNeverToShowWallet() + + suspend fun setNeverToShowToken() +} \ No newline at end of file From b654ab08a7c5a98ea0be6fc34f3645b6c05604c2 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 12 Dec 2023 21:01:59 +0300 Subject: [PATCH 025/102] Updated on 2026-08-14 --- .../tap/di/domain/TokensDomainModule.kt | 8 ++ .../settings/DefaultSwapPromoRepository.kt | 3 +- domain/tokens/build.gradle.kts | 3 + .../model/warnings/CryptoCurrencyWarning.kt | 2 + .../tokens/GetCurrencyWarningsUseCase.kt | 91 ++++++++++++++++--- .../components/TokenDetailsNotification.kt | 17 ++++ .../TokenDetailsNotificationConverter.kt | 4 + .../tokendetails/ui/TokenDetailsScreen.kt | 24 +++-- .../viewmodels/TokenDetailsClickIntents.kt | 3 + .../viewmodels/TokenDetailsViewModel.kt | 10 +- .../state/components/WalletNotification.kt | 12 +++ .../components/common/WalletNotifications.kt | 30 +++--- .../wallet/viewmodels/WalletClickIntents.kt | 2 + .../WalletNotificationsListFactory.kt | 23 ++++- .../wallet/viewmodels/WalletViewModel.kt | 8 ++ 15 files changed, 204 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt index d4488fe919..d62f5001d9 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TokensDomainModule.kt @@ -1,9 +1,11 @@ package com.tangem.tap.di.domain import com.tangem.domain.exchange.RampStateManager +import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.repository.* import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -83,6 +85,9 @@ internal object TokensDomainModule { currenciesRepository: CurrenciesRepository, quotesRepository: QuotesRepository, networksRepository: NetworksRepository, + marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, + swapRepository: SwapRepository, + showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, dispatchers: CoroutineDispatcherProvider, ): GetCurrencyWarningsUseCase { return GetCurrencyWarningsUseCase( @@ -90,6 +95,9 @@ internal object TokensDomainModule { currenciesRepository = currenciesRepository, quotesRepository = quotesRepository, networksRepository = networksRepository, + marketCryptoCurrencyRepository = marketCryptoCurrencyRepository, + swapRepository = swapRepository, + showSwapPromoTokenUseCase = showSwapPromoTokenUseCase, dispatchers = dispatchers, ) } diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt index 5b4611572e..dd94a2e07c 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt @@ -43,12 +43,13 @@ 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.MONTH) == MONTH_KEY && currentDate.get(Calendar.YEAR) == YEAR_KEY } 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 } } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 8fabb669f0..e32512bb66 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -18,6 +18,9 @@ dependencies { implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) implementation(projects.domain.appCurrency.models) + implementation(projects.domain.settings) + implementation(projects.features.swap.domain.api) + implementation(projects.features.swap.domain.models) /** Project - Other */ implementation(projects.core.utils) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt index f6c14d8482..d818e80eea 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/warnings/CryptoCurrencyWarning.kt @@ -31,4 +31,6 @@ sealed class CryptoCurrencyWarning { data class Rent(val rent: BigDecimal, val exemptionAmount: BigDecimal) : CryptoCurrencyWarning() data class HasPendingTransactions(val blockchainSymbol: String) : CryptoCurrencyWarning() + + object SwapPromo : CryptoCurrencyWarning() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 0ff4f92ae7..f658706441 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,24 +1,33 @@ package com.tangem.domain.tokens +import arrow.core.Either +import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.warnings.CryptoCurrencyWarning import com.tangem.domain.tokens.operations.CurrenciesStatusesOperations import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.flow.* import java.math.BigDecimal +@Suppress("LongParameterList") class GetCurrencyWarningsUseCase( private val walletManagersFacade: WalletManagersFacade, private val currenciesRepository: CurrenciesRepository, private val quotesRepository: QuotesRepository, private val networksRepository: NetworksRepository, + private val swapRepository: SwapRepository, + private val marketCryptoCurrencyRepository: MarketCryptoCurrencyRepository, + private val showSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val dispatchers: CoroutineDispatcherProvider, ) { @@ -29,20 +38,32 @@ class GetCurrencyWarningsUseCase( isSingleWalletWithTokens: Boolean, ): Flow> { val currency = currencyStatus.currency + val operations = CurrenciesStatusesOperations( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + networksRepository = networksRepository, + userWalletId = userWalletId, + ) return combine( getCoinRelatedWarnings( - userWalletId = userWalletId, + operations = operations, networkId = currency.network.id, currencyId = currency.id, derivationPath = derivationPath, isSingleWalletWithTokens = isSingleWalletWithTokens, ), + operations.getCurrenciesStatusesFlow().conflate(), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), - flowOf(getNetworkUnavailableWarning(currencyStatus)), - flowOf(getNetworkNoAccountWarning(currencyStatus)), - ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeNetworkUnavailable, maybeNetworkNoAccount -> + showSwapPromoTokenUseCase().conflate(), + ) { coinRelatedWarnings, cryptoStatuses, maybeRentWarning, maybeEdWarning, shouldShowSwapPromo -> setOfNotNull( + getSwapPromoNotificationWarning( + shouldShowSwapPromo = shouldShowSwapPromo, + userWalletId = userWalletId, + currencyStatus = currencyStatus, + cryptoStatuses = cryptoStatuses, + ), maybeRentWarning, maybeEdWarning?.let { CryptoCurrencyWarning.ExistentialDeposit( @@ -51,27 +72,69 @@ class GetCurrencyWarningsUseCase( ) }, *coinRelatedWarnings.toTypedArray(), - maybeNetworkUnavailable, - maybeNetworkNoAccount, + getNetworkUnavailableWarning(currencyStatus), + getNetworkNoAccountWarning(currencyStatus), ) }.flowOn(dispatchers.io) } + private suspend fun getSwapPromoNotificationWarning( + shouldShowSwapPromo: Boolean, + userWalletId: UserWalletId, + currencyStatus: CryptoCurrencyStatus, + cryptoStatuses: Either>, + ): CryptoCurrencyWarning? { + val currency = currencyStatus.currency + return if (shouldShowSwapPromo && marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)) { + cryptoStatuses.fold( + ifLeft = { null }, + ifRight = { cryptoCurrencyStatuses -> + val pairs = swapRepository.getPairs( + LeastTokenInfo( + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = currency.network.backendId, + ), + cryptoCurrencyStatuses.map { it.currency }, + ) + val filteredCurrencies = cryptoCurrencyStatuses.filterNot { + it.currency.network.backendId == currency.network.backendId + } + val currencyPairs = pairs.pairs.filter { + it.from.network == currency.network.backendId || + it.to.network == currency.network.backendId + } + val isExchangeable = currencyPairs.any { pair -> + val availablePair = if (currencyStatus.value.amount.isZero()) { + filteredCurrencies.filterNot { it.value.amount.isZero() } + } else { + filteredCurrencies + } + availablePair + .any { + it.currency.network.backendId == pair.to.network || + it.currency.network.backendId == pair.from.network + } + } + if (isExchangeable) { + CryptoCurrencyWarning.SwapPromo + } else { + null + } + }, + ) + } else { + null + } + } + @Suppress("LongParameterList") private suspend fun getCoinRelatedWarnings( - userWalletId: UserWalletId, + operations: CurrenciesStatusesOperations, networkId: Network.ID, currencyId: CryptoCurrency.ID, derivationPath: Network.DerivationPath, isSingleWalletWithTokens: Boolean, ): Flow> { - val operations = CurrenciesStatusesOperations( - currenciesRepository = currenciesRepository, - quotesRepository = quotesRepository, - networksRepository = networksRepository, - userWalletId = userWalletId, - ) - val currencyFlow = if (isSingleWalletWithTokens) { operations.getCurrencyStatusSingleWalletWithTokensFlow(currencyId) } else { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt index 989aa84d9b..fe9ef56abc 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsNotification.kt @@ -39,6 +39,23 @@ internal sealed class TokenDetailsNotification(val config: NotificationConfig) { ), ) + data class SwapPromo( + val onSwapClick: () -> Unit, + val onCloseClick: () -> Unit, + ) : TokenDetailsNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.token_swap_promotion_title), + subtitle = resourceReference(id = R.string.token_swap_promotion_message), + iconResId = R.drawable.ic_swap_promo_34, + backgroundResId = R.drawable.img_swap_promo_banner_background, + onCloseClick = onCloseClick, + buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( + text = resourceReference(id = com.tangem.core.ui.R.string.token_swap_promotion_button), + onClick = onSwapClick, + ), + ), + ) + object NetworksUnreachable : Warning( title = resourceReference(R.string.warning_network_unreachable_title), subtitle = resourceReference(R.string.warning_network_unreachable_message), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index d9362571ca..0f4a2d7876 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -45,6 +45,10 @@ internal class TokenDetailsNotificationConverter( is CryptoCurrencyWarning.HasPendingTransactions -> TokenDetailsNotification.HasPendingTransactions( coinSymbol = warning.blockchainSymbol, ) + is CryptoCurrencyWarning.SwapPromo -> TokenDetailsNotification.SwapPromo( + onSwapClick = clickIntents::onSwapClick, + onCloseClick = clickIntents::onSwapPromoDismiss, + ) } } } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt index 57f5e40688..1f1ae20a89 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/TokenDetailsScreen.kt @@ -28,6 +28,7 @@ import com.tangem.core.ui.components.bottomsheets.tokenreceive.TokenReceiveBotto import com.tangem.core.ui.components.marketprice.MarketPriceBlock import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationWithBackground import com.tangem.core.ui.components.transactions.state.TxHistoryState import com.tangem.core.ui.components.transactions.txHistoryItems import com.tangem.core.ui.event.EventEffect @@ -104,14 +105,21 @@ internal fun TokenDetailsScreen(state: TokenDetailsState) { key = { it::class.java }, contentType = { it.config::class.java }, itemContent = { - Notification( - modifier = itemModifier.animateItemPlacement(), - config = it.config, - iconTint = when (it) { - is TokenDetailsNotification.Warning -> null - is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent - }, - ) + if (it is TokenDetailsNotification.SwapPromo) { + NotificationWithBackground( + config = it.config, + modifier = itemModifier.animateItemPlacement(), + ) + } else { + Notification( + modifier = itemModifier.animateItemPlacement(), + config = it.config, + iconTint = when (it) { + is TokenDetailsNotification.Informational -> TangemTheme.colors.icon.accent + else -> null + }, + ) + } }, ) if (state.isMarketPriceAvailable) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 95093f7fdb..05c6e64828 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -3,6 +3,7 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels import com.tangem.core.ui.components.bottomsheets.tokenreceive.AddressModel import com.tangem.domain.tokens.model.CryptoCurrency +@Suppress("TooManyFunctions") interface TokenDetailsClickIntents { fun onBackClick() @@ -42,4 +43,6 @@ interface TokenDetailsClickIntents { fun onSwapTransactionClick(txId: String) fun onGoToProviderClick(url: String) + + fun onSwapPromoDismiss() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 8b10c5c379..1fd2811a87 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -20,6 +20,7 @@ import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.redux.ReduxStateHolder +import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.* import com.tangem.domain.tokens.legacy.TradeCryptoAction import com.tangem.domain.tokens.model.CryptoCurrency @@ -35,8 +36,8 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetExploreUrlUseCase import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase import com.tangem.domain.wallets.usecase.GetUserWalletUseCase -import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRouter import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState @@ -74,6 +75,7 @@ internal class TokenDetailsViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase, private val getMultiCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusesSyncUseCase, + private val shouldShowSwapPromoTokenUseCase: ShouldShowSwapPromoTokenUseCase, private val swapRepository: SwapRepository, private val swapTransactionRepository: SwapTransactionRepository, private val swapTransactionStatusStore: SwapTransactionStatusStore, @@ -547,6 +549,12 @@ internal class TokenDetailsViewModel @Inject constructor( router.openUrl(url) } + override fun onSwapPromoDismiss() { + viewModelScope.launch(dispatchers.main) { + shouldShowSwapPromoTokenUseCase.neverToShow() + } + } + private companion object { const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt index 331ccc1502..6aeebbc6c7 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletNotification.kt @@ -166,4 +166,16 @@ sealed class WalletNotification(val config: NotificationConfig) { onCloseClick = onCloseClick, ), ) + + data class SwapPromo( + val onCloseClick: () -> Unit, + ) : WalletNotification( + config = NotificationConfig( + title = resourceReference(id = R.string.main_swap_promotion_title), + subtitle = resourceReference(id = R.string.main_swap_promotion_message), + iconResId = R.drawable.ic_swap_promo_34, + backgroundResId = R.drawable.img_swap_promo_banner_background, + onCloseClick = onCloseClick, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt index 9e0f9f5123..f258a71ce9 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletNotifications.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.ui.Modifier import com.tangem.core.ui.components.notifications.Notification +import com.tangem.core.ui.components.notifications.NotificationWithBackground import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.wallet.presentation.wallet.state.components.WalletNotification import kotlinx.collections.immutable.ImmutableList @@ -24,17 +25,24 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.warning - is WalletNotification.Informational -> TangemTheme.colors.icon.accent - is WalletNotification.RateApp -> TangemTheme.colors.icon.attention - is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 - is WalletNotification.Warning -> null - }, - ) + if (it is WalletNotification.SwapPromo) { + NotificationWithBackground( + config = it.config, + modifier = modifier.animateItemPlacement(), + ) + } else { + Notification( + config = it.config, + modifier = modifier.animateItemPlacement(), + iconTint = when (it) { + is WalletNotification.Critical -> TangemTheme.colors.icon.warning + is WalletNotification.Informational -> TangemTheme.colors.icon.accent + is WalletNotification.RateApp -> TangemTheme.colors.icon.attention + is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + else -> null + }, + ) + } }, ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt index 5c390dcb0b..dad5efb741 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletClickIntents.kt @@ -72,4 +72,6 @@ internal interface WalletClickIntents { fun onExploreClick() fun onTransactionClick(txHash: String) + + fun onCloseSwapPromoNotificationClick() } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt index 6540090c11..c0145c919b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletNotificationsListFactory.kt @@ -5,6 +5,7 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.demo.IsDemoCardUseCase import com.tangem.domain.settings.IsReadyToShowRateAppUseCase +import com.tangem.domain.settings.ShouldShowSwapPromoWalletUseCase import com.tangem.domain.tokens.GetMissedAddressesCryptoCurrenciesUseCase import com.tangem.domain.tokens.error.GetCurrenciesError import com.tangem.domain.tokens.model.CryptoCurrency @@ -26,16 +27,21 @@ import kotlinx.coroutines.flow.flowOf * @property isDemoCardUseCase use case that checks if card is demo * @property isReadyToShowRateAppUseCase use case that checks if card is user already rate app * @property isNeedToBackupUseCase use case that checks if wallet need backup cards + * @property getMissedAddressCryptoCurrenciesUseCase use case that gets missed address crypto currencies + * @property hasSingleWalletSignedHashesUseCase use case that checks if single wallet signed hashes + * @property shouldShowSwapPromoWalletUseCase use case that checks if should show swap promo * @property clickIntents screen click intents * [REDACTED_AUTHOR] */ +@Suppress("LongParameterList") internal class WalletNotificationsListFactory( private val isDemoCardUseCase: IsDemoCardUseCase, private val isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, private val isNeedToBackupUseCase: IsNeedToBackupUseCase, private val getMissedAddressCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, private val hasSingleWalletSignedHashesUseCase: HasSingleWalletSignedHashesUseCase, + private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, private val clickIntents: WalletClickIntents, ) { @@ -51,9 +57,12 @@ internal class WalletNotificationsListFactory( flow2 = isReadyToShowRateAppUseCase().conflate(), flow3 = isNeedToBackupUseCase(selectedWallet.walletId).conflate(), flow4 = getMissedAddressCryptoCurrenciesUseCase(selectedWallet.walletId).conflate(), - ) { hasSignedHashes, isReadyToShowRating, isNeedToBackup, maybeMissedAddressCurrencies -> + flow5 = shouldShowSwapPromoWalletUseCase().conflate(), + ) { hasSignedHashes, isReadyToShowRating, isNeedToBackup, maybeMissedAddressCurrencies, isShowSwapPromo -> readyForRateAppNotification = true buildList { + addSwapPromoNotification(isShowSwapPromo, cardTypesResolver) + addCriticalNotifications(cardTypesResolver) addInformationalNotifications(cardTypesResolver, maybeMissedAddressCurrencies) @@ -77,6 +86,18 @@ internal class WalletNotificationsListFactory( } } + private fun MutableList.addSwapPromoNotification( + showSwapPromo: Boolean, + cardTypesResolver: CardTypesResolver, + ) { + addIf( + element = WalletNotification.SwapPromo( + clickIntents::onCloseSwapPromoNotificationClick, + ), + condition = showSwapPromo && cardTypesResolver.isMultiwalletAllowed(), + ) + } + private fun MutableList.addCriticalNotifications(cardTypesResolver: CardTypesResolver) { addIf( element = WalletNotification.Critical.DevCard, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 6534df2c8c..7d3c8f1dab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -128,6 +128,7 @@ internal class WalletViewModel @Inject constructor( private val getExplorerTransactionUrlUseCase: GetExplorerTransactionUrlUseCase, private val isDemoCardUseCase: IsDemoCardUseCase, private val scanCardToUnlockWalletUseCase: ScanCardToUnlockWalletClickHandler, + private val shouldShowSwapPromoWalletUseCase: ShouldShowSwapPromoWalletUseCase, isReadyToShowRateAppUseCase: IsReadyToShowRateAppUseCase, isNeedToBackupUseCase: IsNeedToBackupUseCase, getMissedAddressesCryptoCurrenciesUseCase: GetMissedAddressesCryptoCurrenciesUseCase, @@ -146,6 +147,7 @@ internal class WalletViewModel @Inject constructor( isNeedToBackupUseCase = isNeedToBackupUseCase, getMissedAddressCryptoCurrenciesUseCase = getMissedAddressesCryptoCurrenciesUseCase, hasSingleWalletSignedHashesUseCase = hasSingleWalletSignedHashesUseCase, + shouldShowSwapPromoWalletUseCase = shouldShowSwapPromoWalletUseCase, clickIntents = this, ) @@ -774,6 +776,12 @@ internal class WalletViewModel @Inject constructor( refreshSingleCurrencyContent(selectedWalletIndex) } + override fun onCloseSwapPromoNotificationClick() { + viewModelScope.launch(dispatchers.main) { + shouldShowSwapPromoWalletUseCase.neverToShow() + } + } + // FIXME: refreshSingleCurrencyContent mustn't update the TxHistory and Buttons. It only must fetch primary // currency. Now it not works because GetPrimaryCurrency's subscriber uses .distinctUntilChanged() private fun refreshSingleCurrencyContent(walletIndex: Int) { From a34cb0687c90badd6cde57f26609e48bbcbd2765 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 13:46:29 +0300 Subject: [PATCH 026/102] Updated on 2026-08-14 --- .../analytics/TokenSwapPromoAnalyticsEvent.kt | 18 ++++++++++++++++++ .../TokenDetailsNotificationConverter.kt | 2 +- .../viewmodels/TokenDetailsClickIntents.kt | 2 ++ .../viewmodels/TokenDetailsViewModel.kt | 7 +++++++ .../analytics/WalletScreenAnalyticsEvent.kt | 2 ++ .../wallet/viewmodels/WalletViewModel.kt | 1 + 6 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt new file mode 100644 index 0000000000..cc39985633 --- /dev/null +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/analytics/TokenSwapPromoAnalyticsEvent.kt @@ -0,0 +1,18 @@ +package com.tangem.domain.tokens.models.analytics + +import com.tangem.core.analytics.models.AnalyticsEvent + +sealed class TokenSwapPromoAnalyticsEvent( + event: String, + params: Map = mapOf(), +) : AnalyticsEvent("Swap Promo", event, params, null) { + + object Close : TokenSwapPromoAnalyticsEvent(event = "Button - Close") + + class Exchange( + token: String, + ) : TokenSwapPromoAnalyticsEvent( + event = "Button - Exchange Now", + params = mapOf("Token" to token), + ) +} \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt index 0f4a2d7876..de2e921017 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsNotificationConverter.kt @@ -46,7 +46,7 @@ internal class TokenDetailsNotificationConverter( coinSymbol = warning.blockchainSymbol, ) is CryptoCurrencyWarning.SwapPromo -> TokenDetailsNotification.SwapPromo( - onSwapClick = clickIntents::onSwapClick, + onSwapClick = clickIntents::onSwapPromoClick, onCloseClick = clickIntents::onSwapPromoDismiss, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt index 05c6e64828..d049e3bf31 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsClickIntents.kt @@ -45,4 +45,6 @@ interface TokenDetailsClickIntents { fun onGoToProviderClick(url: String) fun onSwapPromoDismiss() + + fun onSwapPromoClick() } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1fd2811a87..ffda8ffed0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -29,6 +29,7 @@ import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenReceiveAnalyticsEvent import com.tangem.domain.tokens.models.analytics.TokenScreenAnalyticsEvent +import com.tangem.domain.tokens.models.analytics.TokenSwapPromoAnalyticsEvent import com.tangem.domain.txhistory.usecase.GetExplorerTransactionUrlUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsCountUseCase import com.tangem.domain.txhistory.usecase.GetTxHistoryItemsUseCase @@ -552,9 +553,15 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onSwapPromoDismiss() { viewModelScope.launch(dispatchers.main) { shouldShowSwapPromoTokenUseCase.neverToShow() + analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close) } } + override fun onSwapPromoClick() { + onSwapClick() + analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) + } + private companion object { const val EXCHANGE_STATUS_UPDATE_DELAY = 10_000L } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index f244b3ffca..0d4dcc4a3f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -63,4 +63,6 @@ sealed class WalletScreenAnalyticsEvent { object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked") object WalletUnlockTapped : MainScreen(event = "Button - Wallet Unlock Tapped") } + + object SwapPromo : AnalyticsEvent(category = "Swap Promo", event = "Button - Close") } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 7d3c8f1dab..a410bff92e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -779,6 +779,7 @@ internal class WalletViewModel @Inject constructor( override fun onCloseSwapPromoNotificationClick() { viewModelScope.launch(dispatchers.main) { shouldShowSwapPromoWalletUseCase.neverToShow() + analyticsEventsHandler.send(WalletScreenAnalyticsEvent.SwapPromo) } } From e65d8a05812aef5b141eb12af391af092031fe61 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 12:29:40 +0300 Subject: [PATCH 027/102] Updated on 2026-08-14 --- .../state/SwapTransactionsState.kt | 12 +++--- .../state/factory/TokenDetailsStateFactory.kt | 14 +++++++ ...enDetailsSwapTransactionsStateConverter.kt | 40 +++++++++---------- .../exchange/ExchangeStatusBlock.kt | 10 ++--- .../exchange/ExchangeStatusBottomSheet.kt | 9 ++--- .../exchange/ExchangeStatusItems.kt | 4 +- .../viewmodels/ExchangeStatusFactory.kt | 7 ++-- .../viewmodels/TokenDetailsViewModel.kt | 10 ++++- .../analytics/WalletScreenAnalyticsEvent.kt | 2 - .../wallet/viewmodels/WalletViewModel.kt | 1 - 10 files changed, 62 insertions(+), 47 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index 5e42550ab6..d55821009b 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -1,12 +1,13 @@ package com.tangem.feature.tokendetails.presentation.tokendetails.state +import androidx.compose.runtime.Immutable import com.tangem.core.ui.components.currency.tokenicon.TokenIconState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.ExchangeStatusNotifications -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.collections.immutable.ImmutableList internal data class SwapTransactionsState( val txId: String, @@ -14,10 +15,10 @@ internal data class SwapTransactionsState( val txUrl: String? = null, val timestamp: TextReference, val fiatSymbol: String, - val activeStatus: MutableStateFlow, - val hasFailed: MutableStateFlow, - val statuses: MutableStateFlow>, - val notification: MutableStateFlow = MutableStateFlow(null), + val activeStatus: ExchangeStatus?, + val hasFailed: Boolean, + val statuses: ImmutableList, + val notification: ExchangeStatusNotifications? = null, val toCryptoCurrencyId: CryptoCurrency.ID, val toCryptoAmount: String, val toCryptoSymbol: String, @@ -32,6 +33,7 @@ internal data class SwapTransactionsState( val onGoToProviderClick: (String) -> Unit, ) +@Immutable internal class ExchangeStatusState( val status: ExchangeStatus, val text: TextReference, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt index 2c2330ba57..39b31fb3f0 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsStateFactory.kt @@ -34,6 +34,7 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +@Suppress("TooManyFunctions") internal class TokenDetailsStateFactory( private val currentStateProvider: Provider, private val appCurrencyProvider: Provider, @@ -247,6 +248,19 @@ internal class TokenDetailsStateFactory( ) } + fun updateStateWithExchangeStatusBottomSheet(swapTxState: SwapTransactionsState): TangemBottomSheetConfig? { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig + val currentConfig = bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return bottomSheetConfig + return bottomSheetConfig.copy( + content = if (currentConfig.value != swapTxState) { + ExchangeStatusBottomSheetConfig(swapTxState) + } else { + currentConfig + }, + ) + } + fun getStateAndTriggerEvent( state: TokenDetailsState, errorMessage: TextReference, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 3650c86759..fd13879bc9 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -20,11 +20,10 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.component import com.tangem.feature.tokendetails.presentation.tokendetails.viewmodels.TokenDetailsClickIntents import com.tangem.features.tokendetails.impl.R import com.tangem.utils.converter.Converter +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toPersistentList -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.update import java.math.BigDecimal internal class TokenDetailsSwapTransactionsStateConverter( @@ -70,14 +69,12 @@ internal class TokenDetailsSwapTransactionsStateConverter( txUrl = transaction.status?.txUrl, timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"), fiatSymbol = appCurrency.symbol, - statuses = MutableStateFlow(getStatuses(transaction.status?.status)), - hasFailed = MutableStateFlow(transaction.status?.status == ExchangeStatus.Failed), - activeStatus = MutableStateFlow(transaction.status?.status), - notification = MutableStateFlow( - getNotification( - transaction.status?.status, - transaction.status?.txUrl, - ), + statuses = getStatuses(transaction.status?.status), + hasFailed = transaction.status?.status == ExchangeStatus.Failed, + activeStatus = transaction.status?.status, + notification = getNotification( + transaction.status?.status, + transaction.status?.txUrl, ), toCryptoCurrencyId = toCurrency.currency.id, toCryptoAmount = BigDecimalFormatter.formatCryptoAmount( @@ -109,13 +106,16 @@ internal class TokenDetailsSwapTransactionsStateConverter( return result.toPersistentList() } - fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?) { - if (statusModel == null || tx.activeStatus.value == statusModel.status) return - val hasFailed = tx.hasFailed.value || statusModel.status == ExchangeStatus.Failed - tx.activeStatus.update { statusModel.status } - tx.hasFailed.update { hasFailed } - tx.notification.update { getNotification(statusModel.status, statusModel.txUrl) } - tx.statuses.update { getStatuses(statusModel.status, hasFailed) } + fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?): SwapTransactionsState { + if (statusModel == null || tx.activeStatus == statusModel.status) return tx + val hasFailed = tx.hasFailed || statusModel.status == ExchangeStatus.Failed + return tx.copy( + activeStatus = statusModel.status, + hasFailed = hasFailed, + notification = getNotification(statusModel.status, statusModel.txUrl), + statuses = getStatuses(statusModel.status, hasFailed), + txUrl = statusModel.txUrl, + ) } private fun getFiatAmount(toFiatAmount: BigDecimal): String { @@ -149,8 +149,8 @@ internal class TokenDetailsSwapTransactionsStateConverter( } } - private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): List { - if (status == null) return emptyList() + private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList { + if (status == null) return persistentListOf() val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting val isConfirming = status == ExchangeStatus.Confirming val isVerifying = status == ExchangeStatus.Verifying @@ -181,7 +181,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( isRefunded = isRefunded, hasFailed = hasFailed, ), - ) + ).toPersistentList() } private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index 9d44f11a9e..1574bb2db6 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -18,23 +18,21 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.SpacerWMax import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.res.TangemTheme import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.tokendetails.presentation.tokendetails.state.ExchangeStatusState import com.tangem.features.tokendetails.impl.R -import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.collections.immutable.ImmutableList @Composable internal fun ExchangeStatusBlock( - statuses: MutableStateFlow>, + statuses: ImmutableList, showLink: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, ) { - val statusValues = statuses.collectAsStateWithLifecycle() Column( modifier = modifier .clip(TangemTheme.shapes.roundedCornersXMedium) @@ -77,10 +75,10 @@ internal fun ExchangeStatusBlock( } } - statusValues.value.forEachIndexed { index, item -> + statuses.forEachIndexed { index, item -> ExchangeStatusStep( stepStatus = item, - isLast = index == statusValues.value.lastIndex, + isLast = index == statuses.lastIndex, ) } } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index f8850738e2..7cd58e238d 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -8,7 +8,6 @@ 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.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH12 @@ -36,8 +35,6 @@ internal fun ExchangeStatusBottomSheet(config: TangemBottomSheetConfig) { @Composable private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetConfig) { val config = content.value - val status = config.activeStatus.collectAsStateWithLifecycle() - val notification = config.notification.collectAsStateWithLifecycle() Column( modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing16), ) { @@ -77,15 +74,15 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC SpacerH12() ExchangeStatusBlock( statuses = config.statuses, - showLink = notification.value == null && config.txUrl != null, + showLink = config.notification == null && config.txUrl != null, onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) }, ) AnimatedContent( - targetState = notification.value, + targetState = config.notification, label = "Exchange Status Notification Change", ) { it?.let { - val tint = when (status.value) { + val tint = when (config.activeStatus) { ExchangeStatus.Verifying -> TangemTheme.colors.icon.attention ExchangeStatus.Failed -> TangemTheme.colors.icon.warning else -> null diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index c73e3e3051..c32011f436 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.constraintlayout.compose.* -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.atoms.text.EllipsisText import com.tangem.core.ui.components.atoms.text.TextEllipsis import com.tangem.core.ui.components.currency.tokenicon.TokenIcon @@ -41,8 +40,7 @@ internal fun LazyListScope.swapTransactionsItems( contentType = { swapTxs[it]::class.java }, ) { val item = swapTxs[it] - val status = item.activeStatus.collectAsStateWithLifecycle() - val (iconRes, tint) = when (status.value) { + 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 else -> null to null diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index e1f2ca8766..7ef7622a82 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -12,8 +12,8 @@ import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.models.analytics.TokenExchangeAnalyticsEvent import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase -import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.api.SwapRepository 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 @@ -81,8 +81,9 @@ internal class ExchangeStatusFactory( swapTxList.map { tx -> async { val statusModel = getExchangeStatus(tx.txId) - swapTransactionsStateConverter.updateTxStatus(tx, statusModel) - tx.removeIfFinished(statusModel?.status) + swapTransactionsStateConverter + .updateTxStatus(tx, statusModel) + .removeIfFinished(statusModel?.status) } } .awaitAll() diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index ffda8ffed0..100aabe066 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -43,6 +43,7 @@ import com.tangem.feature.tokendetails.presentation.router.InnerTokenDetailsRout 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.TokenDetailsStateFactory +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.features.tokendetails.impl.R import com.tangem.features.tokendetails.navigation.TokenDetailsRouter import com.tangem.utils.coroutines.* @@ -229,7 +230,14 @@ internal class TokenDetailsViewModel @Inject constructor( } }, onSuccess = { updatedTxs -> - uiState = uiState.copy(swapTxs = updatedTxs) + val config = uiState.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig + val currentTx = updatedTxs.firstOrNull { it.txId == config?.value?.txId } + uiState = uiState.copy( + swapTxs = updatedTxs, + bottomSheetConfig = currentTx?.let( + stateFactory::updateStateWithExchangeStatusBottomSheet, + ), + ) }, onError = {}, ), diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt index 0d4dcc4a3f..f244b3ffca 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/WalletScreenAnalyticsEvent.kt @@ -63,6 +63,4 @@ sealed class WalletScreenAnalyticsEvent { object NoticeWalletLocked : MainScreen(event = "Notice - Wallet Locked") object WalletUnlockTapped : MainScreen(event = "Button - Wallet Unlock Tapped") } - - object SwapPromo : AnalyticsEvent(category = "Swap Promo", event = "Button - Close") } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index a410bff92e..7d3c8f1dab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -779,7 +779,6 @@ internal class WalletViewModel @Inject constructor( override fun onCloseSwapPromoNotificationClick() { viewModelScope.launch(dispatchers.main) { shouldShowSwapPromoWalletUseCase.neverToShow() - analyticsEventsHandler.send(WalletScreenAnalyticsEvent.SwapPromo) } } From b38cfdcf9ef38a203528aa25df87bfd3663bbd68 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 16:21:41 +0300 Subject: [PATCH 028/102] Updated on 2026-08-14 --- .../NotificationWithBackground.kt | 14 ++++++------ .../main/res/drawable/ic_swap_promo_34.xml | 20 ------------------ .../src/main/res/drawable/img_swap_promo.webp | Bin 0 -> 2284 bytes .../components/TokenDetailsNotification.kt | 2 +- .../state/components/WalletNotification.kt | 2 +- 5 files changed, 10 insertions(+), 28 deletions(-) delete mode 100644 core/ui/src/main/res/drawable/ic_swap_promo_34.xml create mode 100644 core/ui/src/main/res/drawable/img_swap_promo.webp diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index 072a7389f6..b377102b35 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -66,10 +66,12 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = Image( painter = painterResource(config.iconResId), contentDescription = null, - modifier = Modifier.constrainAs(iconRef) { - start.linkTo(parent.start, spacing12) - linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f) - }, + modifier = Modifier + .size(TangemTheme.dimens.size34) + .constrainAs(iconRef) { + start.linkTo(parent.start, spacing12) + linkTo(titleRef.top, subtitleRef.bottom, bias = 0.1f) + }, ) Text( text = config.title.resolveReference(), @@ -147,7 +149,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid NotificationConfig( title = resourceReference(id = R.string.main_swap_promotion_title), subtitle = resourceReference(id = R.string.main_swap_promotion_message), - iconResId = R.drawable.ic_swap_promo_34, + iconResId = R.drawable.img_swap_promo, backgroundResId = R.drawable.img_swap_promo_banner_background, ), NotificationConfig( @@ -158,7 +160,7 @@ private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvid "currencies between any chains you wish. Commission free period " + "till Dec 31.", ), - iconResId = R.drawable.ic_swap_promo_34, + iconResId = R.drawable.img_swap_promo, backgroundResId = R.drawable.img_swap_promo_banner_background, buttonsState = NotificationConfig.ButtonsState.SecondaryButtonConfig( text = resourceReference(id = R.string.token_swap_promotion_button), diff --git a/core/ui/src/main/res/drawable/ic_swap_promo_34.xml b/core/ui/src/main/res/drawable/ic_swap_promo_34.xml deleted file mode 100644 index 3c279910c8..0000000000 --- a/core/ui/src/main/res/drawable/ic_swap_promo_34.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/core/ui/src/main/res/drawable/img_swap_promo.webp b/core/ui/src/main/res/drawable/img_swap_promo.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f5bd02d24fb43d2b4f97dbd1e6fcde14cca4170 GIT binary patch literal 2284 zcmVHC>gEYuk<6JV4GoG@@mOZqj{LX&@p9{0}*@ZPiMD z+fGj9x?)Kg8z3(MpXuuW7LG~Z4f(H5@w^KNiGl`@&jYbB|wX!Hx zl2V%`&4?vREbEeNlqA!HvMS9cSY}$3^^f?W)oYujnnsjr)skX~8EdIcrjabxh|Ne` zs+C01Xc`2rMFR#Zii8-01QH>D2r3XL0vJpPU@Q_mE83@GLNNjV6NbZ;i!`F5DaGUA zWbtX}lL%lmiyDy#WqFdYG%IUxOB9GlM59@;PmVq(k1Q4s- z%Sc+z!#-S()`5}C));-*W3*a#a?Dt3yE&!nPtd5A5ssUTbuHai z#z2-9_8nu!8gqnGJEw5{2_0({cghH>b+G?tN!zwt+nUjVV`-7J0WmP>NJr9^X}ibu z*s{m=NQ<%0mk)1`cjuIz9``;!PdBG970ybt!dc-ID01ZgSz#(ng(-k0Xo4na8aKfs zXimi{cm&OOXimjtdNSh-oO1}WZ90GK=ME7u0nl8EwyhnKq-=N3JbT%;?aIGw zEB~^UjEFCd@>;?B$Da=n(SO*I4g9{|DtnG*u_2@-AFG9)P~#%|j-2W@xuE3|Ff zwv8Vh+s=-Rd@r&;Afo>cBuQ$U-g1G3b3lY2c;xBFM>dm;#Yk5yp2%cBJ$a}f*~Aw) zE0KH8w+NX91@gyx{URa7Uf=V`6LsYC86#H|dyyQ?kZa0>CGrkN6%7o&%S%J|Y^f$p zQCV4U1gdCe)lZVu1OEQ z5?s^5>^Q2&P1?1t2~&J$3t#i&8;0dtrJLE~>(t~L+}3&>;CrP~O^#5WN0<|QLXp|v z67auh3v*^4FG;wOEl>l%S04#gSlT+$&k%SozLd7E_G0XoNLv!f(=fx}lOajPpJ=yf zXLhWd*m2lEpgu&&qBYYtEN>rQUD!Dr0XZL}1Z!qi_BcPexw*bL0qQ~m(Y0{-DYu1_iS)* z>Rn@hum*teSS8al%e(sC2*w}A3EzAoLQIc!Q5P2bT~07qC)`AhGmUUMD#ee4{&`ph@h zGQRE6H7J~tUFtw?65mT}P3)mm)r(p-eRf>!G^j++ZQJ{mVqJQz5lr+5eq%f7yLRKD zUog05<-9LwR4w`VkR~)fA9jJ6O~LcGgs9-2v5`hUkybHxU~n;Cta~M+dz|0;NocQe zFwZXGWT4d;4}*d7h+fpNadN#?Y4q;S9ph0@A~p%kR3j`>PM>NA(>2{;6g~E~1&EaT zHKRuywrLnSOYd(6gmdvKI&VBMLw8Mn^AELj-rQ106~cHoGm5#C`p^bXrRG_{J6vV_ zMGx+9leP*-XuVZ~k;`vNMjYT#W_f#x8#Zl+TA}q2V5IWMv zH0a?q;-N^t1>X?Cu^-l9iBRTy@VkbYAiQCb_5F_@c3y$bBYVEF^=o-3cN0T|K|~OD`vL~ z+_)>BRN-a13BLcfzBmInQ`;NR-5g$(ZMi5dqDMD~>rF4ztd_6gl?NH{hu00S@%g7W z%JS(NQ>DOD;1Lnv`OsKp-&O5mAQl$)@E&tPpr#dPMqNl==Vt|#zn`x$bGud4mPE0W zo0P2xeSSx-;ij&eSen1+8HnlMl`aZVcFMl$HI$?Rxp|?67U_@O6i zZW4FjfRK;?Psztx0I=D!xU1akh8cTFR#KRs8%HM+#58nFE>5ir54V1x!eI-lFDTB6 z!SX5qL|pqyiV?siUFRR*Vaod{V00_Gb2Uz+E~UIM(S(0CpsXlr9>YxneD!;01H|h> z)8oQ|`~yQ1vNS+xJ5ppHRpjY6^eX{-u&Mf`iK=r$Mhk)8d)FMtiYY1^z ztCN$3u4+uj?P{G{Q&&@WvYQ?4YpHfQDgY`R8&&n4W78+>istBKZFy#TdV6+#_t?_c G&j0` Date: Wed, 13 Dec 2023 19:07:00 +0200 Subject: [PATCH 029/102] Updated on 2026-08-14 --- .../tangem/feature/swap/domain/SwapInteractorImpl.kt | 8 ++++---- .../feature/swap/converters/TokensDataConverter.kt | 10 ++++------ .../tangem/feature/swap/viewmodels/SwapViewModel.kt | 6 ++++++ 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 3754e0c835..631d3e6d51 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -102,15 +102,15 @@ internal class SwapInteractorImpl @Inject constructor( currency = currency, leastPairs = pairsLeast.pairs, cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, - tokenInfoForFilter = { it.from }, - tokenInfoForAvailable = { it.to }, + tokenInfoForFilter = { it.to }, + tokenInfoForAvailable = { it.from }, ), toGroup = getToCurrenciesGroup( currency = currency, leastPairs = pairsLeast.pairs, cryptoCurrenciesList = walletCurrencyStatusesExceptInitial, - tokenInfoForFilter = { it.to }, - tokenInfoForAvailable = { it.from }, + tokenInfoForFilter = { it.from }, + tokenInfoForAvailable = { it.to }, ), allProviders = pairsLeast.allProviders, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 6f42bbd1d2..b6c99aee61 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -2,10 +2,7 @@ package com.tangem.feature.swap.converters import com.tangem.common.Provider import com.tangem.core.ui.components.currency.tokenicon.TokenIconState -import com.tangem.core.ui.extensions.getTintForTokenIcon -import com.tangem.core.ui.extensions.networkIconResId -import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.extensions.tryGetBackgroundForTokenIcon +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 @@ -15,6 +12,7 @@ import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState +import com.tangem.feature.swap.presentation.R import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList @@ -26,8 +24,8 @@ class TokensDataConverter( ) : Converter { override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { - val availableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource - val unavailableTitle = TokenToSelectState.Title(stringReference("My tokens")) // todo replace with resource + val availableTitle = TokenToSelectState.Title(resourceReference(R.string.exchange_tokens_available_tokens_header)) + val unavailableTitle = TokenToSelectState.Title(resourceReference(R.string.exchange_tokens_available_tokens_header)) return SwapSelectTokenStateHolder( availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() 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 a36badc51e..06270b52d9 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 @@ -653,12 +653,18 @@ internal class SwapViewModel @Inject constructor( private fun onChangeCardsClicked() { val newFromToken = dataState.toCryptoCurrency val newToToken = dataState.fromCryptoCurrency + + if (newFromToken != null && newToToken != null) { dataState = dataState.copy( fromCryptoCurrency = newFromToken, toCryptoCurrency = newToToken, ) isOrderReversed = !isOrderReversed + dataState.tokensDataState?.let { + updateTokensState(it) + } + val decimals = newFromToken.currency.decimals lastAmount.value = cutAmountWithDecimals(decimals, lastAmount.value) uiState = stateBuilder.updateSwapAmount( From 7da444caf1843c899bd47f786aae3341e2f33e3d Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 21:47:11 +0200 Subject: [PATCH 030/102] Updated on 2026-08-14 --- .../feature/swap/models/SwapStateHolder.kt | 6 +- .../tangem/feature/swap/ui/StateBuilder.kt | 31 +++-- .../feature/swap/ui/SwapScreenContent.kt | 45 ++++--- .../feature/swap/viewmodels/SwapViewModel.kt | 113 +++++++++--------- 4 files changed, 115 insertions(+), 80 deletions(-) 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 ae4d9ba3cf..1502568f92 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 @@ -19,7 +19,7 @@ data class SwapStateHolder( val blockchainId: String, // not the same as networkId, its local id in app val warnings: List = emptyList(), val alert: SwapWarning.GenericWarning? = null, - val updateInProgress: Boolean = false, + val changeCardsButtonState: ChangeCardsButtonState = ChangeCardsButtonState.ENABLED, val providerState: ProviderState, val fee: FeeItemState = FeeItemState.Empty, @@ -131,4 +131,8 @@ sealed interface SwapWarning { enum class GenericWarningType { NETWORK, OTHER +} + +enum class ChangeCardsButtonState { + ENABLED, DISABLED, UPDATE_IN_PROGRESS } \ 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 18933ba236..5564b792a7 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 @@ -84,7 +84,7 @@ internal class StateBuilder( onBackClicked = actions.onBackClicked, onChangeCardsClicked = actions.onChangeCardsClicked, onMaxAmountSelected = actions.onMaxAmountSelected, - updateInProgress = true, + changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), ) @@ -138,7 +138,7 @@ internal class StateBuilder( loading = false, onClick = { }, ), - updateInProgress = false, + changeCardsButtonState = ChangeCardsButtonState.DISABLED, ) } @@ -186,7 +186,7 @@ internal class StateBuilder( swapButton = SwapButton(enabled = false, loading = true, onClick = {}), providerState = ProviderState.Loading(), permissionState = uiStateHolder.permissionState, - updateInProgress = true, + changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, ) } @@ -207,6 +207,7 @@ internal class StateBuilder( bestRatedProviderId: String, isManyProviders: Boolean, selectedFeeType: FeeType, + isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -257,7 +258,11 @@ internal class StateBuilder( loading = false, onClick = actions.onSwapClick, ), - updateInProgress = false, + changeCardsButtonState = if (isReverseSwapPossible) { + ChangeCardsButtonState.ENABLED + } else { + ChangeCardsButtonState.DISABLED + }, providerState = swapProvider.convertToContentClickableProviderState( isBestRate = bestRatedProviderId == swapProvider.providerId, fromTokenInfo = quoteModel.fromTokenInfo, @@ -360,6 +365,7 @@ internal class StateBuilder( fromToken: TokenSwapInfo, toToken: CryptoCurrencyStatus?, dataError: DataError, + isReverseSwapPossible: Boolean ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -409,7 +415,11 @@ internal class StateBuilder( loading = false, onClick = actions.onSwapClick, ), - updateInProgress = false, + changeCardsButtonState = if (isReverseSwapPossible) { + ChangeCardsButtonState.ENABLED + } else { + ChangeCardsButtonState.DISABLED + }, providerState = providerState, ) } @@ -475,6 +485,7 @@ internal class StateBuilder( fun createQuotesEmptyAmountState( uiStateHolder: SwapStateHolder, emptyAmountState: SwapState.EmptyAmountState, + isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder @@ -514,7 +525,11 @@ internal class StateBuilder( loading = false, onClick = { }, ), - updateInProgress = false, + changeCardsButtonState = if (isReverseSwapPossible) { + ChangeCardsButtonState.ENABLED + } else { + ChangeCardsButtonState.DISABLED + }, providerState = ProviderState.Empty(), ) } @@ -538,7 +553,7 @@ internal class StateBuilder( fun createSilentLoadState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( - updateInProgress = true, + changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, ) } @@ -713,7 +728,7 @@ internal class StateBuilder( onClick = onAlertClick, type = if (txState is TxState.NetworkError) GenericWarningType.NETWORK else GenericWarningType.OTHER, ), - updateInProgress = false, + changeCardsButtonState = ChangeCardsButtonState.ENABLED, ) } 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 8d1fe2e6df..cab2bb74d7 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 @@ -195,23 +195,36 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { contentColor = TangemTheme.colors.text.primary1, modifier = modifier.size(TangemTheme.dimens.size48), onClick = state.onChangeCardsClicked, - enabled = !state.updateInProgress, + enabled = state.changeCardsButtonState == ChangeCardsButtonState.ENABLED, ) { - if (state.updateInProgress) { - CircularProgressIndicator( - modifier = Modifier - .size(TangemTheme.dimens.size16) - .padding(TangemTheme.dimens.spacing14), - color = TangemTheme.colors.icon.primary1, - strokeWidth = TangemTheme.dimens.size2, - ) - } else { - Icon( - painter = painterResource(id = R.drawable.ic_exchange_vertical_24), - contentDescription = null, - tint = TangemTheme.colors.text.primary1, - modifier = Modifier.padding(TangemTheme.dimens.spacing12), - ) + when (state.changeCardsButtonState) { + ChangeCardsButtonState.UPDATE_IN_PROGRESS -> { + CircularProgressIndicator( + modifier = Modifier + .size(TangemTheme.dimens.size16) + .padding(TangemTheme.dimens.spacing14), + color = TangemTheme.colors.icon.primary1, + strokeWidth = TangemTheme.dimens.size2, + ) + } + ChangeCardsButtonState.ENABLED -> { + Icon( + painter = painterResource(id = R.drawable.ic_exchange_vertical_24), + contentDescription = null, + tint = TangemTheme.colors.text.primary1, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) + } + ChangeCardsButtonState.DISABLED -> { + Icon( + painter = painterResource(id = R.drawable.ic_exchange_vertical_24), + contentDescription = null, + tint = TangemTheme.colors.text.disabled, + modifier = Modifier.padding(TangemTheme.dimens.spacing12), + ) + } + + } } } 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 06270b52d9..0bd154c2c6 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 @@ -60,8 +60,8 @@ internal class SwapViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { - private val initialCryptoCurrency: CryptoCurrency = savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] - ?: error("no expected parameter CryptoCurrency found`") + private val initialCryptoCurrency: CryptoCurrency = + savedStateHandle[SwapFragment.CURRENCY_BUNDLE_KEY] ?: error("no expected parameter CryptoCurrency found`") private lateinit var initialCryptoCurrencyStatus: CryptoCurrencyStatus private var isBalanceHidden = true @@ -114,15 +114,12 @@ internal class SwapViewModel @Inject constructor( } override fun onCreate(owner: LifecycleOwner) { - getBalanceHidingSettingsUseCase() - .flowWithLifecycle(owner.lifecycle) - .onEach { + getBalanceHidingSettingsUseCase().flowWithLifecycle(owner.lifecycle).onEach { isBalanceHidden = it.isBalanceHidden withContext(dispatchers.main) { uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) } - } - .launchIn(viewModelScope) + }.launchIn(viewModelScope) } override fun onCleared() { @@ -175,19 +172,18 @@ internal class SwapViewModel @Inject constructor( selectedCurrency = null, ) - uiState = - stateBuilder.createInitialErrorState( - uiState, - (it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code, - ) { - uiState = stateBuilder.createInitialLoadingState( - initialCurrency = initialCryptoCurrency, - networkInfo = blockchainInteractor.getBlockchainInfo( - initialCryptoCurrency.network.backendId, - ), - ) - initTokens() - } + uiState = stateBuilder.createInitialErrorState( + uiState, + (it as? ExpressException)?.dataError?.code ?: DataError.UnknownError.code, + ) { + uiState = stateBuilder.createInitialLoadingState( + initialCurrency = initialCryptoCurrency, + networkInfo = blockchainInteractor.getBlockchainInfo( + initialCryptoCurrency.network.backendId, + ), + ) + initTokens() + } } } } @@ -216,7 +212,7 @@ internal class SwapViewModel @Inject constructor( } private fun updateTokensState(dataState: TokensDataStateExpress) { - val tokensDataState = if (!isOrderReversed) dataState.toGroup else dataState.fromGroup + val tokensDataState = if (isOrderReversed) dataState.fromGroup else dataState.toGroup uiState = stateBuilder.addTokensToState( uiState = uiState, tokensDataState = tokensDataState, @@ -295,14 +291,12 @@ internal class SwapViewModel @Inject constructor( if (providersState.isNotEmpty()) { val (provider, state) = updateLoadedQuotes(providersState) setupLoadedState(provider, state, fromToken) - val successStates = providersState - .getLastLoadedSuccessStates() + val successStates = providersState.getLastLoadedSuccessStates() val pricesLowerBest = getPricesLowerBest(successStates) uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, pricesLowerBest = pricesLowerBest, - tokenSwapInfoForProviders = successStates.entries - .associate { it.key.providerId to it.value.toTokenInfo }, + tokenSwapInfoForProviders = successStates.entries.associate { it.key.providerId to it.value.toTokenInfo }, ) } else { Timber.e("Accidentally empty quotes list") @@ -329,6 +323,7 @@ internal class SwapViewModel @Inject constructor( bestRatedProviderId = bestRatedProviderId, isManyProviders = dataState.lastLoadedSwapStates.size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, + isReverseSwapPossible = isReverseSwapPossible() ) if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { analyticsEventHandler.send( @@ -343,6 +338,7 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.createQuotesEmptyAmountState( uiStateHolder = uiState, emptyAmountState = state, + isReverseSwapPossible = isReverseSwapPossible(), ) } is SwapState.SwapError -> { @@ -353,6 +349,7 @@ internal class SwapViewModel @Inject constructor( fromToken = state.fromTokenInfo, toToken = dataState.toCryptoCurrency, dataError = state.error, + isReverseSwapPossible = isReverseSwapPossible(), ) sendErrorAnalyticsEvent(state.error, provider) } @@ -419,16 +416,15 @@ internal class SwapViewModel @Inject constructor( } private fun selectDefaultFee(state: SwapState.QuotesLoadedState): TxFee? { - return dataState.selectedFee - ?: when (val txFee = state.txFee) { - TxFeeState.Empty -> null - is TxFeeState.MultipleFeeState -> { - txFee.normalFee - } - is TxFeeState.SingleFeeState -> { - txFee.fee - } + return dataState.selectedFee ?: when (val txFee = state.txFee) { + TxFeeState.Empty -> null + is TxFeeState.MultipleFeeState -> { + txFee.normalFee } + is TxFeeState.SingleFeeState -> { + txFee.fee + } + } } @Suppress("LongMethod") @@ -453,8 +449,7 @@ internal class SwapViewModel @Inject constructor( includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount, fee = requireNotNull(dataState.selectedFee), ) - } - .onSuccess { + }.onSuccess { when (it) { is TxState.TxSent -> { val url = blockchainInteractor.getExplorerTransactionLink( @@ -499,8 +494,7 @@ internal class SwapViewModel @Inject constructor( } } } - } - .onFailure { + }.onFailure { Timber.e(it) startLoadingQuotesFromLastState() makeDefaultAlert() @@ -539,8 +533,7 @@ internal class SwapViewModel @Inject constructor( approveData = requireNotNull(dataState.approveDataModel) { "dataState.approveDataModel might not be null" }, - forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token) - ?.contractAddress + forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)?.contractAddress ?: "", fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { "dataState.fromCurrency might not be null" @@ -554,8 +547,7 @@ internal class SwapViewModel @Inject constructor( }, ), ) - } - .onSuccess { + }.onSuccess { when (it) { is TxState.TxSent -> { uiState = stateBuilder.loadingPermissionState(uiState) @@ -569,8 +561,7 @@ internal class SwapViewModel @Inject constructor( } } } - } - .onFailure { + }.onFailure { makeDefaultAlert() } } @@ -789,8 +780,8 @@ internal class SwapViewModel @Inject constructor( }, onClickFee = { val selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL - val txFeeState = dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState - ?: return@UiActions + val txFeeState = + dataState.getCurrentLoadedSwapState()?.txFee as? TxFeeState.MultipleFeeState ?: return@UiActions uiState = stateBuilder.showSelectFeeBottomSheet( uiState = uiState, selectedFee = selectedFee, @@ -868,9 +859,7 @@ internal class SwapViewModel @Inject constructor( private fun findBestQuoteProvider(state: SuccessLoadedSwapData): SwapProvider? { // finding best quotes return state.minByOrNull { - if (!it.value.fromTokenInfo.amountFiat.isNullOrZero() && - !it.value.toTokenInfo.amountFiat.isNullOrZero() - ) { + 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, @@ -900,11 +889,9 @@ internal class SwapViewModel @Inject constructor( } private fun createSelectedAppCurrencyFlow(): StateFlow { - return getSelectedAppCurrencyUseCase() - .map { maybeAppCurrency -> + return getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> maybeAppCurrency.getOrElse { AppCurrency.Default } - } - .stateIn( + }.stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = AppCurrency.Default, @@ -937,11 +924,27 @@ internal class SwapViewModel @Inject constructor( } private fun Map.getLastLoadedSuccessStates(): SuccessLoadedSwapData { - return this - .filter { it.value is SwapState.QuotesLoadedState } + return this.filter { it.value is SwapState.QuotesLoadedState } .mapValues { it.value as SwapState.QuotesLoadedState } } + private fun isReverseSwapPossible(): Boolean { + val from = dataState.fromCryptoCurrency ?: return false + val to = dataState.toCryptoCurrency ?: return false + + val currenciesGroup = if (isOrderReversed) { + dataState.tokensDataState?.toGroup + } else { + dataState.tokensDataState?.fromGroup + } ?: return false + + val chosen = if (isOrderReversed) from else to + + return currenciesGroup.available + .map { it.currencyStatus.currency } + .contains(chosen.currency) + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From 16a4167be4a5ebfeea060397b09ba3efa35a95ea Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 13 Dec 2023 21:53:03 +0200 Subject: [PATCH 031/102] Updated on 2026-08-14 --- .../swap/converters/TokensDataConverter.kt | 8 +- .../tangem/feature/swap/ui/StateBuilder.kt | 3 +- .../feature/swap/ui/SwapScreenContent.kt | 2 - .../feature/swap/viewmodels/SwapViewModel.kt | 144 +++++++++--------- 4 files changed, 80 insertions(+), 77 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index b6c99aee61..895fcb1ea5 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -24,8 +24,12 @@ class TokensDataConverter( ) : Converter { override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { - val availableTitle = TokenToSelectState.Title(resourceReference(R.string.exchange_tokens_available_tokens_header)) - val unavailableTitle = TokenToSelectState.Title(resourceReference(R.string.exchange_tokens_available_tokens_header)) + val availableTitle = TokenToSelectState.Title( + resourceReference(R.string.exchange_tokens_available_tokens_header), + ) + val unavailableTitle = TokenToSelectState.Title( + resourceReference(R.string.exchange_tokens_available_tokens_header), + ) return SwapSelectTokenStateHolder( availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() 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 5564b792a7..e66df2962d 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 @@ -359,13 +359,14 @@ internal class StateBuilder( } } + @Suppress("LongParameterList") fun createQuotesErrorState( uiStateHolder: SwapStateHolder, swapProvider: SwapProvider, fromToken: TokenSwapInfo, toToken: CryptoCurrencyStatus?, dataError: DataError, - isReverseSwapPossible: Boolean + isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder 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 cab2bb74d7..360cd4e016 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 @@ -223,8 +223,6 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { modifier = Modifier.padding(TangemTheme.dimens.spacing12), ) } - - } } } 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 0bd154c2c6..b7606f80d0 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 @@ -115,11 +115,11 @@ internal class SwapViewModel @Inject constructor( override fun onCreate(owner: LifecycleOwner) { getBalanceHidingSettingsUseCase().flowWithLifecycle(owner.lifecycle).onEach { - isBalanceHidden = it.isBalanceHidden - withContext(dispatchers.main) { - uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) - } - }.launchIn(viewModelScope) + isBalanceHidden = it.isBalanceHidden + withContext(dispatchers.main) { + uiState = stateBuilder.updateBalanceHiddenState(uiState, isBalanceHidden) + } + }.launchIn(viewModelScope) } override fun onCleared() { @@ -296,7 +296,8 @@ internal class SwapViewModel @Inject constructor( uiState = stateBuilder.updateProvidersBottomSheetContent( uiState = uiState, pricesLowerBest = pricesLowerBest, - tokenSwapInfoForProviders = successStates.entries.associate { it.key.providerId to it.value.toTokenInfo }, + tokenSwapInfoForProviders = successStates.entries + .associate { it.key.providerId to it.value.toTokenInfo }, ) } else { Timber.e("Accidentally empty quotes list") @@ -323,7 +324,7 @@ internal class SwapViewModel @Inject constructor( bestRatedProviderId = bestRatedProviderId, isManyProviders = dataState.lastLoadedSwapStates.size > 1, selectedFeeType = dataState.selectedFee?.feeType ?: FeeType.NORMAL, - isReverseSwapPossible = isReverseSwapPossible() + isReverseSwapPossible = isReverseSwapPossible(), ) if (uiState.warnings.any { it is SwapWarning.UnableToCoverFeeWarning }) { analyticsEventHandler.send( @@ -450,55 +451,55 @@ internal class SwapViewModel @Inject constructor( fee = requireNotNull(dataState.selectedFee), ) }.onSuccess { - when (it) { - is TxState.TxSent -> { - val url = blockchainInteractor.getExplorerTransactionLink( - networkId = dataState.networkId, - txAddress = it.txAddress, - ) - uiState = stateBuilder.createSuccessState( - uiState = uiState, - txState = it, - dataState = dataState, - txUrl = url, - onExploreClick = { - val txHash = it.txAddress - if (txHash.isNotEmpty()) { - swapRouter.openUrl(url) - } + when (it) { + is TxState.TxSent -> { + val url = blockchainInteractor.getExplorerTransactionLink( + networkId = dataState.networkId, + txAddress = it.txAddress, + ) + uiState = stateBuilder.createSuccessState( + uiState = uiState, + txState = it, + dataState = dataState, + txUrl = url, + onExploreClick = { + val txHash = it.txAddress + if (txHash.isNotEmpty()) { + swapRouter.openUrl(url) + } + analyticsEventHandler.send( + event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), + ) + }, + onStatusClick = { + val txExternalUrl = it.txExternalUrl + if (!txExternalUrl.isNullOrBlank()) { + swapRouter.openUrl(txExternalUrl) analyticsEventHandler.send( - event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), + event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol), ) - }, - onStatusClick = { - val txExternalUrl = it.txExternalUrl - if (!txExternalUrl.isNullOrBlank()) { - swapRouter.openUrl(txExternalUrl) - analyticsEventHandler.send( - event = SwapEvents.ButtonStatus(initialCryptoCurrency.symbol), - ) - } - }, - ) - sendSuccessEvent() + } + }, + ) + sendSuccessEvent() - swapRouter.openScreen(SwapNavScreen.Success) - } - is TxState.UserCancelled -> { - startLoadingQuotesFromLastState() - } - else -> { - startLoadingQuotesFromLastState() - uiState = stateBuilder.createErrorTransaction(uiState, it) { - uiState = stateBuilder.clearAlert(uiState) - } + swapRouter.openScreen(SwapNavScreen.Success) + } + is TxState.UserCancelled -> { + startLoadingQuotesFromLastState() + } + else -> { + startLoadingQuotesFromLastState() + uiState = stateBuilder.createErrorTransaction(uiState, it) { + uiState = stateBuilder.clearAlert(uiState) } } - }.onFailure { - Timber.e(it) - startLoadingQuotesFromLastState() - makeDefaultAlert() } + }.onFailure { + Timber.e(it) + startLoadingQuotesFromLastState() + makeDefaultAlert() + } } } @@ -533,8 +534,8 @@ internal class SwapViewModel @Inject constructor( approveData = requireNotNull(dataState.approveDataModel) { "dataState.approveDataModel might not be null" }, - forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token)?.contractAddress - ?: "", + forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token) + ?.contractAddress ?: "", fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { "dataState.fromCurrency might not be null" }, @@ -548,22 +549,22 @@ internal class SwapViewModel @Inject constructor( ), ) }.onSuccess { - when (it) { - is TxState.TxSent -> { - uiState = stateBuilder.loadingPermissionState(uiState) - uiState = stateBuilder.dismissBottomSheet(uiState) - startLoadingQuotesFromLastState(isSilent = true) - } - is TxState.UserCancelled -> Unit - else -> { - uiState = stateBuilder.createErrorTransaction(uiState, it) { - uiState = stateBuilder.clearAlert(uiState) - } + when (it) { + is TxState.TxSent -> { + uiState = stateBuilder.loadingPermissionState(uiState) + uiState = stateBuilder.dismissBottomSheet(uiState) + startLoadingQuotesFromLastState(isSilent = true) + } + is TxState.UserCancelled -> Unit + else -> { + uiState = stateBuilder.createErrorTransaction(uiState, it) { + uiState = stateBuilder.clearAlert(uiState) } } - }.onFailure { - makeDefaultAlert() } + }.onFailure { + makeDefaultAlert() + } } } @@ -645,7 +646,6 @@ internal class SwapViewModel @Inject constructor( val newFromToken = dataState.toCryptoCurrency val newToToken = dataState.fromCryptoCurrency - if (newFromToken != null && newToToken != null) { dataState = dataState.copy( fromCryptoCurrency = newFromToken, @@ -890,12 +890,12 @@ internal class SwapViewModel @Inject constructor( private fun createSelectedAppCurrencyFlow(): StateFlow { return getSelectedAppCurrencyUseCase().map { maybeAppCurrency -> - maybeAppCurrency.getOrElse { AppCurrency.Default } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.Eagerly, - initialValue = AppCurrency.Default, - ) + maybeAppCurrency.getOrElse { AppCurrency.Default } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.Eagerly, + initialValue = AppCurrency.Default, + ) } private fun findSwapProviders(fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus): List { From 38c9940d7d05d64ea66e7080e526fc432c90b210 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Dec 2023 13:17:02 +0300 Subject: [PATCH 032/102] Updated on 2026-08-14 --- .../tokens/GetCurrencyWarningsUseCase.kt | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index f658706441..3090438e08 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -16,6 +16,8 @@ import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.feature.swap.domain.models.domain.LeastTokenInfo import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runCatching +import com.tangem.utils.isNullOrZero import kotlinx.coroutines.flow.* import java.math.BigDecimal @@ -89,30 +91,36 @@ class GetCurrencyWarningsUseCase( cryptoStatuses.fold( ifLeft = { null }, ifRight = { cryptoCurrencyStatuses -> - val pairs = swapRepository.getPairs( - LeastTokenInfo( - contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = currency.network.backendId, - ), - cryptoCurrencyStatuses.map { it.currency }, - ) + val pairs = runCatching(dispatchers.io) { + swapRepository.getPairs( + LeastTokenInfo( + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = currency.network.backendId, + ), + cryptoCurrencyStatuses.map { it.currency }, + ) + }.getOrNull()?.pairs ?: emptyList() + val filteredCurrencies = cryptoCurrencyStatuses.filterNot { - it.currency.network.backendId == currency.network.backendId + it.currency.id == currency.id } - val currencyPairs = pairs.pairs.filter { + val currencyPairs = pairs.filter { it.from.network == currency.network.backendId || it.to.network == currency.network.backendId } val isExchangeable = currencyPairs.any { pair -> - val availablePair = if (currencyStatus.value.amount.isZero()) { - filteredCurrencies.filterNot { it.value.amount.isZero() } + val availablePair = if (currencyStatus.value.amount.isNullOrZero()) { + filteredCurrencies.filterNot { it.value.amount.isNullOrZero() } } else { filteredCurrencies } availablePair .any { - it.currency.network.backendId == pair.to.network || - it.currency.network.backendId == pair.from.network + ( + it.currency.network.backendId == pair.to.network || + it.currency.network.backendId == pair.from.network + ) && + !it.value.amount.isNullOrZero() } } if (isExchangeable) { From 49cce99b6657b072286fe1e36e21351b74f000c9 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Dec 2023 20:28:25 +0300 Subject: [PATCH 033/102] Updated on 2026-08-14 --- .../com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 b7606f80d0..b2c3060ae4 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 @@ -577,10 +577,12 @@ internal class SwapViewModel @Inject constructor( tokenDataState.toGroup } val available = group.available.filter { - it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) + it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) } val unavailable = group.unavailable.filter { - it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) + it.currencyStatus.currency.name.contains(searchQuery, ignoreCase = true) || + it.currencyStatus.currency.symbol.contains(searchQuery, ignoreCase = true) } val filteredTokenDataState = if (isOrderReversed) { tokenDataState.copy( From 5b282452e7dc00ff223faeb9ad52778479c24b8b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 14 Dec 2023 20:52:42 +0300 Subject: [PATCH 034/102] Updated on 2026-08-14 --- .../NotificationWithBackground.kt | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt index b377102b35..691cded7ba 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/notifications/NotificationWithBackground.kt @@ -22,10 +22,15 @@ import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.Dimension import androidx.constraintlayout.compose.Visibility import com.tangem.core.ui.R -import com.tangem.core.ui.components.SecondaryButtonIconStart +import com.tangem.core.ui.components.buttons.common.TangemButton +import com.tangem.core.ui.components.buttons.common.TangemButtonColors +import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.LocalIsInDarkTheme +import com.tangem.core.ui.res.TangemColorPalette.Dark6 +import com.tangem.core.ui.res.TangemColorPalette.Light4 import com.tangem.core.ui.res.TangemTheme /** @@ -113,10 +118,19 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = config.onCloseClick?.invoke() }, ) - SecondaryButtonIconStart( + val isDarkMode = LocalIsInDarkTheme.current + TangemButton( text = button?.text?.resolveReference().orEmpty(), - iconResId = button?.iconResId ?: R.drawable.ic_exchange_vertical_24, + icon = TangemButtonIconPosition.Start(button?.iconResId ?: R.drawable.ic_exchange_vertical_24), onClick = button?.onClick ?: {}, + colors = TangemButtonColors( + backgroundColor = if (isDarkMode) Light4 else TangemTheme.colors.button.secondary, + contentColor = Dark6, + disabledBackgroundColor = TangemTheme.colors.button.disabled, + disabledContentColor = TangemTheme.colors.text.disabled, + ), + enabled = true, + showProgress = false, modifier = Modifier.constrainAs(buttonRef) { start.linkTo(parent.start, spacing12) end.linkTo(parent.end, spacing12) @@ -135,7 +149,7 @@ fun NotificationWithBackground(config: NotificationConfig, modifier: Modifier = //region preview @Preview @Composable -private fun NotificationWithBackgroundPreview( +private fun NotificationWithBackgroundPreview_Light( @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, ) { TangemTheme { @@ -143,6 +157,16 @@ private fun NotificationWithBackgroundPreview( } } +@Preview +@Composable +private fun NotificationWithBackgroundPreview_Dark( + @PreviewParameter(NotificationWithBackgroundPreviewProvider::class) config: NotificationConfig, +) { + TangemTheme(isDark = true) { + NotificationWithBackground(config = config) + } +} + private class NotificationWithBackgroundPreviewProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( From fb06b08e96015df968a52702f5a68df302af5ea1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Dec 2023 10:05:07 +0300 Subject: [PATCH 035/102] Updated on 2026-08-14 --- .../tokens/GetCryptoCurrencyActionsUseCase.kt | 21 +++++++------------ 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 61962b7ca5..2a0d6ad530 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -1,6 +1,5 @@ package com.tangem.domain.tokens -import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.exchange.RampStateManager import com.tangem.domain.tokens.model.CryptoCurrency @@ -12,7 +11,6 @@ import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.wallets.models.UserWallet -import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.isNullOrZero import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -53,10 +51,9 @@ class GetCryptoCurrencyActionsUseCase( val flow = networkFlow.mapLatest { maybeCoinStatus -> createTokenActionsState( - userWalletId = userWallet.walletId, + userWallet = userWallet, coinStatus = maybeCoinStatus.getOrNull(), cryptoCurrencyStatus = cryptoCurrencyStatus, - cardTypesResolver = userWallet.scanResponse.cardTypesResolver, ) } @@ -65,19 +62,17 @@ class GetCryptoCurrencyActionsUseCase( } private suspend fun createTokenActionsState( - userWalletId: UserWalletId, + userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, - cardTypesResolver: CardTypesResolver, ): TokenActionsState { return TokenActionsState( - walletId = userWalletId, + walletId = userWallet.walletId, cryptoCurrencyStatus = cryptoCurrencyStatus, states = createListOfActions( - userWalletId, + userWallet, coinStatus, cryptoCurrencyStatus, - cardTypesResolver, ), ) } @@ -87,10 +82,9 @@ class GetCryptoCurrencyActionsUseCase( * Actions priority: [Buy Send Receive Sell Swap] */ private suspend fun createListOfActions( - userWalletId: UserWalletId, + userWallet: UserWallet, coinStatus: CryptoCurrencyStatus?, cryptoCurrencyStatus: CryptoCurrencyStatus, - cardTypesResolver: CardTypesResolver, ): List { val cryptoCurrency = cryptoCurrencyStatus.currency if (cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation) { @@ -116,10 +110,9 @@ class GetCryptoCurrencyActionsUseCase( activeList.add(TokenActionsState.ActionState.Send(true)) } - val isMulticurrencyWallet = cardTypesResolver.isTangemWallet() || cardTypesResolver.isWallet2() // swap - if (isMulticurrencyWallet) { - if (marketCryptoCurrencyRepository.isExchangeable(userWalletId, cryptoCurrency)) { + if (userWallet.isMultiCurrency) { + if (marketCryptoCurrencyRepository.isExchangeable(userWallet.walletId, cryptoCurrency)) { activeList.add(TokenActionsState.ActionState.Swap(true)) } else { disabledList.add(TokenActionsState.ActionState.Swap(false)) From cfd9da0f4bec84c114de6909d94c5ad18f23a7a5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Dec 2023 15:06:07 +0300 Subject: [PATCH 036/102] Updated on 2026-08-14 --- .../local/preferences/AppPreferencesStore.kt | 5 +++++ .../local/preferences/PreferencesKeys.kt | 2 +- .../settings/DefaultSwapPromoRepository.kt | 19 +++++++++++-------- .../ShouldShowSwapPromoTokenUseCase.kt | 6 ++++-- .../repositories/SwapPromoRepository.kt | 4 ++-- .../tokens/GetCurrencyWarningsUseCase.kt | 9 +++------ .../viewmodels/TokenDetailsViewModel.kt | 7 +++++-- 7 files changed, 31 insertions(+), 21 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt index 1619c322de..0c38cd669b 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/AppPreferencesStore.kt @@ -32,6 +32,11 @@ class AppPreferencesStore( return edit { transform(it) } } + /** Get data [T] by [key]. If data is not found, it returns [default] */ + inline fun MutablePreferences.getOrDefault(key: Preferences.Key, default: T): T { + return this[key] ?: default + } + /** * Get nullable data [T] by string [key] from [MutablePreferences] * diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 26d5b3cf08..ffa0aeee60 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -45,7 +45,7 @@ object PreferencesKeys { val IS_WALLET_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoShown") } - val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isTokenSwapPromoShown") } + val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { stringSetPreferencesKey(name = "isTokenSwapPromoShown") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt index dd94a2e07c..cfdc97ba1c 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt @@ -21,9 +21,9 @@ class DefaultSwapPromoRepository( .map { it && checkPromoPeriod() } } - override fun isReadyToShowToken(): Flow { - return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_SHOW_KEY, true) - .map { it && checkPromoPeriod() } + override fun isReadyToShowToken(userWalletId: String, currencyId: String): Flow { + return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_SHOW_KEY, emptySet()) + .map { !it.contains(userWalletId + currencyId) && checkPromoPeriod() } } override suspend fun setNeverToShowWallet() { @@ -33,11 +33,14 @@ class DefaultSwapPromoRepository( ) } - override suspend fun setNeverToShowToken() { - appPreferencesStore.store( - key = IS_TOKEN_SWAP_PROMO_SHOW_KEY, - value = false, - ) + override suspend fun setNeverToShowToken(userWalletId: String, currencyId: String) { + appPreferencesStore.editData { mutablePreferences -> + val storesCurrencies = mutablePreferences.getOrDefault(IS_TOKEN_SWAP_PROMO_SHOW_KEY, emptySet()) + mutablePreferences.set( + key = IS_TOKEN_SWAP_PROMO_SHOW_KEY, + value = storesCurrencies.toMutableSet().apply { add(userWalletId + currencyId) }, + ) + } } private fun checkPromoPeriod(): Boolean { diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt index 0c75a4f52a..6d7b0b30f9 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt @@ -5,7 +5,9 @@ import kotlinx.coroutines.flow.Flow class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) { - operator fun invoke(): Flow = swapPromoRepository.isReadyToShowToken() + operator fun invoke(userWalletId: String, currencyId: String): Flow = + swapPromoRepository.isReadyToShowToken(userWalletId, currencyId) - suspend fun neverToShow() = swapPromoRepository.setNeverToShowToken() + suspend fun neverToShow(userWalletId: String, currencyId: String) = + swapPromoRepository.setNeverToShowToken(userWalletId, currencyId) } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt index 95d31f65d8..96d180f4df 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt @@ -5,9 +5,9 @@ import kotlinx.coroutines.flow.Flow interface SwapPromoRepository { fun isReadyToShowWallet(): Flow - fun isReadyToShowToken(): Flow + fun isReadyToShowToken(userWalletId: String, currencyId: String): Flow suspend fun setNeverToShowWallet() - suspend fun setNeverToShowToken() + suspend fun setNeverToShowToken(userWalletId: String, currencyId: String) } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 3090438e08..849e3933a5 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -57,7 +57,7 @@ class GetCurrencyWarningsUseCase( operations.getCurrenciesStatusesFlow().conflate(), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), - showSwapPromoTokenUseCase().conflate(), + showSwapPromoTokenUseCase(userWalletId.stringValue, currency.id.value).conflate(), ) { coinRelatedWarnings, cryptoStatuses, maybeRentWarning, maybeEdWarning, shouldShowSwapPromo -> setOfNotNull( getSwapPromoNotificationWarning( @@ -116,11 +116,8 @@ class GetCurrencyWarningsUseCase( } availablePair .any { - ( - it.currency.network.backendId == pair.to.network || - it.currency.network.backendId == pair.from.network - ) && - !it.value.amount.isNullOrZero() + it.currency.network.backendId == pair.to.network || + it.currency.network.backendId == pair.from.network } } if (isExchangeable) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 100aabe066..ef5cb340ad 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -560,14 +560,17 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onSwapPromoDismiss() { viewModelScope.launch(dispatchers.main) { - shouldShowSwapPromoTokenUseCase.neverToShow() + shouldShowSwapPromoTokenUseCase.neverToShow(userWalletId.stringValue, cryptoCurrency.id.value) analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close) } } override fun onSwapPromoClick() { + viewModelScope.launch(dispatchers.main) { + shouldShowSwapPromoTokenUseCase.neverToShow(userWalletId.stringValue, cryptoCurrency.id.value) + analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) + } onSwapClick() - analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) } private companion object { From b19deae346aa44ca50f268c72836d6d08464db18 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Dec 2023 17:28:03 +0300 Subject: [PATCH 037/102] Updated on 2026-08-14 --- .../models/response/ExchangeProvider.kt | 6 + core/res/src/main/res/values-ru/strings.xml | 6 + core/res/src/main/res/values/strings.xml | 4 + .../swap/converters/SwapPairInfoConverter.kt | 4 + .../domain/models/domain/SwapPairLeast.kt | 2 + features/swap/presentation/build.gradle.kts | 2 + .../feature/swap/models/SwapStateHolder.kt | 12 ++ .../tangem/feature/swap/models/UiActions.kt | 2 + .../models/states/WebViewBottomSheetConfig.kt | 5 + .../tangem/feature/swap/ui/StateBuilder.kt | 32 ++++++ .../com/tangem/feature/swap/ui/SwapScreen.kt | 7 +- .../feature/swap/ui/SwapScreenContent.kt | 104 ++++++++++++++++++ .../feature/swap/ui/WebViewBottomSheet.kt | 79 +++++++++++++ .../feature/swap/viewmodels/SwapViewModel.kt | 12 +- 14 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt index 4e2bc6d69c..acbd56ec03 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeProvider.kt @@ -17,6 +17,12 @@ data class ExchangeProvider( @Json(name = "imageSmall") val imageSmallUrl: String, + + @Json(name = "termsOfUse") + val termsOfUse: String?, + + @Json(name = "privacyPolicy") + val privacyPolicy: String?, ) enum class ExchangeProviderType { diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 21bec9195d..9c3eb60b10 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -203,11 +203,17 @@ Провайдер запрашивает прохождение верификации Список токенов в вашем кошельке Получение наилучших курсов... + Плавающая ставка + К провайдеру + Пользуясь сервисом вы соглашаетесь с %s + Пользуясь сервисом вы соглашаетесь с %1$s и %2$s + Политикой конфиденциальности Провайдер Лучший курс Доступно с %s Недоступно для этой пары Требуется разрешение + Условиями использования Информация ниже не является обязательной. Вы можете стереть её, если хотите. Расскажите, каких функций вам не хватает, и мы постараемся вам помочь. Скажите, пожалуйста, какая у вас карта? diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 3088fc97ea..b81d198635 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -224,11 +224,15 @@ Fetching best rates... Floating rate Go to provider + By using swap functionality, you agree with provider’s %s + By using swap functionality, you agree with provider’s %1$s and %2$s + Privacy Policy Provider Best rate Available from %s Unavailable for this pair Permission Required + Terms of Use 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. Please tell us what card do you have diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt index 4ea0faf9cd..5bb5079f95 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/SwapPairInfoConverter.kt @@ -43,6 +43,8 @@ class SwapPairInfoConverter : Converter Unit, val onBackClicked: () -> Unit, @@ -106,6 +107,17 @@ sealed interface TransactionCardType { ) : TransactionCardType } +data class TosState( + val tosLink: LegalState?, + val policyLink: LegalState?, +) + +data class LegalState( + val title: TextReference, + val link: String, + val onClick: (String) -> Unit, +) + sealed interface SwapWarning { data class PermissionNeeded(val notificationConfig: NotificationConfig) : SwapWarning object InsufficientFunds : SwapWarning 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 48a3d55f55..258039517e 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 @@ -21,4 +21,6 @@ data class UiActions( val onProviderClick: (String) -> Unit, val onProviderSelect: (String) -> Unit, val onBuyClick: () -> Unit, + val onPolicyClick: (String) -> Unit, + val onTosClick: (String) -> Unit, ) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt new file mode 100644 index 0000000000..ee2bce3961 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/WebViewBottomSheetConfig.kt @@ -0,0 +1,5 @@ +package com.tangem.feature.swap.models.states + +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent + +internal data class WebViewBottomSheetConfig(val url: String) : TangemBottomSheetConfigContent \ 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 e66df2962d..89a2f5343e 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 @@ -271,6 +271,26 @@ internal class StateBuilder( selectionType = ProviderState.SelectionType.CLICK, onProviderClick = actions.onProviderClick, ), + tosState = createTosState(swapProvider), + ) + } + + private fun createTosState(swapProvider: SwapProvider): TosState { + return TosState( + tosLink = swapProvider.termsOfUse?.let { + LegalState( + title = resourceReference(R.string.express_terms_of_use), + link = it, + onClick = actions.onTosClick, + ) + }, + policyLink = swapProvider.privacyPolicy?.let { + LegalState( + title = resourceReference(R.string.express_privacy_policy), + link = it, + onClick = actions.onPolicyClick, + ) + }, ) } @@ -422,6 +442,7 @@ internal class StateBuilder( ChangeCardsButtonState.DISABLED }, providerState = providerState, + tosState = createTosState(swapProvider), ) } @@ -804,6 +825,17 @@ internal class StateBuilder( } } + fun showWebViewBottomSheet(uiState: SwapStateHolder, url: String, onDismiss: () -> Unit): SwapStateHolder { + val config = WebViewBottomSheetConfig(url = url) + return uiState.copy( + bottomSheetConfig = TangemBottomSheetConfig( + isShow = true, + onDismissRequest = onDismiss, + content = config, + ), + ) + } + fun showPermissionBottomSheet(uiState: SwapStateHolder, onDismiss: () -> Unit): SwapStateHolder { val permissionState = uiState.permissionState if (permissionState is SwapPermissionState.ReadyForRequest) { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index c25ea64c0b..cc39867e7d 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -3,8 +3,7 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material.* -import androidx.compose.material3.* +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource @@ -14,6 +13,7 @@ import com.tangem.feature.swap.models.SwapStateHolder import com.tangem.feature.swap.models.states.ChooseFeeBottomSheetConfig import com.tangem.feature.swap.models.states.ChooseProviderBottomSheetConfig import com.tangem.feature.swap.models.states.GivePermissionBottomSheetConfig +import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig import com.tangem.feature.swap.presentation.R @Composable @@ -48,6 +48,9 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { is ChooseFeeBottomSheetConfig -> { ChooseFeeBottomSheet(config = config) } + is WebViewBottomSheetConfig -> { + WebViewBottomSheet(config = config) + } } } } 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 360cd4e016..ba5cc5a853 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 @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.ClickableText import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -11,8 +12,12 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.constraintlayout.compose.ConstraintLayout import com.tangem.common.Strings.STARS @@ -72,6 +77,15 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi } } + state.tosState?.let { + ProviderTos( + tosState = it, + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = TangemTheme.dimens.spacing16), + ) + } + AnimatedVisibility( visible = keyboard is Keyboard.Opened, modifier = Modifier @@ -185,6 +199,84 @@ private fun TransactionCardData( } } +@Composable +private fun ProviderTos(tosState: TosState, modifier: Modifier = Modifier) { + val tos = tosState.tosLink + val policy = tosState.policyLink + if (tos == null && policy == null) return + + val (annotatedString, click) = getAnnotatedStringForLegalsWithClick(tos, policy) + + ClickableText( + text = annotatedString, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = TangemTheme.dimens.spacing54), + style = TangemTheme.typography.caption2.copy(textAlign = TextAlign.Center), + onClick = click, + ) +} + +@Composable +private fun getAnnotatedStringForLegalsWithClick( + tos: LegalState?, + policy: LegalState?, +): Pair Unit> { + return if (tos != null && policy != null) { + val tosTitle = tos.title.resolveReference() + val policyTitle = policy.title.resolveReference() + val fullString = stringResource(id = R.string.express_legal_two_placeholders, tosTitle, policyTitle) + val tosIndex = fullString.indexOf(tosTitle) + val policyIndex = fullString.indexOf(policyTitle) + val string = buildAnnotatedString { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(fullString.substring(0, tosIndex)) + } + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + append(fullString.substring(tosIndex, tosIndex + tosTitle.length)) + } + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(fullString.substring(tosIndex + tosTitle.length, policyIndex)) + } + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + append(fullString.substring(policyIndex, policyIndex + policyTitle.length)) + } + } + val click = { i: Int -> + val tosStyle = requireNotNull(string.spanStyles.getOrNull(1)) + if (i in tosStyle.start..tosStyle.end) { + tos.onClick(tos.link) + } + val policyStyle = requireNotNull(string.spanStyles.lastOrNull()) + if (i in policyStyle.start..policyStyle.end) { + policy.onClick(policy.link) + } + } + string to click + } else { + val legal = requireNotNull(tos ?: policy) { "tos or policy must not be null" } + val legalTitle = legal.title + .resolveReference() + val fullString = stringResource(id = R.string.express_legal_one_placeholder, legal) + val legalIndex = fullString.indexOf(legalTitle) + val string = buildAnnotatedString { + withStyle(SpanStyle(color = TangemTheme.colors.text.tertiary)) { + append(fullString.substring(0, legalIndex)) + } + withStyle(SpanStyle(color = TangemTheme.colors.text.accent)) { + append(fullString.substring(legalIndex, legalIndex + legalTitle.length)) + } + } + val click = { i: Int -> + val legalStyle = requireNotNull(string.spanStyles.lastOrNull()) + if (i in legalStyle.start..legalStyle.end) { + legal.onClick(legal.link) + } + } + string to click + } +} + @OptIn(ExperimentalMaterialApi::class) @Composable private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { @@ -407,6 +499,18 @@ private val state = SwapStateHolder( permissionState = SwapPermissionState.InProgress, blockchainId = "POLYGON", providerState = ProviderState.Loading(), + tosState = TosState( + tosLink = LegalState( + title = stringReference("Tangem"), + link = "https://tangem.com", + onClick = {}, + ), + policyLink = LegalState( + title = stringReference("Tangem"), + link = "https://tangem.com", + onClick = {}, + ), + ), ) @Preview diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt new file mode 100644 index 0000000000..4a068cddbd --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt @@ -0,0 +1,79 @@ +package com.tangem.feature.swap.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.tooling.preview.Preview +import com.google.accompanist.web.WebView +import com.google.accompanist.web.rememberWebViewState +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheet +import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig + +@Composable +fun WebViewBottomSheet(config: TangemBottomSheetConfig) { + TangemBottomSheet( + config = config, + contentColor = TangemTheme.colors.background.tertiary, + ) { content: WebViewBottomSheetConfig -> + WebViewBottomSheetContent( + WebViewBottomSheetConfig(url = content.url), + ) + } +} + +@Composable +private fun WebViewBottomSheetContent(content: WebViewBottomSheetConfig) { + val state = rememberWebViewState(content.url) + val isInPreviewMode = LocalInspectionMode.current + Column( + modifier = Modifier + .fillMaxHeight() + .verticalScroll(rememberScrollState()), + ) { + WebView( + state = state, + modifier = Modifier + .background(TangemTheme.colors.background.secondary), + captureBackPresses = false, + onCreated = { + if (!isInPreviewMode) { + it.settings.apply { + javaScriptEnabled = false + allowFileAccess = false + } + } + }, + ) + } +} + +@Preview +@Composable +private fun Preview_WebViewBottomSheetContent_InLightTheme() { + TangemTheme(isDark = false) { + WebViewBottomSheetContent( + content = WebViewBottomSheetConfig( + url = "https://tangem.com/en/", + ), + ) + } +} + +@Preview +@Composable +private fun Preview_WebViewBottomSheetContent_InDarkTheme() { + TangemTheme(isDark = true) { + WebViewBottomSheetContent( + content = WebViewBottomSheetConfig( + url = "https://tangem.com/en/", + ), + ) + } +} \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index b2c3060ae4..399f3b6f79 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 @@ -22,8 +22,8 @@ import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.domain.PermissionOptions import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.feature.swap.domain.models.domain.SwapProvider -import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.domain.models.formatToUIRepresentation +import com.tangem.feature.swap.domain.models.ui.* import com.tangem.feature.swap.models.* import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen @@ -845,6 +845,16 @@ internal class SwapViewModel @Inject constructor( onRetryClick = { startLoadingQuotesFromLastState() }, + onPolicyClick = { + uiState = stateBuilder.showWebViewBottomSheet(uiState, it) { + uiState = stateBuilder.dismissBottomSheet(uiState) + } + }, + onTosClick = { + uiState = stateBuilder.showWebViewBottomSheet(uiState, it) { + uiState = stateBuilder.dismissBottomSheet(uiState) + } + }, ) } From 97afda563f7b87d44dd318483372971aceabb67a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Dec 2023 17:39:29 +0300 Subject: [PATCH 038/102] Updated on 2026-08-14 --- .../tokendetails/viewmodels/TokenDetailsViewModel.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index ef5cb340ad..bafa950c0c 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -219,6 +219,8 @@ internal class TokenDetailsViewModel @Inject constructor( viewModelScope.launch(dispatchers.io) { swapTxStatusTaskScheduler.cancelTask() exchangeStatusFactory.invoke() + .distinctUntilChanged() + .filterNot { it.isEmpty() } .onEach { swapTxs -> swapTxStatusTaskScheduler.scheduleTask( viewModelScope, @@ -230,13 +232,14 @@ internal class TokenDetailsViewModel @Inject constructor( } }, onSuccess = { updatedTxs -> - val config = uiState.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig - val currentTx = updatedTxs.firstOrNull { it.txId == config?.value?.txId } + val config = uiState.bottomSheetConfig + val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig + val currentTx = updatedTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId } uiState = uiState.copy( swapTxs = updatedTxs, bottomSheetConfig = currentTx?.let( stateFactory::updateStateWithExchangeStatusBottomSheet, - ), + ) ?: config, ) }, onError = {}, From 424dd37a238a60ac31f15f5d2c3090ee783289fe Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Dec 2023 17:39:46 +0300 Subject: [PATCH 039/102] Updated on 2026-08-14 --- .../tokens/GetCurrencyWarningsUseCase.kt | 106 +++++++++--------- .../tangem/feature/swap/SwapRepositoryImpl.kt | 49 +++++++- .../feature/swap/domain/api/SwapRepository.kt | 3 + 3 files changed, 105 insertions(+), 53 deletions(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 849e3933a5..ae959e251d 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -1,6 +1,5 @@ package com.tangem.domain.tokens -import arrow.core.Either import com.tangem.domain.settings.ShouldShowSwapPromoTokenUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus @@ -54,18 +53,16 @@ class GetCurrencyWarningsUseCase( derivationPath = derivationPath, isSingleWalletWithTokens = isSingleWalletWithTokens, ), - operations.getCurrenciesStatusesFlow().conflate(), flowOf(walletManagersFacade.getRentInfo(userWalletId, currency.network)), flowOf(walletManagersFacade.getExistentialDeposit(userWalletId, currency.network)), - showSwapPromoTokenUseCase(userWalletId.stringValue, currency.id.value).conflate(), - ) { coinRelatedWarnings, cryptoStatuses, maybeRentWarning, maybeEdWarning, shouldShowSwapPromo -> + getSwapPromoNotificationWarning( + operations = operations, + userWalletId = userWalletId, + currencyStatus = currencyStatus, + ).conflate(), + ) { coinRelatedWarnings, maybeRentWarning, maybeEdWarning, maybeSwapPromo -> setOfNotNull( - getSwapPromoNotificationWarning( - shouldShowSwapPromo = shouldShowSwapPromo, - userWalletId = userWalletId, - currencyStatus = currencyStatus, - cryptoStatuses = cryptoStatuses, - ), + maybeSwapPromo, maybeRentWarning, maybeEdWarning?.let { CryptoCurrencyWarning.ExistentialDeposit( @@ -81,54 +78,59 @@ class GetCurrencyWarningsUseCase( } private suspend fun getSwapPromoNotificationWarning( - shouldShowSwapPromo: Boolean, + operations: CurrenciesStatusesOperations, userWalletId: UserWalletId, currencyStatus: CryptoCurrencyStatus, - cryptoStatuses: Either>, - ): CryptoCurrencyWarning? { + ): Flow { val currency = currencyStatus.currency - return if (shouldShowSwapPromo && marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)) { - cryptoStatuses.fold( - ifLeft = { null }, - ifRight = { cryptoCurrencyStatuses -> - val pairs = runCatching(dispatchers.io) { - swapRepository.getPairs( - LeastTokenInfo( - contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", - network = currency.network.backendId, - ), - cryptoCurrencyStatuses.map { it.currency }, - ) - }.getOrNull()?.pairs ?: emptyList() + val cryptoStatuses = operations.getCurrenciesStatusesSync() + return combine( + showSwapPromoTokenUseCase(userWalletId.stringValue, currency.id.value).conflate(), + flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(), + ) { shouldShowSwapPromo, isExchangeable -> + if (shouldShowSwapPromo && isExchangeable) { + cryptoStatuses.fold( + ifLeft = { null }, + ifRight = { cryptoCurrencyStatuses -> + val pairs = runCatching(dispatchers.io) { + swapRepository.getPairsOnly( + LeastTokenInfo( + contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress ?: "0", + network = currency.network.backendId, + ), + cryptoCurrencyStatuses.map { it.currency }, + ) + }.getOrNull()?.pairs ?: emptyList() - val filteredCurrencies = cryptoCurrencyStatuses.filterNot { - it.currency.id == currency.id - } - val currencyPairs = pairs.filter { - it.from.network == currency.network.backendId || - it.to.network == currency.network.backendId - } - val isExchangeable = currencyPairs.any { pair -> - val availablePair = if (currencyStatus.value.amount.isNullOrZero()) { - filteredCurrencies.filterNot { it.value.amount.isNullOrZero() } - } else { - filteredCurrencies + val filteredCurrencies = cryptoCurrencyStatuses.filterNot { + it.currency.id == currency.id } - availablePair - .any { - it.currency.network.backendId == pair.to.network || - it.currency.network.backendId == pair.from.network + val currencyPairs = pairs.filter { + it.from.network == currency.network.backendId || + it.to.network == currency.network.backendId + } + val showPromo = currencyPairs.any { pair -> + val availablePair = if (currencyStatus.value.amount.isNullOrZero()) { + filteredCurrencies.filterNot { it.value.amount.isNullOrZero() } + } else { + filteredCurrencies } - } - if (isExchangeable) { - CryptoCurrencyWarning.SwapPromo - } else { - null - } - }, - ) - } else { - null + availablePair + .any { + it.currency.network.backendId == pair.to.network || + it.currency.network.backendId == pair.from.network + } + } + if (showPromo) { + CryptoCurrencyWarning.SwapPromo + } else { + null + } + }, + ) + } else { + null + } } } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index ffd12827ba..62c8ebb0b6 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -41,7 +41,7 @@ import java.math.BigDecimal import javax.inject.Inject import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo -@Suppress("LongParameterList") +@Suppress("LongParameterList", "LargeClass") internal class SwapRepositoryImpl @Inject constructor( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, @@ -109,6 +109,53 @@ internal class SwapRepositoryImpl @Inject constructor( } } + override suspend fun getPairsOnly( + initialCurrency: LeastTokenInfo, + currencyList: List, + ): PairsWithProviders { + return withContext(coroutineDispatcher.io) { + try { + val initial = NetworkLeastTokenInfo( + contractAddress = initialCurrency.contractAddress, + network = initialCurrency.network, + ) + val currenciesList = currencyList.map { leastTokenInfoConverter.convert(it) } + + val pairsDeferred = async { + getPairsInternal( + from = arrayListOf(initial), + to = currenciesList, + ) + } + + val reversedPairsDeferred = async { + getPairsInternal( + from = currenciesList, + to = arrayListOf(initial), + ) + } + + val pairs = pairsDeferred.await().getOrThrow() + val reversedPairs = reversedPairsDeferred.await().getOrThrow() + + val allPairs = pairs + reversedPairs + + return@withContext swapPairInfoConverter.convert( + SwapPairsWithProviders( + swapPair = allPairs, + providers = emptyList(), + ), + ) + } catch (exception: Exception) { + if (exception is ApiResponseError.HttpException) { + throw ExpressException(errorsDataConverter.convert(exception.errorBody ?: "")) + } else { + throw exception + } + } + } + } + private suspend fun getPairsInternal( from: List, to: List, diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index ea82fe6495..713cbeeb9e 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -11,6 +11,9 @@ interface SwapRepository { suspend fun getPairs(initialCurrency: LeastTokenInfo, currencyList: List): PairsWithProviders + /** Express getPairs request variant without providers request */ + suspend fun getPairsOnly(initialCurrency: LeastTokenInfo, currencyList: List): PairsWithProviders + suspend fun getRates(currencyId: String, tokenIds: List): Map suspend fun getExchangeableTokens(networkId: String): List From ee388c5cb0f050e3ce8e7ac8b659363bf31a0439 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 15 Dec 2023 16:48:48 +0200 Subject: [PATCH 040/102] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 27 ++++++++++++++++--- .../tangem/feature/swap/ui/ProviderItem.kt | 4 +-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 9c3eb60b10..5705d3bd04 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -197,10 +197,27 @@ Статус Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами Выберите провайдера - Чтобы узнать причину, посетите сайт провайдера - Чтобы вернуть ваши деньги, посетите сайт провайдера + Курс обмена + Чтобы вернуть ваши деньги, посетите сайт провайдера + Операция не выполнена провайдером Посетите сайт провайдера для проверки Провайдер запрашивает прохождение верификации + Подтверждено + Подтверждение + Подтверждение... + Обменяно + Обмен + Обмен... + Неудачно + Депозит получен + Ожидание депозита + Ожидаем пополнения... + Возвращено + Отправляем + Отправка средств... + Отправлено + Верифицировано + Требуется верификация Список токенов в вашем кошельке Получение наилучших курсов... Плавающая ставка @@ -521,6 +538,8 @@ Ваш кошелек Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s Безлимитно + Вы отправляете + Вы получите Открыть в обозревателе В процессе Обменять @@ -547,9 +566,9 @@ Токен %1$s является основной валютой в сети %2$s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети. Невозможно скрыть %s Нет цены + Обменять Обменяйте этот токен на другие активы в вашем портфеле Представляем кроссчейн-своп - Обменять контракт: %s У вас еще нет транзакций Не удалось загрузить историю транзакций.\nНажмите на кнопку перезагрузки, чтобы обновить информацию. @@ -662,6 +681,8 @@ У вас есть активная транзакция У вас в списке нет монет доступных для обмена с %s Нет доступных токенов для обмена + Чтобы совершить транзакцию, вам необходимо внести немного %1$s %2$s + Невозможно покрыть комиссию %s Cервис временно недоступен Пожалуйста, измените сумму для обмена Сумма для обмена должна быть не менее %s 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 41adc2a361..4a217bcbfe 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 @@ -247,7 +247,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { Box(modifier = modifier.fillMaxWidth()) { Column { Text( - text = "Provider", + text = stringResource(R.string.express_provider), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing12), @@ -265,7 +265,7 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { strokeWidth = TangemTheme.dimens.size2, ) Text( - text = "Fetching best rates ...", + text = stringResource(R.string.express_fetch_best_rates), style = TangemTheme.typography.body2, color = TangemTheme.colors.text.tertiary, modifier = Modifier.padding(start = TangemTheme.dimens.spacing4), From 7d087423e98df85aa9b744b3fb6fd01b6e86754f Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 12:02:54 +0300 Subject: [PATCH 041/102] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractor.kt | 7 --- .../feature/swap/domain/SwapInteractorImpl.kt | 45 ++++++++++--------- .../swap/viewmodels/SwapProcessDataState.kt | 1 - .../feature/swap/viewmodels/SwapViewModel.kt | 30 ++++++------- 4 files changed, 39 insertions(+), 44 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt index 765647a01e..d3ec1058b9 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractor.kt @@ -2,7 +2,6 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount @@ -15,8 +14,6 @@ interface SwapInteractor { suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress - fun initDerivationPathAndNetwork(derivationPath: String?, network: Network) - /** * Gives permission to swap, this starts scan card process * @@ -30,7 +27,6 @@ interface SwapInteractor { * Find best quote for given tokens to swap * under the hood calls different methods to receive data, depends on permission for given token * - * @param networkId network for tokens * @param fromToken [Currency] from which want to swap * @param toToken [Currency] that receive after swap * @param amountToSwap amount you want to swap @@ -39,7 +35,6 @@ interface SwapInteractor { */ @Throws(IllegalStateException::class) suspend fun findBestQuote( - networkId: String, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, providers: List, @@ -62,7 +57,6 @@ interface SwapInteractor { @Throws(IllegalStateException::class) suspend fun onSwap( swapProvider: SwapProvider, - networkId: String, swapData: SwapDataModel?, currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, @@ -76,7 +70,6 @@ interface SwapInteractor { selectedFee: FeeType, fromToken: CryptoCurrencyStatus, amountToSwap: String, - networkId: String, ): SwapState.QuotesLoadedState /** diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 631d3e6d51..443fa9f71e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -62,7 +62,6 @@ internal class SwapInteractorImpl @Inject constructor( private val swapCurrencyConverter = SwapCurrencyConverter() private val amountFormatter = AmountFormatter() - private var derivationPath: String? = null private var network: Network? = null override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { @@ -182,14 +181,9 @@ internal class SwapInteractorImpl @Inject constructor( return repository.getPairs(initialCurrency, currenciesList) } - @Deprecated("used in old swap mechanism") - override fun initDerivationPathAndNetwork(derivationPath: String?, network: Network) { - this.derivationPath = derivationPath - this.network = network - } - @Deprecated("used in old swap mechanism") override suspend fun givePermissionToSwap(networkId: String, permissionOptions: PermissionOptions): TxState { + val derivationPath = permissionOptions.fromToken.network.derivationPath.value val dataToSign = if (permissionOptions.approveType == SwapApproveType.UNLIMITED) { getApproveData( networkId = networkId, @@ -233,7 +227,6 @@ internal class SwapInteractorImpl @Inject constructor( @Deprecated("used in old swap mechanism") override suspend fun findBestQuote( - networkId: String, fromToken: CryptoCurrencyStatus, toToken: CryptoCurrencyStatus, providers: List, @@ -249,7 +242,7 @@ internal class SwapInteractorImpl @Inject constructor( } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) val isBalanceWithoutFeeEnough = isBalanceEnough(fromToken, amount, null) - + val networkId = fromToken.currency.network.backendId when (provider.type) { ExchangeProviderType.DEX -> { manageDex( @@ -310,7 +303,10 @@ internal class SwapInteractorImpl @Inject constructor( if (isAllowedToSpend && allowPermissionsHandler.isAddressAllowanceInProgress(fromTokenAddress)) { allowPermissionsHandler.removeAddressFromProgress(fromTokenAddress) - transactionManager.updateWalletManager(networkId, derivationPath) + transactionManager.updateWalletManager( + networkId, + fromToken.currency.network.derivationPath.value, + ) } return if (isAllowedToSpend && isBalanceWithoutFeeEnough) { provider to loadDexSwapData( @@ -361,7 +357,6 @@ internal class SwapInteractorImpl @Inject constructor( override suspend fun onSwap( swapProvider: SwapProvider, - networkId: String, swapData: SwapDataModel?, currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, @@ -389,7 +384,7 @@ internal class SwapInteractorImpl @Inject constructor( } ExchangeProviderType.DEX -> { onSwapDex( - networkId = networkId, + networkId = currencyToSend.currency.network.backendId, swapData = requireNotNull(swapData), currencyToSend = currencyToSend.currency, currencyToGet = currencyToGet.currency, @@ -405,7 +400,6 @@ internal class SwapInteractorImpl @Inject constructor( selectedFee: FeeType, fromToken: CryptoCurrencyStatus, amountToSwap: String, - networkId: String, ): SwapState.QuotesLoadedState { val amountDecimal = toBigDecimalOrNull(amountToSwap) if (amountDecimal == null || amountDecimal.signum() == 0) { @@ -413,7 +407,7 @@ internal class SwapInteractorImpl @Inject constructor( } val amount = SwapAmount(amountDecimal, getTokenDecimals(fromToken.currency)) val includeFeeInAmount = getIncludeFeeInAmount( - networkId = networkId, + networkId = fromToken.currency.network.backendId, txFee = state.txFee, amount = amount, fromToken = fromToken.currency, @@ -422,6 +416,7 @@ internal class SwapInteractorImpl @Inject constructor( return state.copy( permissionState = PermissionDataState.Empty, preparedSwapConfigState = state.preparedSwapConfigState.copy( + isFeeEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, isBalanceEnough = includeFeeInAmount !is IncludeFeeInAmount.BalanceNotEnough, includeFeeInAmount = includeFeeInAmount, ), @@ -438,6 +433,7 @@ internal class SwapInteractorImpl @Inject constructor( ): TxState { val amountDecimal = requireNotNull(toBigDecimalOrNull(amountToSwap)) { "wrong amount format" } val amount = SwapAmount(amountDecimal, getTokenDecimals(currencyToSend)) + val derivationPath = currencyToSend.network.derivationPath.value val result = transactionManager.sendTransaction( txData = SwapTxData( networkId = networkId, @@ -481,6 +477,7 @@ internal class SwapInteractorImpl @Inject constructor( } } + @Suppress("LongMethod") private suspend fun onSwapCex( currencyToSend: CryptoCurrencyStatus, currencyToGet: CryptoCurrencyStatus, @@ -519,6 +516,7 @@ internal class SwapInteractorImpl @Inject constructor( val externalUrl = (exchangeData.transaction as? ExpressTransactionModel.CEX)?.externalTxUrl + val derivationPath = currencyToSend.currency.network.derivationPath.value return result.fold( ifLeft = { when (it) { @@ -659,7 +657,7 @@ internal class SwapInteractorImpl @Inject constructor( val allowance = repository.getAllowance( userWalletId = userWallet.walletId, networkId = networkId, - derivationPath = derivationPath, + derivationPath = fromToken.network.derivationPath.value, tokenDecimalCount = getTokenDecimals(fromToken), tokenAddress = getTokenAddress(fromToken), spenderAddress = spenderAddress, @@ -832,7 +830,10 @@ internal class SwapInteractorImpl @Inject constructor( } val tokenForFeeBalance = - userWalletManager.getNativeTokenBalance(networkId, derivationPath) ?: ProxyAmount.empty() + userWalletManager.getNativeTokenBalance( + networkId, + fromToken.network.derivationPath.value, + ) ?: ProxyAmount.empty() if (amount.value > tokenForFeeBalance.value) { return IncludeFeeInAmount.BalanceNotEnough @@ -907,7 +908,7 @@ internal class SwapInteractorImpl @Inject constructor( destinationAddress = swapData.transaction.txTo, increaseBy = INCREASE_GAS_LIMIT_BY, data = (swapData.transaction as ExpressTransactionModel.DEX).txData, - derivationPath = derivationPath, + derivationPath = fromToken.currency.network.derivationPath.value, ) val txFeeState = when (feeData) { is ProxyFees.MultipleFees -> feeData.proxyFeesToFeeState(networkId) @@ -1048,6 +1049,7 @@ internal class SwapInteractorImpl @Inject constructor( permissionState = PermissionDataState.PermissionLoading, ) } + val derivationPath = fromToken.network.derivationPath.value // setting up amount for approve with given amount for swap [SwapApproveType.Limited] val transactionData = getApproveData( networkId = networkId, @@ -1091,7 +1093,7 @@ internal class SwapInteractorImpl @Inject constructor( permissionState = PermissionDataState.PermissionReadyForRequest( currency = fromToken.symbol, amount = INFINITY_SYMBOL, - walletAddress = getWalletAddress(networkId), + walletAddress = getWalletAddress(networkId, derivationPath), spenderAddress = getTokenAddress(fromToken), requestApproveData = RequestApproveStateData( fee = feeState, @@ -1262,7 +1264,7 @@ internal class SwapInteractorImpl @Inject constructor( } } - private suspend fun getWalletAddress(networkId: String): String { + private suspend fun getWalletAddress(networkId: String, derivationPath: String?): String { return userWalletManager.getWalletAddress(networkId, derivationPath) } @@ -1286,7 +1288,10 @@ internal class SwapInteractorImpl @Inject constructor( if (fee == null) { return false } - val nativeTokenBalance = userWalletManager.getNativeTokenBalance(networkId, derivationPath) + val nativeTokenBalance = userWalletManager.getNativeTokenBalance( + networkId, + fromToken.network.derivationPath.value, + ) val percentsToFeeIncrease = BigDecimal.ONE return when (fromToken) { is CryptoCurrency.Coin -> { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt index 7b5bd446fa..7623970edf 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapProcessDataState.kt @@ -11,7 +11,6 @@ import com.tangem.feature.swap.models.ApproveType data class SwapProcessDataState( // Initial network id - val networkId: String, val fromCryptoCurrency: CryptoCurrencyStatus? = null, val toCryptoCurrency: CryptoCurrencyStatus? = null, // Amount from input 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 399f3b6f79..b1438e8115 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 @@ -79,7 +79,7 @@ internal class SwapViewModel @Inject constructor( private val amountDebouncer = Debouncer() private val singleTaskScheduler = SingleTaskScheduler>() - private var dataState by mutableStateOf(SwapProcessDataState(networkId = initialCryptoCurrency.network.backendId)) + private var dataState by mutableStateOf(SwapProcessDataState()) var uiState: SwapStateHolder by mutableStateOf( stateBuilder.createInitialLoadingState( @@ -103,11 +103,6 @@ internal class SwapViewModel @Inject constructor( requireNotNull(getCryptoCurrencyStatusUseCase(it.walletId, initialCryptoCurrency.id).getOrNull()) { "Failed to get initial crypto currency status" } - - swapInteractor.initDerivationPathAndNetwork( - derivationPath = initialCryptoCurrency.network.derivationPath.value, - network = initialCryptoCurrency.network, - ) initTokens() } } @@ -278,7 +273,6 @@ internal class SwapViewModel @Inject constructor( approveDataModel = null, ) swapInteractor.findBestQuote( - networkId = dataState.networkId, fromToken = fromToken, toToken = toToken, providers = toProvidersList, @@ -438,13 +432,13 @@ internal class SwapViewModel @Inject constructor( Timber.e("Last loaded quotes state is null") return } + val fromCurrency = requireNotNull(dataState.fromCryptoCurrency) viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { swapInteractor.onSwap( swapProvider = provider, - networkId = dataState.networkId, swapData = dataState.swapDataModel, - currencyToSend = requireNotNull(dataState.fromCryptoCurrency), + currencyToSend = fromCurrency, currencyToGet = requireNotNull(dataState.toCryptoCurrency), amountToSwap = requireNotNull(dataState.amount), includeFeeInAmount = lastLoadedQuotesState.preparedSwapConfigState.includeFeeInAmount, @@ -454,7 +448,7 @@ internal class SwapViewModel @Inject constructor( when (it) { is TxState.TxSent -> { val url = blockchainInteractor.getExplorerTransactionLink( - networkId = dataState.networkId, + networkId = fromCurrency.currency.network.backendId, txAddress = it.txAddress, ) uiState = stateBuilder.createSuccessState( @@ -528,17 +522,18 @@ internal class SwapViewModel @Inject constructor( is TxFeeState.SingleFeeState -> fee.fee null -> error("Fee should not be null") } + val fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { + "dataState.fromCurrency might not be null" + } swapInteractor.givePermissionToSwap( - networkId = dataState.networkId, + networkId = fromToken.network.backendId, permissionOptions = PermissionOptions( approveData = requireNotNull(dataState.approveDataModel) { "dataState.approveDataModel might not be null" }, forTokenContractAddress = (dataState.fromCryptoCurrency?.currency as? CryptoCurrency.Token) ?.contractAddress ?: "", - fromToken = requireNotNull(dataState.fromCryptoCurrency?.currency) { - "dataState.fromCurrency might not be null" - }, + fromToken = fromToken, approveType = requireNotNull(dataState.approveType) { "uiState.permissionState should not be null" }.toDomainApproveType(), @@ -805,7 +800,6 @@ internal class SwapViewModel @Inject constructor( selectedFee = it.feeType, fromToken = fromToken, amountToSwap = amountToSwap, - networkId = dataState.networkId, ) setupLoadedState(selectedProvider, updatedState, fromToken) } @@ -839,7 +833,11 @@ internal class SwapViewModel @Inject constructor( }, onBuyClick = { swapInteractor.getSelectedWallet()?.let { - swapRouter.openTokenDetails(it.walletId, swapInteractor.getNativeToken(dataState.networkId)) + val fromToken = dataState.fromCryptoCurrency ?: return@let + swapRouter.openTokenDetails( + it.walletId, + swapInteractor.getNativeToken(fromToken.currency.network.backendId), + ) } }, onRetryClick = { From 910c869cffe6b4b9d8e92df261d366cd8e3b528c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 12:06:31 +0300 Subject: [PATCH 042/102] Updated on 2026-08-14 --- .../tangem/feature/swap/ui/SwapScreenContent.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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 ba5cc5a853..2b2ebd577e 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 @@ -74,16 +74,15 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) MainButton(state = state, onPermissionWarningClick = state.onShowPermissionBottomSheet) - } - } - state.tosState?.let { - ProviderTos( - tosState = it, - modifier = Modifier - .align(Alignment.BottomCenter) - .padding(bottom = TangemTheme.dimens.spacing16), - ) + state.tosState?.let { + ProviderTos( + tosState = it, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing16), + ) + } + } } AnimatedVisibility( From c0b13b5a821cb68f295db272c4f9bad35d10c62d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 12:07:31 +0300 Subject: [PATCH 043/102] Updated on 2026-08-14 --- app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt | 5 +++++ .../main/java/com/tangem/domain/redux/ReduxStateHolder.kt | 2 ++ .../tokendetails/viewmodels/TokenDetailsViewModel.kt | 4 ++-- .../presentation/wallet/viewmodels/WalletViewModel.kt | 6 +++--- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt index c1262e0e1f..953ee95b85 100644 --- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt +++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt @@ -11,6 +11,7 @@ import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.UserWallet import com.tangem.tap.common.entities.FiatCurrency import com.tangem.tap.common.extensions.dispatchOnMain +import com.tangem.tap.common.extensions.dispatchWithMain import com.tangem.tap.common.extensions.onUserWalletSelected import com.tangem.tap.common.redux.AppState import com.tangem.tap.domain.TangemSdkManager @@ -69,6 +70,10 @@ class AppStateHolder @Inject constructor() : WalletsStateHolder, ReduxNavControl mainStore?.dispatch(action) } + override suspend fun dispatchWithMain(action: Action) { + mainStore?.dispatchWithMain(action) + } + override suspend fun onUserWalletSelected(userWallet: UserWallet) { mainStore?.onUserWalletSelected(userWallet) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt index 61170a1927..58c0bb0dde 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/redux/ReduxStateHolder.kt @@ -7,5 +7,7 @@ interface ReduxStateHolder { fun dispatch(action: Action) + suspend fun dispatchWithMain(action: Action) + suspend fun onUserWalletSelected(userWallet: UserWallet) } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index bafa950c0c..06039c3878 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -333,7 +333,7 @@ internal class TokenDetailsViewModel @Inject constructor( val cryptoCurrencyStatus = cryptoCurrencyStatus ?: return - viewModelScope.launch(dispatchers.io) { + viewModelScope.launch(dispatchers.main) { when (val currency = cryptoCurrencyStatus.currency) { is CryptoCurrency.Coin -> { reduxStateHolder.dispatch( @@ -363,7 +363,7 @@ internal class TokenDetailsViewModel @Inject constructor( .distinctUntilChanged() .firstOrNull() - reduxStateHolder.dispatch( + reduxStateHolder.dispatchWithMain( action = TradeCryptoAction.New.SendToken( userWallet = wallet, tokenCurrency = tokenCurrency, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt index 7d3c8f1dab..0b3284437c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/WalletViewModel.kt @@ -675,7 +675,7 @@ internal class WalletViewModel @Inject constructor( .collectLatest { it.onRight { coinStatus -> uiState = stateFactory.getStateWithClosedBottomSheet() - reduxStateHolder.dispatch( + reduxStateHolder.dispatchWithMain( action = TradeCryptoAction.New.SendToken( userWallet = userWallet, tokenCurrency = requireNotNull(cryptoCurrencyStatus.currency as? CryptoCurrency.Token), @@ -1163,8 +1163,8 @@ internal class WalletViewModel @Inject constructor( ) } - private fun setupWalletConnectOnWallet(userWallet: UserWallet) { - reduxStateHolder.dispatch( + private suspend fun setupWalletConnectOnWallet(userWallet: UserWallet) { + reduxStateHolder.dispatchWithMain( action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet), ) } From 51bc76d3d10e425ef3147dc8ed0b2eacfd0a8417 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 11:16:47 +0200 Subject: [PATCH 044/102] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../swap/ui/ChooseProviderBottomSheet.kt | 26 ++++++++++++++++++- .../src/main/res/drawable/ic_lightning_16.xml | 10 +++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 features/swap/presentation/src/main/res/drawable/ic_lightning_16.xml diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 5705d3bd04..744452a5fa 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -198,6 +198,7 @@ Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами Выберите провайдера Курс обмена + Больше провайдеров на подходе.\nСледите за обновлениями! Чтобы вернуть ваши деньги, посетите сайт провайдера Операция не выполнена провайдером Посетите сайт провайдера для проверки diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index b81d198635..55f40ad98b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -196,6 +196,7 @@ Status Providers facilitate transactions, ensuring smooth and efficient token swaps Choose provider + More providers are coming soon.\nStay tuned! Estimated amount Exchange by %s Visit provider’s website to refund your money 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 363085e36c..0621d0a12a 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 @@ -4,11 +4,13 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon import androidx.compose.material.Text 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.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -55,7 +57,12 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC ) Column( modifier = Modifier - .padding(TangemTheme.dimens.spacing16) + .padding( + top = TangemTheme.dimens.spacing16, + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing14, + ) .background( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, @@ -76,6 +83,23 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC ) } } + Icon( + 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), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.icon.informative, + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing6, bottom = TangemTheme.dimens.spacing16) + .padding(horizontal = TangemTheme.dimens.spacing56) + .align(Alignment.CenterHorizontally), + textAlign = TextAlign.Center, + ) } } diff --git a/features/swap/presentation/src/main/res/drawable/ic_lightning_16.xml b/features/swap/presentation/src/main/res/drawable/ic_lightning_16.xml new file mode 100644 index 0000000000..f796778e0e --- /dev/null +++ b/features/swap/presentation/src/main/res/drawable/ic_lightning_16.xml @@ -0,0 +1,10 @@ + + + From 853123765307160636013f36fc0ffd07754eb13a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 11:31:24 +0200 Subject: [PATCH 045/102] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 443fa9f71e..b2a70e5799 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -79,7 +79,7 @@ internal class SwapInteractorImpl @Inject constructor( .filter { val currencyFilter = it.currency.network.backendId != currency.network.backendId || it.currency.getContractAddress() != currency.getContractAddress() - val statusFilter = it.value is CryptoCurrencyStatus.Loaded + val statusFilter = it.value is CryptoCurrencyStatus.Loaded || it.value is CryptoCurrencyStatus.NoAccount val notCustomTokenFilter = !it.currency.isCustom statusFilter && currencyFilter && notCustomTokenFilter } From 762121b9b5fdf466f70ac04f6a4a58e9796841bf Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 13:58:49 +0300 Subject: [PATCH 046/102] Updated on 2026-08-14 --- .../feature/swap/domain/models/ui/SwapState.kt | 16 +++++++++++++++- .../feature/swap/domain/SwapInteractorImpl.kt | 15 ++++++++++++--- .../feature/swap/models/SwapStateHolder.kt | 2 ++ .../com/tangem/feature/swap/ui/StateBuilder.kt | 16 +++++++++++++--- .../tangem/feature/swap/ui/SwapScreenContent.kt | 12 +++++++----- .../tangem/feature/swap/ui/TransactionCard.kt | 14 +++++++------- 6 files changed, 56 insertions(+), 19 deletions(-) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index d2788d35a4..97c7c85fb3 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -13,7 +13,7 @@ sealed interface SwapState { data class QuotesLoadedState( val fromTokenInfo: TokenSwapInfo, val toTokenInfo: TokenSwapInfo, - val priceImpact: Float, + val priceImpact: PriceImpact, val networkCurrency: String, val preparedSwapConfigState: PreparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = false, @@ -40,6 +40,20 @@ sealed interface SwapState { ) : SwapState } +sealed class PriceImpact { + + abstract val value: Float + + fun getIntPercentValue() = (value * HUNDRED_PERCENTS).toInt() + data class Empty(override val value: Float = 0f) : PriceImpact() + data class ValueWithNotify(override val value: Float) : PriceImpact() + data class Value(override val value: Float) : PriceImpact() + + companion object { + private const val HUNDRED_PERCENTS = 100 + } +} + sealed class PermissionDataState { data class PermissionReadyForRequest( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index b2a70e5799..2de0ce5464 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -772,6 +772,7 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount = quoteModel.toTokenAmount, swapData = null, txFeeState = txFee, + exchangeProviderType = exchangeProviderType, ) when (exchangeProviderType) { @@ -930,6 +931,7 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount = swapData.toTokenAmount, swapData = swapData, txFeeState = txFeeState, + exchangeProviderType = ExchangeProviderType.DEX, ) swapState.copy( permissionState = PermissionDataState.Empty, @@ -967,6 +969,7 @@ internal class SwapInteractorImpl @Inject constructor( toTokenAmount: SwapAmount, swapData: SwapDataModel?, txFeeState: TxFeeState, + exchangeProviderType: ExchangeProviderType, ): SwapState.QuotesLoadedState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency @@ -991,6 +994,7 @@ internal class SwapInteractorImpl @Inject constructor( fromRate = rates[fromToken.id]?.fiatRate?.toDouble() ?: 0.0, toTokenAmount = toTokenAmount.value, toRate = rates[toToken.id]?.fiatRate?.toDouble() ?: 0.0, + exchangeProviderType = exchangeProviderType, ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), swapDataModel = swapData, @@ -1316,10 +1320,15 @@ internal class SwapInteractorImpl @Inject constructor( fromRate: Double, toTokenAmount: BigDecimal, toRate: Double, - ): Float { - val toTokenFiatValue = toTokenAmount.multiply(toRate.toBigDecimal()) + exchangeProviderType: ExchangeProviderType, + ): PriceImpact { val fromTokenFiatValue = fromTokenAmount.multiply(fromRate.toBigDecimal()) - return (BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)).toFloat() + val toTokenFiatValue = toTokenAmount.multiply(toRate.toBigDecimal()) + val value = (BigDecimal.ONE - toTokenFiatValue.divide(fromTokenFiatValue, 2, RoundingMode.HALF_UP)).toFloat() + if (exchangeProviderType == ExchangeProviderType.CEX) { + return PriceImpact.Value(value) + } + return PriceImpact.ValueWithNotify(value) } private suspend fun getApproveData( 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 8b42445651..00b174011d 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 @@ -8,6 +8,7 @@ import com.tangem.core.ui.components.states.Item import com.tangem.core.ui.components.states.SelectableItemsState import com.tangem.core.ui.extensions.TextReference import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.domain.models.ui.TxFee import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState @@ -24,6 +25,7 @@ data class SwapStateHolder( val fee: FeeItemState = FeeItemState.Empty, val permissionState: SwapPermissionState = SwapPermissionState.Empty, + val priceImpact: PriceImpact, val successState: SwapSuccessStateHolder? = null, val selectTokenState: SwapSelectTokenStateHolder? = null, 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 89a2f5343e..c74bd436f7 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 @@ -87,6 +87,7 @@ internal class StateBuilder( changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, onShowPermissionBottomSheet = actions.openPermissionBottomSheet, providerState = ProviderState.Empty(), + priceImpact = PriceImpact.Empty(), ) } @@ -139,6 +140,7 @@ internal class StateBuilder( onClick = { }, ), changeCardsButtonState = ChangeCardsButtonState.DISABLED, + priceImpact = PriceImpact.Empty(), ) } @@ -187,6 +189,7 @@ internal class StateBuilder( providerState = ProviderState.Loading(), permissionState = uiStateHolder.permissionState, changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, + priceImpact = PriceImpact.Empty(), ) } @@ -271,6 +274,11 @@ internal class StateBuilder( selectionType = ProviderState.SelectionType.CLICK, onProviderClick = actions.onProviderClick, ), + priceImpact = if (quoteModel.priceImpact.value > PRICE_IMPACT_THRESHOLD) { + quoteModel.priceImpact + } else { + PriceImpact.Empty() + }, tosState = createTosState(swapProvider), ) } @@ -337,10 +345,11 @@ internal class StateBuilder( warnings.add(SwapWarning.InsufficientFunds) } - if (quoteModel.priceImpact > PRICE_IMPACT_THRESHOLD) { + val priceImpact = quoteModel.priceImpact + if (priceImpact is PriceImpact.ValueWithNotify && priceImpact.value > PRICE_IMPACT_THRESHOLD) { warnings.add( SwapWarning.HighPriceImpact( - priceImpact = (quoteModel.priceImpact * HUNDRED_PERCENTS).toInt(), + priceImpact = priceImpact.getIntPercentValue(), notificationConfig = highPriceImpactNotificationConfig(), ), ) @@ -442,6 +451,7 @@ internal class StateBuilder( ChangeCardsButtonState.DISABLED }, providerState = providerState, + priceImpact = PriceImpact.Empty(), tosState = createTosState(swapProvider), ) } @@ -553,6 +563,7 @@ internal class StateBuilder( ChangeCardsButtonState.DISABLED }, providerState = ProviderState.Empty(), + priceImpact = PriceImpact.Empty(), ) } @@ -1226,7 +1237,6 @@ internal class StateBuilder( const val ADDRESS_FIRST_PART_LENGTH = 7 const val ADDRESS_SECOND_PART_LENGTH = 4 private const val PRICE_IMPACT_THRESHOLD = 0.1 - private const val HUNDRED_PERCENTS = 100 private const val UNKNOWN_AMOUNT_SIGN = "—" private const val MAX_DECIMALS_TO_SHOW = 8 } 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 2b2ebd577e..43a6329ace 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 @@ -29,6 +29,7 @@ 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.domain.models.ui.FeeType +import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.* import com.tangem.feature.swap.models.states.FeeItemState import com.tangem.feature.swap.models.states.ProviderState @@ -127,9 +128,9 @@ private fun MainInfo(state: SwapStateHolder) { modifier = Modifier.fillMaxWidth(), ) { val (topCard, bottomCard, button) = createRefs() - val priceImpactWarning = state.warnings.filterIsInstance().firstOrNull() + val priceImpact = state.priceImpact TransactionCardData( - priceImpactWarning = priceImpactWarning, + priceImpact = priceImpact, swapCardState = state.sendCardData, modifier = Modifier.constrainAs(topCard) { top.linkTo(parent.top) @@ -138,7 +139,7 @@ private fun MainInfo(state: SwapStateHolder) { ) val marginCard = TangemTheme.dimens.spacing16 TransactionCardData( - priceImpactWarning = priceImpactWarning, + priceImpact = priceImpact, swapCardState = state.receiveCardData, modifier = Modifier.constrainAs(bottomCard) { top.linkTo(topCard.bottom, margin = marginCard) @@ -159,7 +160,7 @@ private fun MainInfo(state: SwapStateHolder) { @Composable private fun TransactionCardData( - priceImpactWarning: SwapWarning.HighPriceImpact?, + priceImpact: PriceImpact, swapCardState: SwapCardState, onSelectTokenClick: (() -> Unit)?, modifier: Modifier = Modifier, @@ -186,7 +187,7 @@ private fun TransactionCardData( amountEquivalent = swapCardState.amountEquivalent, tokenIconUrl = swapCardState.tokenIconUrl ?: "", tokenCurrency = swapCardState.tokenCurrency, - priceImpact = priceImpactWarning, + priceImpact = priceImpact, networkIconRes = if (swapCardState.isNotNativeToken) swapCardState.networkIconRes else null, iconPlaceholder = swapCardState.coinId?.let { getActiveIconResByCoinId(it) @@ -498,6 +499,7 @@ private val state = SwapStateHolder( permissionState = SwapPermissionState.InProgress, blockchainId = "POLYGON", providerState = ProviderState.Loading(), + priceImpact = PriceImpact.Empty(), tosState = TosState( tosLink = LegalState( title = stringReference("Tangem"), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index b16a5e4c21..e7b51283c2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -37,7 +37,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.* import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.utils.ImageBackgroundContrastChecker -import com.tangem.feature.swap.models.SwapWarning +import com.tangem.feature.swap.domain.models.ui.PriceImpact import com.tangem.feature.swap.models.TransactionCardType import kotlinx.coroutines.launch @@ -49,7 +49,7 @@ fun TransactionCard( tokenIconUrl: String, tokenCurrency: String, amountEquivalent: String?, - priceImpact: SwapWarning.HighPriceImpact?, + priceImpact: PriceImpact, textFieldValue: TextFieldValue?, modifier: Modifier = Modifier, @DrawableRes iconPlaceholder: Int? = null, @@ -138,7 +138,7 @@ fun TransactionCardEmpty( type = type, amountEquivalent = amountEquivalent, textFieldValue = textFieldValue, - priceImpact = null, + priceImpact = PriceImpact.Empty(), ) } @@ -221,7 +221,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie private fun Content( type: TransactionCardType, amountEquivalent: String?, - priceImpact: SwapWarning.HighPriceImpact?, + priceImpact: PriceImpact, textFieldValue: TextFieldValue?, ) { Row( @@ -274,10 +274,10 @@ private fun Content( SpacerH8() if (amountEquivalent != null) { - if (type is TransactionCardType.ReceiveCard && priceImpact != null) { + if (type is TransactionCardType.ReceiveCard && priceImpact !is PriceImpact.Empty) { Row { Text( - text = makePriceImpactBalanceWarning(amountEquivalent, priceImpact.priceImpact), + text = makePriceImpactBalanceWarning(amountEquivalent, priceImpact.getIntPercentValue()), color = TangemTheme.colors.text.tertiary, style = TangemTheme.typography.body2, modifier = Modifier @@ -491,7 +491,7 @@ private fun TransactionCardPreview() { onChangeTokenClick = {}, balance = "123", textFieldValue = TextFieldValue(), - priceImpact = null, + priceImpact = PriceImpact.Empty(), ) } From b5b4122c5fdf0e9c30a0af06879ef047e71ab396 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 17:33:15 +0300 Subject: [PATCH 047/102] Updated on 2026-08-14 --- .../com/tangem/feature/swap/ui/SwapScreen.kt | 4 +- .../feature/swap/ui/SwapScreenContent.kt | 75 +++++++++---------- .../tangem/feature/swap/ui/TransactionCard.kt | 8 +- 3 files changed, 42 insertions(+), 45 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt index cc39867e7d..04a53a581c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreen.kt @@ -1,8 +1,7 @@ package com.tangem.feature.swap.ui import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.layout.* import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -29,6 +28,7 @@ internal fun SwapScreen(stateHolder: SwapStateHolder) { iconRes = R.drawable.ic_close_24, ) }, + contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0), containerColor = TangemTheme.colors.background.secondary, ) { scaffoldPaddings -> 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 43a6329ace..54a29db438 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 @@ -1,6 +1,6 @@ package com.tangem.feature.swap.ui -import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.* import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -45,58 +45,53 @@ internal fun SwapScreenContent(state: SwapStateHolder, modifier: Modifier = Modi .fillMaxSize() .background(color = TangemTheme.colors.background.secondary), ) { - Column { - Column( + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding( + start = TangemTheme.dimens.spacing16, + end = TangemTheme.dimens.spacing16, + top = TangemTheme.dimens.spacing16, + bottom = TangemTheme.dimens.spacing32, + ), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), + ) { + MainInfo(state) + + ProviderItemBlock( + state = state.providerState, modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - .padding( - start = TangemTheme.dimens.spacing16, - end = TangemTheme.dimens.spacing16, - top = TangemTheme.dimens.spacing16, - bottom = TangemTheme.dimens.spacing32, + .clickable( + enabled = state.providerState.onProviderClick != null, + onClick = { state.providerState.onProviderClick?.invoke(state.providerState.id) }, ), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), - ) { - MainInfo(state) + ) - ProviderItemBlock( - state = state.providerState, + FeeItemBlock(state = state.fee) + + if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) + + MainButton(state = state, onPermissionWarningClick = state.onShowPermissionBottomSheet) + + state.tosState?.let { + ProviderTos( + tosState = it, modifier = Modifier - .clickable( - enabled = state.providerState.onProviderClick != null, - onClick = { state.providerState.onProviderClick?.invoke(state.providerState.id) }, - ), + .padding(top = TangemTheme.dimens.spacing16), ) - - FeeItemBlock(state = state.fee) - - if (state.warnings.isNotEmpty()) SwapWarnings(warnings = state.warnings) - - MainButton(state = state, onPermissionWarningClick = state.onShowPermissionBottomSheet) - - state.tosState?.let { - ProviderTos( - tosState = it, - modifier = Modifier - .padding(top = TangemTheme.dimens.spacing16), - ) - } } } - AnimatedVisibility( - visible = keyboard is Keyboard.Opened, - modifier = Modifier - .imePadding() - .align(Alignment.BottomCenter), - ) { + if (keyboard is Keyboard.Opened) { Text( text = stringResource(id = R.string.send_max_amount_label), style = TangemTheme.typography.button, color = TangemTheme.colors.text.primary1, modifier = Modifier + .align(Alignment.BottomCenter) + .imePadding() .fillMaxWidth() .background(TangemTheme.colors.button.secondary) .clickable { state.onMaxAmountSelected?.invoke() } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index e7b51283c2..5b16375022 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -191,7 +191,9 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie color = TangemTheme.colors.text.tertiary, maxLines = 1, style = MaterialTheme.typography.subtitle2, - modifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size24), + modifier = Modifier + .defaultMinSize(minHeight = TangemTheme.dimens.size24) + .align(Alignment.CenterVertically), ) SpacerW16() if (balance.isNotBlank()) { @@ -201,8 +203,8 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie color = TangemTheme.colors.text.tertiary, style = MaterialTheme.typography.body2, modifier = Modifier - .defaultMinSize(minHeight = TangemTheme.dimens.size20) - .padding(top = TangemTheme.dimens.spacing2), + .defaultMinSize(minHeight = TangemTheme.dimens.size24) + .align(Alignment.CenterVertically), ) } } else { From 946c8690f3b868d1ac7e17b307f80f5b63a16c5c Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 16:33:58 +0200 Subject: [PATCH 048/102] Updated on 2026-08-14 --- .../feature/swap/analytics/SwapEvents.kt | 13 ++++++++-- .../feature/swap/viewmodels/SwapViewModel.kt | 26 ++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index b128e88b19..5a16eca233 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -3,6 +3,7 @@ package com.tangem.feature.swap.analytics import com.tangem.core.analytics.models.AnalyticsEvent import com.tangem.feature.swap.domain.models.domain.SwapProvider import com.tangem.feature.swap.domain.models.ui.FeeType +import com.tangem.feature.swap.models.ApproveType private const val SWAP_CATEGORY = "Swap" @@ -38,9 +39,17 @@ sealed class SwapEvents( object ButtonGivePermissionClicked : SwapEvents(event = "Button - Give permission") - data class ButtonPermissionApproveClicked(val sendToken: String, val receiveToken: String) : SwapEvents( + data class ButtonPermissionApproveClicked( + val sendToken: String, + val receiveToken: String, + val approveType: ApproveType, + ) : SwapEvents( event = "Button - Permission Approve", - params = mapOf("Send Token" to sendToken, "Receive Token" to receiveToken), + params = mapOf( + "Send Token" to sendToken, + "Receive Token" to receiveToken, + "Type" to if (approveType == ApproveType.LIMITED) "Current Transaction" else "Unlimited" + ), ) object ButtonPermissionCancelClicked : SwapEvents(event = "Button - Permission Cancel") 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 b1438e8115..fe3d608266 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 @@ -733,16 +733,7 @@ internal class SwapViewModel @Inject constructor( }, onGivePermissionClick = { givePermissionsToSwap() - val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol - val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol - if (sendTokenSymbol != null && receiveTokenSymbol != null) { - analyticsEventHandler.send( - SwapEvents.ButtonPermissionApproveClicked( - sendToken = sendTokenSymbol, - receiveToken = receiveTokenSymbol, - ), - ) - } + sendPermissionApproveClickedEvent() }, onChangeCardsClicked = { onChangeCardsClicked() @@ -955,6 +946,21 @@ internal class SwapViewModel @Inject constructor( .contains(chosen.currency) } + private fun sendPermissionApproveClickedEvent() { + val sendTokenSymbol = dataState.fromCryptoCurrency?.currency?.symbol + val receiveTokenSymbol = dataState.toCryptoCurrency?.currency?.symbol + val approveType = dataState.approveType + if (sendTokenSymbol != null && receiveTokenSymbol != null && approveType != null) { + analyticsEventHandler.send( + SwapEvents.ButtonPermissionApproveClicked( + sendToken = sendTokenSymbol, + receiveToken = receiveTokenSymbol, + approveType = approveType, + ), + ) + } + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" From 9d14fb859fa359a7a73a8b7eb03b71e9843cb0b7 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 18:00:09 +0300 Subject: [PATCH 049/102] Updated on 2026-08-14 --- .../main/java/com/tangem/feature/swap/analytics/SwapEvents.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt index 5a16eca233..2fd8fa08ac 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/analytics/SwapEvents.kt @@ -48,7 +48,7 @@ sealed class SwapEvents( params = mapOf( "Send Token" to sendToken, "Receive Token" to receiveToken, - "Type" to if (approveType == ApproveType.LIMITED) "Current Transaction" else "Unlimited" + "Type" to if (approveType == ApproveType.LIMITED) "Current Transaction" else "Unlimited", ), ) From 23416dba220262a63da8698ec009a9f60fe91695 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 18:03:12 +0300 Subject: [PATCH 050/102] Updated on 2026-08-14 --- .../local/preferences/PreferencesKeys.kt | 2 +- .../settings/DefaultSwapPromoRepository.kt | 19 ++++++++----------- .../ShouldShowSwapPromoTokenUseCase.kt | 6 ++---- .../repositories/SwapPromoRepository.kt | 4 ++-- .../tokens/GetCurrencyWarningsUseCase.kt | 2 +- .../viewmodels/TokenDetailsViewModel.kt | 4 ++-- 6 files changed, 16 insertions(+), 21 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index ffa0aeee60..26d5b3cf08 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -45,7 +45,7 @@ object PreferencesKeys { val IS_WALLET_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isWalletSwapPromoShown") } - val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { stringSetPreferencesKey(name = "isTokenSwapPromoShown") } + val IS_TOKEN_SWAP_PROMO_SHOW_KEY by lazy { booleanPreferencesKey(name = "isTokenSwapPromoShown") } } /** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore */ diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt index cfdc97ba1c..dd94a2e07c 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt @@ -21,9 +21,9 @@ class DefaultSwapPromoRepository( .map { it && checkPromoPeriod() } } - override fun isReadyToShowToken(userWalletId: String, currencyId: String): Flow { - return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_SHOW_KEY, emptySet()) - .map { !it.contains(userWalletId + currencyId) && checkPromoPeriod() } + override fun isReadyToShowToken(): Flow { + return appPreferencesStore.get(IS_TOKEN_SWAP_PROMO_SHOW_KEY, true) + .map { it && checkPromoPeriod() } } override suspend fun setNeverToShowWallet() { @@ -33,14 +33,11 @@ class DefaultSwapPromoRepository( ) } - override suspend fun setNeverToShowToken(userWalletId: String, currencyId: String) { - appPreferencesStore.editData { mutablePreferences -> - val storesCurrencies = mutablePreferences.getOrDefault(IS_TOKEN_SWAP_PROMO_SHOW_KEY, emptySet()) - mutablePreferences.set( - key = IS_TOKEN_SWAP_PROMO_SHOW_KEY, - value = storesCurrencies.toMutableSet().apply { add(userWalletId + currencyId) }, - ) - } + override suspend fun setNeverToShowToken() { + appPreferencesStore.store( + key = IS_TOKEN_SWAP_PROMO_SHOW_KEY, + value = false, + ) } private fun checkPromoPeriod(): Boolean { diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt index 6d7b0b30f9..0c75a4f52a 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/ShouldShowSwapPromoTokenUseCase.kt @@ -5,9 +5,7 @@ import kotlinx.coroutines.flow.Flow class ShouldShowSwapPromoTokenUseCase(private val swapPromoRepository: SwapPromoRepository) { - operator fun invoke(userWalletId: String, currencyId: String): Flow = - swapPromoRepository.isReadyToShowToken(userWalletId, currencyId) + operator fun invoke(): Flow = swapPromoRepository.isReadyToShowToken() - suspend fun neverToShow(userWalletId: String, currencyId: String) = - swapPromoRepository.setNeverToShowToken(userWalletId, currencyId) + suspend fun neverToShow() = swapPromoRepository.setNeverToShowToken() } \ No newline at end of file diff --git a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt index 96d180f4df..95d31f65d8 100644 --- a/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt +++ b/domain/settings/src/main/java/com/tangem/domain/settings/repositories/SwapPromoRepository.kt @@ -5,9 +5,9 @@ import kotlinx.coroutines.flow.Flow interface SwapPromoRepository { fun isReadyToShowWallet(): Flow - fun isReadyToShowToken(userWalletId: String, currencyId: String): Flow + fun isReadyToShowToken(): Flow suspend fun setNeverToShowWallet() - suspend fun setNeverToShowToken(userWalletId: String, currencyId: String) + suspend fun setNeverToShowToken() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index ae959e251d..2998ea46b9 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -85,7 +85,7 @@ class GetCurrencyWarningsUseCase( val currency = currencyStatus.currency val cryptoStatuses = operations.getCurrenciesStatusesSync() return combine( - showSwapPromoTokenUseCase(userWalletId.stringValue, currency.id.value).conflate(), + showSwapPromoTokenUseCase().conflate(), flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(), ) { shouldShowSwapPromo, isExchangeable -> if (shouldShowSwapPromo && isExchangeable) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 06039c3878..1e23353d4a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -563,14 +563,14 @@ internal class TokenDetailsViewModel @Inject constructor( override fun onSwapPromoDismiss() { viewModelScope.launch(dispatchers.main) { - shouldShowSwapPromoTokenUseCase.neverToShow(userWalletId.stringValue, cryptoCurrency.id.value) + shouldShowSwapPromoTokenUseCase.neverToShow() analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Close) } } override fun onSwapPromoClick() { viewModelScope.launch(dispatchers.main) { - shouldShowSwapPromoTokenUseCase.neverToShow(userWalletId.stringValue, cryptoCurrency.id.value) + shouldShowSwapPromoTokenUseCase.neverToShow() analyticsEventsHandler.send(TokenSwapPromoAnalyticsEvent.Exchange(cryptoCurrency.symbol)) } onSwapClick() From bde94ffcee5c2a46ac92bf68e7dfff46c2142465 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 18 Dec 2023 22:19:41 +0300 Subject: [PATCH 051/102] Updated on 2026-08-14 --- .../tap/proxy/TransactionManagerImpl.kt | 27 +++++++++++++++++++ .../models/response/ExchangeDataResponse.kt | 6 +++++ .../tangem/feature/swap/SwapRepositoryImpl.kt | 2 +- .../swap/converters/ExpressDataConverter.kt | 2 ++ .../feature/swap/domain/api/SwapRepository.kt | 2 +- .../models/domain/ExpressTransactionModel.kt | 2 ++ .../feature/swap/domain/SwapInteractorImpl.kt | 12 ++++++++- libs/crypto/build.gradle.kts | 8 +++++- .../tangem/lib/crypto/TransactionManager.kt | 4 +++ 9 files changed, 61 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 33823b5d36..a0db65cfba 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -2,9 +2,15 @@ package com.tangem.tap.proxy import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.Message +import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras +import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumWalletManager import com.tangem.blockchain.blockchains.optimism.OptimismWalletManager +import com.tangem.blockchain.blockchains.stellar.StellarMemo +import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras +import com.tangem.blockchain.blockchains.ton.TonTransactionExtras +import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee @@ -18,6 +24,8 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.BlockchainNetwork import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.features.send.impl.presentation.viewmodel.XlmMemoType +import com.tangem.features.send.impl.presentation.viewmodel.determineXlmMemoType import com.tangem.features.wallet.featuretoggles.WalletFeatureToggles import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.* @@ -143,6 +151,25 @@ class TransactionManagerImpl( return blockchain.getExploreTxUrl(txAddress) } + override fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? { + val blockchain = Blockchain.fromNetworkId(networkId) + if (memo == null) return null + return when (blockchain) { + Blockchain.Stellar -> { + val xmlMemo = when (determineXlmMemoType(memo)) { + XlmMemoType.TEXT -> StellarMemo.Text(memo) + XlmMemoType.ID -> StellarMemo.Id(memo.toBigInteger()) + } + StellarTransactionExtras(xmlMemo) + } + Blockchain.Binance -> BinanceTransactionExtras(memo) + Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } + Blockchain.Cosmos -> CosmosTransactionExtras(memo) + Blockchain.TON -> TonTransactionExtras(memo) + else -> null + } + } + override fun getNativeTokenDecimals(networkId: String): Int { return Blockchain.fromNetworkId(networkId)?.decimals() ?: error("blockchain not found") } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index 72deee1a57..b44fa2a019 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -39,6 +39,12 @@ data class ExchangeDataResponse( @Json(name = "externalTxUrl") val externalTxUrl: String?, // null if DEX, url of provider exchange status page if CEX + + @Json(name = "txExtraIdName") + val txExtraIdName: String?, + + @Json(name = "txExtraId") + val txExtraId: String?, ) enum class TxType { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 62c8ebb0b6..24fa9fb08f 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -30,10 +30,10 @@ import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.converters.* import com.tangem.feature.swap.domain.api.SwapRepository -import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.ExpressException import com.tangem.feature.swap.domain.models.createFromAmountWithOffset +import com.tangem.feature.swap.domain.models.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt index 6746243002..86a75abcb7 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt @@ -34,6 +34,8 @@ class ExpressDataConverter : Converter { txTo = transactionDto.txTo, externalTxId = requireNotNull(transactionDto.externalTxId), externalTxUrl = requireNotNull(transactionDto.externalTxUrl), + txExtraIdName = transactionDto.txExtraIdName, + txExtraId = transactionDto.txExtraId, ) } } diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index 713cbeeb9e..c1310c961c 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -3,8 +3,8 @@ package com.tangem.feature.swap.domain.api import arrow.core.Either import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId -import com.tangem.feature.swap.domain.models.domain.* import com.tangem.feature.swap.domain.models.DataError +import com.tangem.feature.swap.domain.models.domain.* import java.math.BigDecimal interface SwapRepository { diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt index 89ab7c6754..a1c0981569 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExpressTransactionModel.kt @@ -25,5 +25,7 @@ sealed class ExpressTransactionModel { override val txTo: String, val externalTxId: String, val externalTxUrl: String, + val txExtraIdName: String?, + val txExtraId: String?, ) : ExpressTransactionModel() } \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 2de0ce5464..2fd8561359 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -499,13 +499,23 @@ internal class SwapInteractorImpl @Inject constructor( toAddress = currencyToGet.value.networkAddress?.defaultAddress?.value ?: "", ).getOrNull() + val exchangeDataCex = exchangeData?.transaction as? ExpressTransactionModel.CEX ?: return TxState.UnknownError + val txExtras = transactionManager.getMemoExtras( + currencyToSend.currency.network.backendId, + exchangeDataCex.txExtraId, + ) + if (txExtras == null && exchangeDataCex.txExtraId != null) { + return TxState.UnknownError + } val txData = walletManagersFacade.createTransaction( amount = amount.value.convertToAmount(currencyToSend.currency), fee = getFeeForTransaction(txFee), memo = null, - destination = (exchangeData?.transaction as ExpressTransactionModel.CEX).txTo, + destination = exchangeDataCex.txTo, userWalletId = userWalletId, network = currencyToSend.currency.network, + )?.copy( + extras = txExtras, ) val result = sendTransactionUseCase( diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 248cc20bd1..60b1f6f10e 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -1,5 +1,8 @@ plugins { - alias(deps.plugins.kotlin.jvm) + alias(deps.plugins.android.library) + alias(deps.plugins.kotlin.android) + alias(deps.plugins.kotlin.kapt) + alias(deps.plugins.kotlin.serialization) id("configuration") } @@ -7,4 +10,7 @@ dependencies { /** Coroutines */ implementation(deps.kotlin.coroutines) + + /** SDK */ + implementation(deps.tangem.blockchain) } \ No newline at end of file diff --git a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt index 3aa47352d5..ee8eb94937 100644 --- a/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt +++ b/libs/crypto/src/main/java/com/tangem/lib/crypto/TransactionManager.kt @@ -1,5 +1,6 @@ package com.tangem.lib.crypto +import com.tangem.blockchain.common.TransactionExtras import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult import java.math.BigDecimal @@ -71,4 +72,7 @@ interface TransactionManager { @Throws(IllegalStateException::class) fun getExplorerTransactionLink(networkId: String, txAddress: String): String + + // TODO: move to another place to use as in Send feature + fun getMemoExtras(networkId: String, memo: String?): TransactionExtras? } \ No newline at end of file From e6ec1f9d30c9fb04dbea95fd6c8d6f09d09ed89e Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Dec 2023 10:41:12 +0300 Subject: [PATCH 052/102] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 5557fb257d..6a3614624a 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-417" +tangemBlockchainSdk = "release-app_5.4-419" #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 ^ From aa73219a2b104a3ae5b33c48ddf153f3e650ce65 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Dec 2023 18:06:45 +0300 Subject: [PATCH 053/102] Updated on 2026-08-14 --- .../com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt index 2998ea46b9..9fe2ba124a 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCurrencyWarningsUseCase.kt @@ -88,7 +88,7 @@ class GetCurrencyWarningsUseCase( showSwapPromoTokenUseCase().conflate(), flowOf(marketCryptoCurrencyRepository.isExchangeable(userWalletId, currency)).conflate(), ) { shouldShowSwapPromo, isExchangeable -> - if (shouldShowSwapPromo && isExchangeable) { + if (shouldShowSwapPromo && isExchangeable && currencyStatus.value !is CryptoCurrencyStatus.Unreachable) { cryptoStatuses.fold( ifLeft = { null }, ifRight = { cryptoCurrencyStatuses -> From a0bf0bbdad58c67caf7398d35525f5e48a43da9a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Dec 2023 20:52:47 +0300 Subject: [PATCH 054/102] Updated on 2026-08-14 --- .../local/preferences/PreferencesKeys.kt | 2 + .../swap/DefaultSwapTransactionRepository.kt | 54 +++++++++++++++++-- .../feature/swap/domain/models/ui/TxState.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 14 +++++ .../swap/domain/SwapTransactionRepository.kt | 5 +- .../swap/domain/di/SwapDomainModule.kt | 2 + .../tangem/feature/swap/ui/StateBuilder.kt | 3 +- .../feature/swap/viewmodels/SwapViewModel.kt | 11 ++-- .../viewmodels/ExchangeStatusFactory.kt | 9 ++-- .../viewmodels/TokenDetailsViewModel.kt | 27 +++++----- 10 files changed, 98 insertions(+), 30 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt index 26d5b3cf08..02432a3d03 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/preferences/PreferencesKeys.kt @@ -35,6 +35,8 @@ object PreferencesKeys { val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") } + val SWAP_TRANSACTIONS_STATUSES_KEY by lazy { stringPreferencesKey(name = "swapTransactionsStatuses") } + val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") } val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt index 97b9a1e2b2..f617305f17 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapTransactionRepository.kt @@ -4,9 +4,11 @@ import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys import com.tangem.datasource.local.preferences.utils.getObjectList import com.tangem.datasource.local.preferences.utils.getObjectListSync +import com.tangem.datasource.local.preferences.utils.getObjectMap import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedLastSwappedCryptoCurrency import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel @@ -24,11 +26,11 @@ class DefaultSwapTransactionRepository( toCryptoCurrencyId: CryptoCurrency.ID, transaction: SavedSwapTransactionModel, ) { + transaction.status?.let { storeTransactionState(transaction.txId, it) } appPreferencesStore.editData { mutablePreferences -> val savedTransactions: List? = mutablePreferences.getObjectList( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, ) - val tokenTransactions = savedTransactions ?.firstOrNull { it.checkId( @@ -62,14 +64,17 @@ class DefaultSwapTransactionRepository( } } - override fun getTransactions( + override suspend fun getTransactions( userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, ): Flow?> { + val txStatuses = appPreferencesStore.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ) return appPreferencesStore.getObjectList( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, ).map { savedTransactions -> - savedTransactions + val currencyTxs = savedTransactions ?.filter { it.userWalletId == userWalletId.stringValue && ( @@ -77,6 +82,14 @@ class DefaultSwapTransactionRepository( it.fromCryptoCurrencyId == cryptoCurrencyId.value ) } + + currencyTxs?.map { currencyTx -> + currencyTx.copy( + transactions = currencyTx.transactions.map { tx -> + tx.copy(status = txStatuses[tx.txId]) + }, + ) + } } } @@ -86,6 +99,7 @@ class DefaultSwapTransactionRepository( toCryptoCurrencyId: CryptoCurrency.ID, txId: String, ) { + clearTransactionsStatuses(txId = txId) appPreferencesStore.editData { mutablePreferences -> val savedList: List? = mutablePreferences.getObjectList( key = PreferencesKeys.SWAP_TRANSACTIONS_KEY, @@ -130,6 +144,22 @@ class DefaultSwapTransactionRepository( } } + override suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel) { + appPreferencesStore.editData { mutablePreferences -> + val savedMap = mutablePreferences.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ) + + val updatesMap = savedMap?.toMutableMap() ?: mutableMapOf() + updatesMap[txId] = status + + mutablePreferences.setObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + value = updatesMap, + ) + } + } + override suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? { val lastSwappedCurrencies = appPreferencesStore.getObjectListSync( key = PreferencesKeys.LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY, @@ -194,4 +224,22 @@ class DefaultSwapTransactionRepository( }, ) } + + private suspend fun clearTransactionsStatuses(txId: String) { + appPreferencesStore.editData { mutablePreferences -> + val savedList = mutablePreferences.getObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + ) + val editedList = savedList?.filterNot { it.key == txId } + + if (editedList.isNullOrEmpty()) { + mutablePreferences.remove(key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY) + } else { + mutablePreferences.setObjectMap( + key = PreferencesKeys.SWAP_TRANSACTIONS_STATUSES_KEY, + value = editedList, + ) + } + } + } } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt index 526b5b2a73..132327e4cb 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TxState.kt @@ -11,6 +11,7 @@ sealed class TxState { val toAmountValue: BigDecimal? = null, val txAddress: String, val txExternalUrl: String? = null, + val txUrl: String? = null, val timestamp: Long, ) : TxState() diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 2fd8561359..be755e8764 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -54,6 +54,7 @@ internal class SwapInteractorImpl @Inject constructor( private val dispatcher: CoroutineDispatcherProvider, private val swapTransactionRepository: SwapTransactionRepository, private val initialToCurrencyResolver: InitialToCurrencyResolver, + private val blockchainInteractor: BlockchainInteractor, ) : SwapInteractor { private val estimateFeeUseCase by lazy(LazyThreadSafetyMode.NONE) { @@ -539,6 +540,10 @@ internal class SwapInteractorImpl @Inject constructor( }, ifRight = { val timestamp = System.currentTimeMillis() + val txUrl = blockchainInteractor.getExplorerTransactionLink( + networkId = currencyToSend.currency.network.backendId, + txAddress = exchangeData.transaction.txTo, + ) storeSwapTransaction( currencyToSend = currencyToSend, currencyToGet = currencyToGet, @@ -546,6 +551,7 @@ internal class SwapInteractorImpl @Inject constructor( swapProvider = swapProvider, swapDataModel = exchangeData, timestamp = timestamp, + txUrl = txUrl, ) storeLastCryptoCurrencyId(currencyToGet.currency) TxState.TxSent( @@ -564,6 +570,7 @@ internal class SwapInteractorImpl @Inject constructor( derivationPath, ).orEmpty(), txExternalUrl = externalUrl, + txUrl = txUrl, timestamp = timestamp, ) }, @@ -597,6 +604,7 @@ internal class SwapInteractorImpl @Inject constructor( swapProvider: SwapProvider, swapDataModel: SwapDataModel, timestamp: Long, + txUrl: String, ) { swapTransactionRepository.storeTransaction( userWalletId = UserWalletId(userWalletManager.getWalletId()), @@ -608,6 +616,12 @@ internal class SwapInteractorImpl @Inject constructor( timestamp = timestamp, fromCryptoAmount = amount.value, toCryptoAmount = swapDataModel.toTokenAmount.value, + status = ExchangeStatusModel( + providerId = swapProvider.providerId, + status = ExchangeStatus.New, + txId = swapDataModel.transaction.txId, + txUrl = txUrl, + ), ), ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt index 2db58ee01d..e00dadb2a9 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapTransactionRepository.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.wallets.models.UserWalletId +import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionModel import kotlinx.coroutines.flow.Flow @@ -15,7 +16,7 @@ interface SwapTransactionRepository { transaction: SavedSwapTransactionModel, ) - fun getTransactions( + suspend fun getTransactions( userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID, ): Flow?> @@ -27,6 +28,8 @@ interface SwapTransactionRepository { txId: String, ) + suspend fun storeTransactionState(txId: String, status: ExchangeStatusModel) + suspend fun storeLastSwappedCryptoCurrencyId(userWalletId: UserWalletId, cryptoCurrencyId: CryptoCurrency.ID) suspend fun getLastSwappedCryptoCurrencyId(userWalletId: UserWalletId): String? diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index d299f7824e..518d7c9fc8 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -42,6 +42,7 @@ class SwapDomainModule { walletManagersFacade: WalletManagersFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, initialToCurrencyResolver: InitialToCurrencyResolver, + blockchainInteractor: BlockchainInteractor, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -56,6 +57,7 @@ class SwapDomainModule { dispatcher = coroutineDispatcherProvider, swapTransactionRepository = swapTransactionRepository, initialToCurrencyResolver = initialToCurrencyResolver, + blockchainInteractor = blockchainInteractor, ) } 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 c74bd436f7..07fd38fad3 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 @@ -717,7 +717,6 @@ internal class StateBuilder( fun createSuccessState( uiState: SwapStateHolder, txState: TxState.TxSent, - txUrl: String, dataState: SwapProcessDataState, onExploreClick: () -> Unit, onStatusClick: () -> Unit, @@ -735,7 +734,7 @@ internal class StateBuilder( return uiState.copy( successState = SwapSuccessStateHolder( timestamp = txState.timestamp, - txUrl = txUrl, + txUrl = txState.txUrl.orEmpty(), providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), showStatusButton = providerState.type == ExchangeProviderType.CEX.name, 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 fe3d608266..b9235b75a5 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 @@ -447,19 +447,14 @@ internal class SwapViewModel @Inject constructor( }.onSuccess { when (it) { is TxState.TxSent -> { - val url = blockchainInteractor.getExplorerTransactionLink( - networkId = fromCurrency.currency.network.backendId, - txAddress = it.txAddress, - ) uiState = stateBuilder.createSuccessState( uiState = uiState, txState = it, dataState = dataState, - txUrl = url, onExploreClick = { - val txHash = it.txAddress - if (txHash.isNotEmpty()) { - swapRouter.openUrl(url) + val txUrl = it.txUrl + if (!txUrl.isNullOrBlank()) { + swapRouter.openUrl(txUrl) } analyticsEventHandler.send( event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 7ef7622a82..4c78d11155 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -54,7 +54,7 @@ internal class ExchangeStatusFactory( ) } - operator fun invoke() = combine( + suspend operator fun invoke() = combine( flow = swapTransactionRepository.getTransactions(userWalletId, cryptoCurrency.id), flow2 = getWalletCryptoCurrencies().conflate(), ) { savedTransactions, cryptoCurrenciesStatusList -> @@ -95,9 +95,10 @@ internal class ExchangeStatusFactory( return swapRepository.getExchangeStatus(txId) .fold( ifLeft = { null }, - ifRight = { - sendStatusUpdateAnalytics(it) - it + ifRight = { statusModel -> + sendStatusUpdateAnalytics(statusModel) + swapTransactionRepository.storeTransactionState(txId, statusModel) + statusModel }, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1e23353d4a..1c3f77a105 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -222,6 +222,7 @@ internal class TokenDetailsViewModel @Inject constructor( .distinctUntilChanged() .filterNot { it.isEmpty() } .onEach { swapTxs -> + updateSwapTx(swapTxs) swapTxStatusTaskScheduler.scheduleTask( viewModelScope, PeriodicTask( @@ -231,18 +232,8 @@ internal class TokenDetailsViewModel @Inject constructor( exchangeStatusFactory.updateSwapTxStatuses(swapTxs) } }, - onSuccess = { updatedTxs -> - val config = uiState.bottomSheetConfig - val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig - val currentTx = updatedTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId } - uiState = uiState.copy( - swapTxs = updatedTxs, - bottomSheetConfig = currentTx?.let( - stateFactory::updateStateWithExchangeStatusBottomSheet, - ) ?: config, - ) - }, - onError = {}, + onSuccess = ::updateSwapTx, + onError = { /* no-op */ }, ), ) } @@ -252,6 +243,18 @@ internal class TokenDetailsViewModel @Inject constructor( } } + private fun updateSwapTx(swapTxs: PersistentList) { + val config = uiState.bottomSheetConfig + val exchangeBottomSheet = config?.content as? ExchangeStatusBottomSheetConfig + val currentTx = swapTxs.firstOrNull { it.txId == exchangeBottomSheet?.value?.txId } + uiState = uiState.copy( + swapTxs = swapTxs, + bottomSheetConfig = currentTx?.let( + stateFactory::updateStateWithExchangeStatusBottomSheet, + ) ?: config, + ) + } + /** * @param refresh - invalidate cache and get data from remote * @param showItemsLoading - show loading items placeholder. From 8a840fe5f0b2cc7d30ad0ae8801b9a463a4a78bb Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Dec 2023 20:24:55 +0200 Subject: [PATCH 055/102] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../swap/models/states/FeeItemState.kt | 1 + .../feature/swap/ui/ChooseFeeBottomSheet.kt | 2 ++ .../com/tangem/feature/swap/ui/FeeItem.kt | 30 +++++++++++++++++-- .../tangem/feature/swap/ui/StateBuilder.kt | 11 +++++-- .../feature/swap/ui/SwapScreenContent.kt | 3 +- 7 files changed, 44 insertions(+), 5 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 744452a5fa..42f9f8b591 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -194,6 +194,7 @@ Мои токены У вас нет добавленных токенов. Добавьте токены для обмена Недоступен для обмена с %s + Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес Статус Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами Выберите провайдера diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 55f40ad98b..1150c0349b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -193,6 +193,7 @@ My tokens You haven\'t added any tokens yet. Add tokens via Market to swap Cannot be swapped for %s + Additionally, the network fee for sending the exchanged funds back to your address is included in the rate Status Providers facilitate transactions, ensuring smooth and efficient token swaps Choose provider diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt index e01a8b8058..6ca10e195f 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/states/FeeItemState.kt @@ -11,6 +11,7 @@ sealed class FeeItemState { val amountCrypto: String, val symbolCrypto: String, val amountFiatFormatted: String, + val explanation: TextReference?, val isClickable: Boolean, val onClick: () -> Unit, ) : FeeItemState() 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 d0f47a62a2..3c3de0d27f 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 @@ -109,6 +109,7 @@ private fun ChooseFeeBottomSheetContent_Preview() { amountCrypto = "1000", symbolCrypto = "MATIC", amountFiatFormatted = "(10$)", + explanation = null, isClickable = false, onClick = {}, ), @@ -118,6 +119,7 @@ private fun ChooseFeeBottomSheetContent_Preview() { amountCrypto = "2000", symbolCrypto = "MATIC", amountFiatFormatted = "(10$)", + explanation = null, isClickable = false, onClick = {}, ), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt index f379ae3511..f5385bf456 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/FeeItem.kt @@ -3,6 +3,8 @@ package com.tangem.feature.swap.ui import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.material.Text +import androidx.compose.material3.Divider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -24,12 +26,13 @@ fun FeeItemBlock(state: FeeItemState) { @Composable fun FeeItem(state: FeeItemState.Content) { - Box( + Column( modifier = Modifier .background( color = TangemTheme.colors.background.action, shape = TangemTheme.shapes.roundedCornersXMedium, ) + .padding(start = TangemTheme.dimens.spacing12) .clip(shape = TangemTheme.shapes.roundedCornersXMedium) .clickable( onClick = state.onClick, @@ -40,13 +43,32 @@ fun FeeItem(state: FeeItemState.Content) { val description = "${state.amountCrypto} ${state.symbolCrypto} (${state.amountFiatFormatted})" SimpleActionRow( modifier = Modifier.padding( - start = TangemTheme.dimens.spacing12, top = TangemTheme.dimens.spacing12, ), title = state.title.resolveReference(), description = description, isClickable = state.isClickable, ) + state.explanation?.let { + Divider( + color = TangemTheme.colors.stroke.primary, + thickness = TangemTheme.dimens.size0_5, + modifier = Modifier.padding( + top = TangemTheme.dimens.spacing10, + bottom = TangemTheme.dimens.spacing10, + end = TangemTheme.dimens.spacing2, + ), + ) + Text( + text = it.resolveReference(), + style = TangemTheme.typography.caption2, + color = TangemTheme.colors.text.tertiary, + modifier = Modifier.padding( + bottom = TangemTheme.dimens.spacing10, + end = TangemTheme.dimens.spacing16, + ), + ) + } } } @@ -59,6 +81,10 @@ private fun FeeItemPreview() { amountCrypto = "1000", symbolCrypto = "MATIC", amountFiatFormatted = "(1000$)", + explanation = stringReference( + "Additionally, the network fee for sending the exchanged funds back to your address is " + + "included in the rate", + ), isClickable = false, onClick = {}, ) 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 c74bd436f7..db0ab167ab 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 @@ -215,7 +215,7 @@ internal class StateBuilder( if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder val warnings = getWarningsForSuccessState(quoteModel, fromToken) - val feeState = createFeeState(quoteModel.txFee, selectedFeeType) + val feeState = createFeeState(quoteModel.txFee, selectedFeeType, swapProvider) val fromCurrencyStatus = quoteModel.fromTokenInfo.cryptoCurrencyStatus val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus return uiStateHolder.copy( @@ -666,7 +666,7 @@ internal class StateBuilder( ) } - private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType): FeeItemState { + private fun createFeeState(txFeeState: TxFeeState, feeType: FeeType, swapProvider: SwapProvider): FeeItemState { val isClickable: Boolean val fee = when (txFeeState) { TxFeeState.Empty -> return FeeItemState.Empty @@ -692,6 +692,11 @@ internal class StateBuilder( title = resourceReference(R.string.common_fee_label), amountCrypto = fee.feeCryptoFormatted, symbolCrypto = fee.cryptoSymbol, + explanation = if (swapProvider.type == ExchangeProviderType.CEX) { + resourceReference(R.string.express_cex_fee_explanation) + } else { + null + }, amountFiatFormatted = fee.feeFiatFormatted, isClickable = isClickable, onClick = actions.onClickFee, @@ -1002,6 +1007,7 @@ internal class StateBuilder( amountCrypto = this.normalFee.feeCryptoFormatted, symbolCrypto = this.normalFee.cryptoSymbol, amountFiatFormatted = this.normalFee.feeFiatFormatted, + explanation = null, isClickable = true, onClick = {}, ), @@ -1011,6 +1017,7 @@ internal class StateBuilder( amountCrypto = this.priorityFee.feeCryptoFormatted, symbolCrypto = this.priorityFee.cryptoSymbol, amountFiatFormatted = this.priorityFee.feeFiatFormatted, + explanation = null, isClickable = true, onClick = {}, ), 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 54a29db438..1e0b3f5944 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 @@ -467,13 +467,14 @@ private val state = SwapStateHolder( amountCrypto = "100", symbolCrypto = "1000", amountFiatFormatted = "(100)", + explanation = null, isClickable = true, onClick = {}, ), warnings = listOf( SwapWarning.PermissionNeeded( notificationConfig = NotificationConfig( - title = stringReference("Give Premission"), + title = stringReference("Give Permission"), subtitle = stringReference("To continue swapping you need to give permission to Tangem"), iconResId = R.drawable.ic_locked_24, ), From ba7038debcdf9d02da527aa303ce23b7963c5471 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 19 Dec 2023 20:29:07 +0200 Subject: [PATCH 056/102] Updated on 2026-08-14 --- .../datasource/di/StatusCodeInterceptor.kt | 49 +++++++++++++++++++ .../swap/converters/ErrorsDataConverter.kt | 2 +- .../feature/swap/domain/models/DataError.kt | 2 + 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt new file mode 100644 index 0000000000..c6f086b1ac --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt @@ -0,0 +1,49 @@ +package com.tangem.datasource.di + +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import timber.log.Timber + +class StatusCodeInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + val originalResponse = chain.proceed(chain.request()) + + + if (shouldInterceptResponse(originalResponse)) { + Timber.e("StatusCodeInterceptor INTERCEPTED%s", originalResponse.request.url.toString()) + + val body = getBody().toResponseBody("application/json".toMediaTypeOrNull()) + val code = getCode() + + return originalResponse.newBuilder() + .code(code) + .body(body) + .build() + } + + return originalResponse + } + + private fun shouldInterceptResponse(response: Response): Boolean { + return response.request.url.toString().contains("exchange-quote") + // && response.request.url.toString().contains("changenow") + } + + + private fun getCode() : Int { + return 400 + } + + private fun getBody() : String { + return "\"error\": {\n" + + " \"code\": 2290,\n" + + " \"description\": \"Core: receivedDecimals is not equal to expressDecimals\",\n" + + " \"message\": \"Not valid\",\n" + + " \"receivedToDecimals\": 5,\n" + + " \"expressToDecimals\": 5\n" + + " }" + } +} \ No newline at end of file diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index 2068fc982a..6ca55d7f04 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -39,7 +39,7 @@ internal class ErrorsDataConverter( receivedFromDecimals = requireNotNull(error.value?.receivedFromDecimals), expressFromDecimals = requireNotNull(error.value?.expressFromDecimals), ) - else -> DataError.UnknownError + else -> DataError.UnknownErrorWithCode(error.code) } } catch (e: Exception) { return DataError.UnknownError diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index 2c3a3e6af6..829cb5e7e4 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -30,6 +30,8 @@ sealed class DataError { val expressFromDecimals: Int, ) : DataError() + data class UnknownErrorWithCode(override val code: Int) : DataError() + object UnknownError : DataError() { override val code: Int = -1 } From eb0b9ca69c41099a8c535b78fdd545d59c0c8145 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Dec 2023 11:54:55 +0300 Subject: [PATCH 057/102] Updated on 2026-08-14 --- .../swap/domain/models/ui/SwapState.kt | 1 + .../feature/swap/domain/SwapInteractorImpl.kt | 51 ++++++++++++------- .../tangem/feature/swap/ui/StateBuilder.kt | 15 ++++-- .../feature/swap/viewmodels/SwapViewModel.kt | 1 + 4 files changed, 47 insertions(+), 21 deletions(-) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 97c7c85fb3..6711df346c 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -37,6 +37,7 @@ sealed interface SwapState { data class SwapError( val fromTokenInfo: TokenSwapInfo, val error: DataError, + val includeFeeInAmount: IncludeFeeInAmount, ) : SwapState } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 2fd8561359..038f23b435 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -37,6 +37,7 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.firstOrNull import timber.log.Timber import java.math.BigDecimal +import java.math.BigInteger import java.math.RoundingMode import javax.inject.Inject @@ -263,7 +264,6 @@ internal class SwapInteractorImpl @Inject constructor( provider = provider, amount = amount, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, - selectedFee = selectedFee, ) } } @@ -340,7 +340,6 @@ internal class SwapInteractorImpl @Inject constructor( provider: SwapProvider, amount: SwapAmount, isBalanceWithoutFeeEnough: Boolean, - selectedFee: FeeType, ): Pair { return provider to loadCexQuoteData( exchangeProviderType = ExchangeProviderType.CEX, @@ -351,7 +350,6 @@ internal class SwapInteractorImpl @Inject constructor( isAllowedToSpend = true, isBalanceWithoutFeeEnough = isBalanceWithoutFeeEnough, provider = provider, - selectedFee = selectedFee, ) } @@ -411,7 +409,6 @@ internal class SwapInteractorImpl @Inject constructor( txFee = state.txFee, amount = amount, fromToken = fromToken.currency, - selectedFee = selectedFee, ) return state.copy( permissionState = PermissionDataState.Empty, @@ -580,10 +577,11 @@ internal class SwapInteractorImpl @Inject constructor( ) return if (fee.gasLimit != 0) { + val feeAmountWithDecimals = feeAmountValue.movePointRight(fee.decimals) Fee.Ethereum( amount = feeAmount, gasLimit = fee.gasLimit.toBigInteger(), - gasPrice = (feeAmountValue / fee.gasLimit.toBigDecimal()).toBigInteger(), + gasPrice = (feeAmountWithDecimals / fee.gasLimit.toBigDecimal()).toBigInteger(), ) } else { Fee.Common(feeAmount) @@ -709,7 +707,6 @@ internal class SwapInteractorImpl @Inject constructor( provider: SwapProvider, isAllowedToSpend: Boolean, isBalanceWithoutFeeEnough: Boolean, - selectedFee: FeeType, ): SwapState { val fromToken = fromTokenStatus.currency val toToken = toTokenStatus.currency @@ -725,7 +722,6 @@ internal class SwapInteractorImpl @Inject constructor( txFee = txFee, amount = amount, fromToken = fromToken, - selectedFee = selectedFee, ) val amountToRequest = if (includeFeeInAmount is IncludeFeeInAmount.Included) { includeFeeInAmount.amountSubtractFee @@ -824,7 +820,7 @@ internal class SwapInteractorImpl @Inject constructor( ?: BigDecimal.ZERO, cryptoCurrencyStatus = fromToken, ) - return SwapState.SwapError(fromTokenSwapInfo, error) + return SwapState.SwapError(fromTokenSwapInfo, error, includeFeeInAmount) }, ) } @@ -834,7 +830,6 @@ internal class SwapInteractorImpl @Inject constructor( txFee: TxFeeState, amount: SwapAmount, fromToken: CryptoCurrency, - selectedFee: FeeType, ): IncludeFeeInAmount { if (fromToken is CryptoCurrency.Token) { return IncludeFeeInAmount.Excluded @@ -851,11 +846,7 @@ internal class SwapInteractorImpl @Inject constructor( } val feeValue = when (txFee) { TxFeeState.Empty -> BigDecimal.ZERO - is TxFeeState.MultipleFeeState -> if (selectedFee == FeeType.NORMAL) { - txFee.normalFee.feeValue - } else { - txFee.priorityFee.feeValue - } + is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue is TxFeeState.SingleFeeState -> txFee.fee.feeValue } @@ -965,6 +956,7 @@ internal class SwapInteractorImpl @Inject constructor( SwapState.SwapError( fromTokenSwapInfo, error, + IncludeFeeInAmount.Excluded, ) }, ) @@ -1191,8 +1183,10 @@ internal class SwapInteractorImpl @Inject constructor( val decimals = transactionManager.getNativeTokenDecimals(networkId) return when (this) { is TransactionFee.Choosable -> { - val feeNormal = this.normal.amount.value ?: BigDecimal.ZERO - val feePriority = this.priority.amount.value ?: BigDecimal.ZERO + val normalFee = this.normal.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND) + val priorityFee = this.priority.increaseGasLimitBy(INCREASE_GAS_LIMIT_FOR_SEND) + val feeNormal = normalFee.amount.value ?: BigDecimal.ZERO + val feePriority = priorityFee.amount.value ?: BigDecimal.ZERO val normalFiatValue = getFormattedFiatFees(networkId, feeNormal)[0] val priorityFiatValue = getFormattedFiatFees(networkId, feePriority)[0] @@ -1207,7 +1201,7 @@ internal class SwapInteractorImpl @Inject constructor( TxFeeState.MultipleFeeState( normalFee = TxFee( feeValue = feeNormal, - gasLimit = this.normal.getGasLimit(), + gasLimit = normalFee.getGasLimit(), feeFiatFormatted = normalFiatValue, feeCryptoFormatted = normalCryptoFee, decimals = decimals, @@ -1216,7 +1210,7 @@ internal class SwapInteractorImpl @Inject constructor( ), priorityFee = TxFee( feeValue = feePriority, - gasLimit = this.priority.getGasLimit(), + gasLimit = priorityFee.getGasLimit(), feeFiatFormatted = priorityFiatValue, feeCryptoFormatted = priorityCryptoFee, decimals = decimals, @@ -1247,6 +1241,26 @@ internal class SwapInteractorImpl @Inject constructor( } } + /** + * Workaround to increase gas limit cause we calculate fee for random address + */ + private fun Fee.increaseGasLimitBy(percentage: Int): Fee { + if (this !is Fee.Ethereum) return this + val gasLimit = this.gasLimit + val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals) + ?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP) + val increaseGasLimit = gasLimit + .multiply(percentage.toBigInteger()) + .divide(BigInteger("100")) + val increasedAmount = this.amount.copy( + value = increaseGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals), + ) + return this.copy( + amount = increasedAmount, + gasLimit = increaseGasLimit, + ) + } + private fun hasOutgoingTransaction(cryptoCurrencyStatuses: CryptoCurrencyStatus): Boolean { return cryptoCurrencyStatuses.value.pendingTransactions.any { it.isOutgoing } } @@ -1377,6 +1391,7 @@ internal class SwapInteractorImpl @Inject constructor( companion object { @Suppress("UnusedPrivateMember") private const val INCREASE_GAS_LIMIT_BY = 112 // 12% + private const val INCREASE_GAS_LIMIT_FOR_SEND = 105 // 5% private const val INFINITY_SYMBOL = "∞" private val ONE_INCH_SUPPORTED_NETWORKS = listOf( 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 c74bd436f7..4e93debef0 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 @@ -394,12 +394,21 @@ internal class StateBuilder( swapProvider: SwapProvider, fromToken: TokenSwapInfo, toToken: CryptoCurrencyStatus?, + includeFeeInAmount: IncludeFeeInAmount, dataError: DataError, isReverseSwapPossible: Boolean, ): SwapStateHolder { if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder - val warning = getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency) + val warnings = mutableListOf() + warnings.add(getWarningForError(dataError, fromToken.cryptoCurrencyStatus.currency)) + if (includeFeeInAmount is IncludeFeeInAmount.Included) { + warnings.add( + SwapWarning.GeneralWarning( + createNetworkFeeCoverageNotificationConfig(), + ), + ) + } val providerState = getProviderStateForError( swapProvider = swapProvider, fromToken = fromToken.cryptoCurrencyStatus.currency, @@ -437,7 +446,7 @@ internal class StateBuilder( amountEquivalent = getFormattedFiatAmount(fromToken.amountFiat), ), receiveCardData = receiveCardData, - warnings = listOf(warning), + warnings = warnings, permissionState = SwapPermissionState.Empty, fee = FeeItemState.Empty, swapButton = SwapButton( @@ -1221,7 +1230,7 @@ internal class StateBuilder( } private fun SwapAmount.getFormattedCryptoAmount(token: CryptoCurrency): String { - return "${this.formatToUIRepresentation()} ${token.network.currencySymbol}" + return "${this.formatToUIRepresentation()} ${token.symbol}" } private fun BigDecimal.calculateRate(to: BigDecimal, decimals: Int): BigDecimal { 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 fe3d608266..1d9fbafcbc 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 @@ -344,6 +344,7 @@ internal class SwapViewModel @Inject constructor( fromToken = state.fromTokenInfo, toToken = dataState.toCryptoCurrency, dataError = state.error, + includeFeeInAmount = state.includeFeeInAmount, isReverseSwapPossible = isReverseSwapPossible(), ) sendErrorAnalyticsEvent(state.error, provider) From ed7d2eff25221b040f0bdffa949d8c2f57793973 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 20 Dec 2023 11:57:01 +0300 Subject: [PATCH 058/102] Updated on 2026-08-14 --- .../tangem/feature/swap/domain/SwapInteractorImpl.kt | 11 +++++------ .../java/com/tangem/feature/swap/ui/StateBuilder.kt | 1 + 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 038f23b435..023399ec63 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -9,7 +9,6 @@ import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.domain.tokens.GetCryptoCurrencyStatusesSyncUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus -import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.Quote import com.tangem.domain.tokens.repository.QuotesRepository import com.tangem.domain.tokens.utils.convertToAmount @@ -63,7 +62,7 @@ internal class SwapInteractorImpl @Inject constructor( private val swapCurrencyConverter = SwapCurrencyConverter() private val amountFormatter = AmountFormatter() - private var network: Network? = null + private val hundredPercent = BigInteger("100") override suspend fun getTokensDataState(currency: CryptoCurrency): TokensDataStateExpress { val selectedWallet = getSelectedWalletSyncUseCase().fold( @@ -1249,15 +1248,15 @@ internal class SwapInteractorImpl @Inject constructor( val gasLimit = this.gasLimit val increasedGasPrice = this.amount.value?.movePointRight(this.amount.decimals) ?.divide(gasLimit.toBigDecimal(), RoundingMode.HALF_UP) - val increaseGasLimit = gasLimit + val increasedGasLimit = gasLimit .multiply(percentage.toBigInteger()) - .divide(BigInteger("100")) + .divide(hundredPercent) val increasedAmount = this.amount.copy( - value = increaseGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals), + value = increasedGasLimit.toBigDecimal().multiply(increasedGasPrice).movePointLeft(this.amount.decimals), ) return this.copy( amount = increasedAmount, - gasLimit = increaseGasLimit, + gasLimit = increasedGasLimit, ) } 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 4e93debef0..5de6a8037d 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 @@ -378,6 +378,7 @@ internal class StateBuilder( } private fun getSwapButtonEnabled(preparedSwapConfigState: PreparedSwapConfigState): Boolean { + if (preparedSwapConfigState.hasOutgoingTransaction) return false return when (preparedSwapConfigState.includeFeeInAmount) { IncludeFeeInAmount.BalanceNotEnough -> false IncludeFeeInAmount.Excluded -> From 925ae57742d1ae581fe0f2367721a237c911b18e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Dec 2023 11:32:15 +0300 Subject: [PATCH 059/102] Updated on 2026-08-14 --- .../tangem/feature/swap/SwapRepositoryImpl.kt | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt index 24fa9fb08f..c95e9b4ddf 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt @@ -5,10 +5,7 @@ import arrow.core.left import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.right -import com.tangem.blockchain.common.Amount -import com.tangem.blockchain.common.Approver -import com.tangem.blockchain.common.Blockchain -import com.tangem.blockchain.common.Token +import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.data.tokens.utils.CryptoCurrencyFactory import com.tangem.datasource.api.common.response.ApiResponse @@ -347,23 +344,27 @@ internal class SwapRepositoryImpl @Inject constructor( return (walletManager as? Approver)?.getApproveData( spenderAddress, - amount?.let { convertToAmount(it, currency, blockchain) }, + amount?.let { convertToAmount(it, currency) }, ) ?: error("Cannot cast to Approver") } - private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency, blockchain: Blockchain): Amount { - return when (currency) { - is CryptoCurrency.Token -> { - Amount(value = amount, blockchain = blockchain) - } - is CryptoCurrency.Coin -> { - Amount( - currencySymbol = currency.symbol, - value = amount, - decimals = currency.decimals, + private fun convertToAmount(amount: BigDecimal, currency: CryptoCurrency): Amount { + return Amount( + currencySymbol = currency.symbol, + value = amount, + decimals = currency.decimals, + type = if (currency is CryptoCurrency.Token) { + AmountType.Token( + Token( + symbol = currency.symbol, + contractAddress = currency.contractAddress, + decimals = currency.decimals, + ), ) - } - } + } else { + AmountType.Coin + }, + ) } private fun getOneInchApi(networkId: String): OneInchApi { From f1ce18c109f7194c69c60354bb0e057f2fdfc77b Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Dec 2023 11:00:44 +0200 Subject: [PATCH 060/102] Updated on 2026-08-14 --- .../feature/swap/models/SwapStateHolder.kt | 10 +++++-- .../tangem/feature/swap/ui/StateBuilder.kt | 26 ++++++++++--------- .../feature/swap/ui/SwapScreenContent.kt | 5 ++-- .../tangem/feature/swap/ui/TransactionCard.kt | 13 ++++------ 4 files changed, 29 insertions(+), 25 deletions(-) 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 00b174011d..83d86eee5b 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 @@ -1,7 +1,9 @@ package com.tangem.feature.swap.models import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.compose.ui.text.input.TextFieldValue +import com.tangem.core.ui.R import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig import com.tangem.core.ui.components.notifications.NotificationConfig import com.tangem.core.ui.components.states.Item @@ -99,13 +101,17 @@ sealed interface FeeSelectState { sealed interface TransactionCardType { - data class SendCard( + val headerResId: Int + + data class Inputtable( val onAmountChanged: ((String) -> Unit), val onFocusChanged: ((Boolean) -> Unit), + @StringRes override val headerResId: Int = R.string.exchange_send_view_header, ) : TransactionCardType - data class ReceiveCard( + data class ReadOnly( val highPriceImpact: String? = null, + @StringRes override val headerResId: Int = R.string.exchange_receive_view_header, ) : TransactionCardType } 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 cbc20fe451..5452767ef8 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 @@ -50,7 +50,7 @@ internal class StateBuilder( return SwapStateHolder( blockchainId = networkInfo.blockchainId, sendCardData = SwapCardState.SwapCardData( - type = TransactionCardType.SendCard(actions.onAmountChanged, actions.onAmountSelected), + type = TransactionCardType.Inputtable(actions.onAmountChanged, actions.onAmountSelected), amountEquivalent = null, amountTextFieldValue = null, token = null, @@ -64,7 +64,7 @@ internal class StateBuilder( isBalanceHidden = true, ), receiveCardData = SwapCardState.SwapCardData( - type = TransactionCardType.ReceiveCard(), + type = TransactionCardType.ReadOnly(), amountEquivalent = null, tokenIconUrl = "", tokenCurrency = "", @@ -98,7 +98,9 @@ internal class StateBuilder( if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), + type = TransactionCardType.ReadOnly( + headerResId = R.string.exchange_send_view_header + ), amountTextFieldValue = TextFieldValue( text = "0", ), @@ -114,7 +116,7 @@ internal class StateBuilder( isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.Empty( - type = TransactionCardType.ReceiveCard(), + type = TransactionCardType.ReadOnly(), amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", amountTextFieldValue = TextFieldValue( text = "0", @@ -156,7 +158,7 @@ internal class StateBuilder( if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = null, token = uiStateHolder.sendCardData.token, @@ -170,7 +172,7 @@ internal class StateBuilder( isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.SwapCardData( - type = TransactionCardType.ReceiveCard(), + type = TransactionCardType.ReadOnly(), amountTextFieldValue = null, amountEquivalent = null, token = uiStateHolder.receiveCardData.token, @@ -220,7 +222,7 @@ internal class StateBuilder( val toCurrencyStatus = quoteModel.toTokenInfo.cryptoCurrencyStatus return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = getFormattedFiatAmount(quoteModel.fromTokenInfo.amountFiat), token = fromCurrencyStatus, @@ -234,7 +236,7 @@ internal class StateBuilder( isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.SwapCardData( - type = TransactionCardType.ReceiveCard(), + type = TransactionCardType.ReadOnly(), amountTextFieldValue = TextFieldValue(quoteModel.toTokenInfo.tokenAmount.formatToUIRepresentation()), amountEquivalent = getFormattedFiatAmount(quoteModel.toTokenInfo.amountFiat), token = toCurrencyStatus, @@ -419,7 +421,7 @@ internal class StateBuilder( ) val receiveCardData = toToken?.let { SwapCardState.SwapCardData( - type = TransactionCardType.ReceiveCard(), + type = TransactionCardType.ReadOnly(), amountTextFieldValue = TextFieldValue( text = "0", ), @@ -435,7 +437,7 @@ internal class StateBuilder( isBalanceHidden = isBalanceHiddenProvider(), ) } ?: SwapCardState.Empty( - type = TransactionCardType.ReceiveCard(), + type = TransactionCardType.ReadOnly(), amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", amountTextFieldValue = TextFieldValue( text = "0", @@ -533,7 +535,7 @@ internal class StateBuilder( if (uiStateHolder.receiveCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.SendCard), + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), amountTextFieldValue = uiStateHolder.sendCardData.amountTextFieldValue, amountEquivalent = emptyAmountState.zeroAmountEquivalent, token = uiStateHolder.sendCardData.token, @@ -547,7 +549,7 @@ internal class StateBuilder( isBalanceHidden = isBalanceHiddenProvider(), ), receiveCardData = SwapCardState.SwapCardData( - type = TransactionCardType.ReceiveCard(), + type = TransactionCardType.ReadOnly(), amountTextFieldValue = TextFieldValue("0"), amountEquivalent = emptyAmountState.zeroAmountEquivalent, token = uiStateHolder.receiveCardData.token, 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 1e0b3f5944..b211130172 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 @@ -1,6 +1,5 @@ package com.tangem.feature.swap.ui -import androidx.compose.animation.* import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -429,7 +428,7 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U // region preview private val sendCard = SwapCardState.SwapCardData( - type = TransactionCardType.SendCard({}) {}, + type = TransactionCardType.Inputtable({}, {}), amountTextFieldValue = TextFieldValue(), amountEquivalent = "1 000 000", tokenIconUrl = "", @@ -444,7 +443,7 @@ private val sendCard = SwapCardState.SwapCardData( ) private val receiveCard = SwapCardState.SwapCardData( - type = TransactionCardType.ReceiveCard(), + type = TransactionCardType.ReadOnly(), amountTextFieldValue = TextFieldValue(), amountEquivalent = "1 000 000", tokenIconUrl = "", diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 5b16375022..0aec67858b 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -182,10 +182,7 @@ private fun Header(type: TransactionCardType, balance: String, modifier: Modifie horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - val title = when (type) { - is TransactionCardType.ReceiveCard -> R.string.exchange_receive_view_header - is TransactionCardType.SendCard -> R.string.exchange_send_view_header - } + val title = type.headerResId Text( text = stringResource(id = title), color = TangemTheme.colors.text.tertiary, @@ -245,7 +242,7 @@ private fun Content( ) { val sumTextModifier = Modifier.defaultMinSize(minHeight = TangemTheme.dimens.size32) when (type) { - is TransactionCardType.ReceiveCard -> { + is TransactionCardType.ReadOnly -> { if (textFieldValue != null) { ResizableText( text = textFieldValue.text, @@ -263,7 +260,7 @@ private fun Content( ) } } - is TransactionCardType.SendCard -> { + is TransactionCardType.Inputtable -> { AutoSizeTextField( modifier = sumTextModifier, textFieldValue = textFieldValue ?: TextFieldValue(), @@ -276,7 +273,7 @@ private fun Content( SpacerH8() if (amountEquivalent != null) { - if (type is TransactionCardType.ReceiveCard && priceImpact !is PriceImpact.Empty) { + if (type is TransactionCardType.ReadOnly && priceImpact !is PriceImpact.Empty) { Row { Text( text = makePriceImpactBalanceWarning(amountEquivalent, priceImpact.getIntPercentValue()), @@ -485,7 +482,7 @@ private fun Preview_SwapMainCard_InDarkTheme() { @Composable private fun TransactionCardPreview() { TransactionCard( - type = TransactionCardType.SendCard({}) {}, + type = TransactionCardType.Inputtable({}, {}), amountEquivalent = "1 000 000", tokenIconUrl = "", tokenCurrency = "DAI", From a4beb2cafcf20e2978ddf65c9f684fb21f66e84e Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Dec 2023 19:48:11 +0300 Subject: [PATCH 061/102] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 6a3614624a..237bd3e4a9 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-419" +tangemBlockchainSdk = "release-app_5.4-423" #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 ^ From bd62089c128dff98b73fb7b733b4c39da3ead983 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Dec 2023 19:58:56 +0300 Subject: [PATCH 062/102] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../auth/DefaultExpressAuthProvider.kt | 2 +- .../network/auth/DefaultOneInchProvider.kt | 13 -- .../tangem/tap/network/auth/di/AuthModule.kt | 10 - .../api/express/TangemExpressApi.kt | 1 + .../models/response/ExchangeDataResponse.kt | 21 +- .../datasource/api/oneinch/OneInchApi.kt | 205 ------------------ .../api/oneinch/OneInchApiFactory.kt | 14 -- .../api/oneinch/OneInchErrorsHandler.kt | 35 --- .../oneinch/errors/OneIncResponseException.kt | 5 - .../api/oneinch/models/AllowanceResponse.kt | 7 - .../oneinch/models/ApproveCalldataResponse.kt | 18 -- .../oneinch/models/ApproveSpenderResponse.kt | 12 - .../api/oneinch/models/PathViewDto.kt | 13 -- .../api/oneinch/models/ProtocolsResponse.kt | 27 --- .../api/oneinch/models/QuoteResponse.kt | 14 -- .../api/oneinch/models/StatusResponse.kt | 8 - .../api/oneinch/models/SwapErrorDto.kt | 42 ---- .../api/oneinch/models/SwapResponse.kt | 10 - .../api/oneinch/models/TokenOneInchDto.kt | 20 -- .../api/oneinch/models/TokensResponse.kt | 12 - .../api/oneinch/models/TransactionDto.kt | 23 -- .../datasource/config/ConfigManagerImpl.kt | 4 +- .../tangem/datasource/config/models/Config.kt | 4 +- .../datasource/config/models/ExpressModel.kt | 12 + .../datasource/config/models/JsonModels.kt | 9 +- .../crypto/DataSignatureVerifier.kt | 6 + .../crypto/Sha256SignatureVerifier.kt | 17 ++ .../tangem/datasource/di/OneInchApisModule.kt | 96 -------- .../tangem/datasource/di/SecurityModule.kt | 21 ++ features/swap/data/build.gradle.kts | 3 + ...sitoryImpl.kt => DefaultSwapRepository.kt} | 54 +++-- .../swap/converters/ExpressDataConverter.kt | 28 ++- .../tangem/feature/swap/di/SwapDataModule.kt | 19 +- .../feature/swap/domain/api/SwapRepository.kt | 13 -- .../feature/swap/domain/models/DataError.kt | 4 + .../swap/domain/models/ui/SwapState.kt | 1 - .../feature/swap/domain/SwapInteractorImpl.kt | 6 - .../tangem/feature/swap/ui/StateBuilder.kt | 2 +- 39 files changed, 149 insertions(+), 664 deletions(-) delete mode 100644 app/src/main/java/com/tangem/tap/network/auth/DefaultOneInchProvider.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApiFactory.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchErrorsHandler.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/errors/OneIncResponseException.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/AllowanceResponse.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveCalldataResponse.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveSpenderResponse.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/PathViewDto.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ProtocolsResponse.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/StatusResponse.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapErrorDto.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokenOneInchDto.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokensResponse.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TransactionDto.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/config/models/ExpressModel.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/crypto/DataSignatureVerifier.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt rename features/swap/data/src/main/java/com/tangem/feature/swap/{SwapRepositoryImpl.kt => DefaultSwapRepository.kt} (89%) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 6ef8ce45d1..0be5650f8f 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 6ef8ce45d183905b5752e2d33c1d8bf2f4bcace6 +Subproject commit 0be5650f8fbcdff22c23fe2e2c7d754c4275a6f6 diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt index 70d580f3c7..6cd26d788e 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/DefaultExpressAuthProvider.kt @@ -15,7 +15,7 @@ internal class DefaultExpressAuthProvider( private var uuid = AtomicReference(UUID.randomUUID()) override fun getApiKey(): String { - return configManager.config.tangemExpressApiKey + return configManager.config.express?.apiKey ?: "" } override fun getUserId(): String { diff --git a/app/src/main/java/com/tangem/tap/network/auth/DefaultOneInchProvider.kt b/app/src/main/java/com/tangem/tap/network/auth/DefaultOneInchProvider.kt deleted file mode 100644 index ade4f48a0f..0000000000 --- a/app/src/main/java/com/tangem/tap/network/auth/DefaultOneInchProvider.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.tap.network.auth - -import com.tangem.datasource.config.ConfigManager -import com.tangem.lib.auth.AuthBearerProvider - -internal class DefaultOneInchProvider( - private val configManager: ConfigManager, -) : AuthBearerProvider { - - override fun getApiKey(): String { - return configManager.config.oneInchApiKey - } -} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt index d59b3f4792..fa9aa920a1 100644 --- a/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt +++ b/app/src/main/java/com/tangem/tap/network/auth/di/AuthModule.kt @@ -2,12 +2,10 @@ package com.tangem.tap.network.auth.di import com.tangem.datasource.config.ConfigManager import com.tangem.datasource.local.userwallet.UserWalletsStore -import com.tangem.lib.auth.AuthBearerProvider import com.tangem.lib.auth.AuthProvider import com.tangem.lib.auth.ExpressAuthProvider import com.tangem.tap.network.auth.DefaultAuthProvider import com.tangem.tap.network.auth.DefaultExpressAuthProvider -import com.tangem.tap.network.auth.DefaultOneInchProvider import com.tangem.tap.proxy.AppStateHolder import dagger.Module import dagger.Provides @@ -36,12 +34,4 @@ class AuthModule { configManager = configManager, ) } - - @Provides - @Singleton - fun provideOneInchAuthProvider(configManager: ConfigManager): AuthBearerProvider { - return DefaultOneInchProvider( - configManager = configManager, - ) - } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt index b78d485fa6..61c2a6c25a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/TangemExpressApi.kt @@ -49,6 +49,7 @@ interface TangemExpressApi { @Query("providerId") providerId: String, @Query("rateType") rateType: String, @Query("toAddress") toAddress: String, + @Query("requestId") requestId: String, ): ApiResponse @GET("exchange-status") diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index b44fa2a019..40c3483f16 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -3,6 +3,10 @@ package com.tangem.datasource.api.express.models.response import com.squareup.moshi.Json import java.math.BigDecimal +data class ExchangeDataResponseWithTxDetails( + val dataResponse: ExchangeDataResponse, + val txDetails: TxDetails, +) data class ExchangeDataResponse( @Json(name = "fromAmount") val fromAmount: String, @@ -16,12 +20,23 @@ data class ExchangeDataResponse( @Json(name = "toDecimals") val toDecimals: Int, - @Json(name = "txType") - val txType: TxType, - @Json(name = "txId") val txId: String, // inner tangem-express transaction id + @Json(name = "txDetailsJson") + val txDetailsJson: String, + + @Json(name = "signature") + val signature: String, +) + +data class TxDetails( + @Json(name = "requestId") + val requestId: String, + + @Json(name = "txType") + val txType: TxType, + @Json(name = "txFrom") val txFrom: String?, // account for debiting tokens (same as toAddress) if DEX, null if CEX diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt deleted file mode 100644 index cf950a0f66..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApi.kt +++ /dev/null @@ -1,205 +0,0 @@ -package com.tangem.datasource.api.oneinch - -import com.tangem.datasource.api.oneinch.models.AllowanceResponse -import com.tangem.datasource.api.oneinch.models.ApproveCalldataResponse -import com.tangem.datasource.api.oneinch.models.ApproveSpenderResponse -import com.tangem.datasource.api.oneinch.models.ProtocolsResponse -import com.tangem.datasource.api.oneinch.models.QuoteResponse -import com.tangem.datasource.api.oneinch.models.StatusResponse -import com.tangem.datasource.api.oneinch.models.SwapResponse -import com.tangem.datasource.api.oneinch.models.TokensResponse -import retrofit2.Response -import retrofit2.http.GET -import retrofit2.http.Query - -interface OneInchApi { - - /** - * Healthcheck return 200 if service is available - * - * @return [StatusResponse] - */ - @GET("healthcheck") - suspend fun healthcheck(): StatusResponse - - //region Approve - /** - * Address of the 1inch router that must be trusted to spend funds for the exchange - * - * @return [ApproveSpenderResponse] - */ - @GET("approve/spender") - suspend fun approveSpender(): ApproveSpenderResponse - - /** - * Generate data for calling the contract in order to allow the 1inch router to spend funds - * - * @param tokenAddress Token address you want to exchange - * @param amount The number of tokens that the 1inch router is allowed to spend. - * If not specified, it will be allowed to spend an infinite amount of tokens. - * - * @return [ApproveCalldataResponse] Transaction body to allow the exchange with the 1inch router - */ - @GET("approve/transaction") - suspend fun approveTransaction( - @Query("tokenAddress") tokenAddress: String, - @Query("amount") amount: String? = null, - ): ApproveCalldataResponse - - /** - * Get the number of tokens that the 1inch router is allowed to spend - * - * @param tokenAddress Token address you want to exchange - * @param walletAddress Wallet address for which you want to check - * - * @return [AllowanceResponse] - */ - @GET("approve/allowance") - suspend fun approveAllowance( - @Query("tokenAddress") tokenAddress: String, - @Query("walletAddress") walletAddress: String, - ): Response - //endregion Approve - - //region Info - /** - * List of tokens that are available for swap in the 1inch Aggregation protocol - * - * @return [TokensResponse] - */ - @GET("tokens") - suspend fun tokensAvailable(): TokensResponse - - /** - * List of liquidity sources that are available for routing in the 1inch Aggregation protocol - * - * @return - */ - @GET("liquidity-sources") - suspend fun liquiditySources(): ProtocolsResponse - //endregion Info - - //region Swap - /** - * Find the best quote to exchange via 1inch router - * - * @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE - * @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302 - * @param amount amount of a token to sell, set in minimal divisible units e.g.: - * 1.00 DAI set as 1000000000000000000 - * 51.03 USDC set as 51030000 - * - * @param protocols default: all - * @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress, - * the rest will be used as input for a swap - * Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap! - * - * @param gasLimit maximum amount of gas for a swap; - * @param connectorTokens token-connectors can be specified via this parameter. - * The more is set — the longer route estimation will take. - * If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap! - * - * @param complexityLevel maximum number of token-connectors to be used in a transaction. - * The more is used — the longer route estimation will take - * min: 0; max: 3; default: 2; !should be the same for quote and swap! - * - * @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap! - * @param parts limit maximum number of parts each main route parts can be split into; - * should be the same for a quote and swap - * default: 20; max: 100 - * - * @param gasPrice 1inch takes in account gas expenses to determine exchange route. - * It is important to use the same gas price on the quote and swap methods. - * Gas price set in wei: 12.5 GWEI set as 12500000000 - * default: fast from network - * - * @return [QuoteResponse] - */ - @GET("quote") - suspend fun quote( - @Query("src") fromTokenAddress: String, - @Query("dst") toTokenAddress: String, - @Query("amount") amount: String, - @Query("protocols") protocols: String? = null, - @Query("fee") fee: String? = null, - @Query("gasLimit") gasLimit: String? = null, - @Query("connectorTokens") connectorTokens: String? = null, - @Query("complexityLevel") complexityLevel: String? = null, - @Query("mainRouteParts") mainRouteParts: String? = null, - @Query("parts") parts: String? = null, - @Query("gasPrice") gasPrice: String? = null, - @Query("includeTokensInfo") includeTokensInfo: Boolean = true, - ): Response - - /** - * Generate data for calling the 1inch router for exchange - * - * @param fromTokenAddress Example : 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE - * @param toTokenAddress Example : 0x111111111117dc0aa78b770fa6a738034120c302 - * @param amount amount of a token to sell, set in minimal divisible units e.g.: - * 1.00 DAI set as 1000000000000000000 - * 51.03 USDC set as 51030000 - * - * @param fromAddress The address that calls the 1inch contract - * @param slippage limit of price slippage you are willing to accept in percentage, may be set with decimals. - * &slippage=0.5 means 0.5% slippage is acceptable. Low values increase chances that transaction will fail, - * high values increase chances of front running. min: 0; max: 50; - * - * @param protocols default: all - * @param destinationAddress Receiver of destination currency. default: fromAddress - * @param fee this percentage of fromTokenAddress token amount will be sent to referrerAddress, - * the rest will be used as input for a swap - * Min: 0; max: 3; Max: 0; max: 3; default: 0; !should be the same for quote and swap! - * - * @param permit https://eips.ethereum.org/EIPS/eip-2612 - * @param compatibilityMode Allows to build calldata without optimized routers - * @param burnChi If true, CHI will be burned from fromAddress to compensate gas. - * Check CHI balance and allowance before turning that on. CHI should be approved for the spender address - * - * @param connectorTokens token-connectors can be specified via this parameter. - * The more is set — the longer route estimation will take. - * If not set, default token-connectors will be usedmax: 5; !should be the same for quote and swap! - * - * @param complexityLevel maximum number of token-connectors to be used in a transaction. - * The more is used — the longer route estimation will take - * min: 0; max: 3; default: 2; !should be the same for quote and swap! - * - * @param mainRouteParts default: 10; max: 50 !should be the same for quote and swap! - * @param parts limit maximum number of parts each main route parts can be split into; - * should be the same for a quote and swap - * default: 20; max: 100 - * - * @param gasLimit maximum amount of gas for a swap; - * @param gasPrice 1inch takes in account gas expenses to determine exchange route. - * It is important to use the same gas price on the quote and swap methods. - * Gas price set in wei: 12.5 GWEI set as 12500000000 - * default: fast from network - * - * @return [SwapResponse] - */ - @GET("swap") - suspend fun swap( - @Query("src") fromTokenAddress: String, - @Query("dst") toTokenAddress: String, - @Query("amount") amount: String, - @Query("from") fromAddress: String, - @Query("slippage") slippage: Int, - @Query("protocols") protocols: String? = null, - @Query("receiver") destinationAddress: String? = null, - @Query("referrer") referrerAddress: String? = null, - @Query("fee") fee: String? = null, - @Query("disableEstimate") disableEstimate: Boolean? = null, - @Query("permit") permit: String? = null, - @Query("compatibility") compatibilityMode: Boolean? = null, - @Query("burnChi") burnChi: Boolean? = null, - @Query("allowPartialFill") allowPartialFill: Boolean? = null, - @Query("parts") parts: String? = null, - @Query("mainRouteParts") mainRouteParts: String? = null, - @Query("connectorTokens") connectorTokens: String? = null, - @Query("complexityLevel") complexityLevel: String? = null, - @Query("gasLimit") gasLimit: String? = null, - @Query("gasPrice") gasPrice: String? = null, - @Query("includeTokensInfo") includeTokensInfo: Boolean = true, - ): Response - //endregion Swap -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApiFactory.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApiFactory.kt deleted file mode 100644 index da21f86558..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchApiFactory.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.datasource.api.oneinch - -class OneInchApiFactory { - - private val oneInchApiMap = mutableMapOf() - - fun putApi(networkId: String, api: OneInchApi) { - oneInchApiMap[networkId] = api - } - - fun getApi(networkId: String): OneInchApi { - return oneInchApiMap[networkId] ?: error("no api found for networkId $networkId") - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchErrorsHandler.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchErrorsHandler.kt deleted file mode 100644 index 4fdc784ecb..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/OneInchErrorsHandler.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.tangem.datasource.api.oneinch - -import com.squareup.moshi.Moshi -import com.tangem.datasource.api.oneinch.errors.OneIncResponseException -import com.tangem.datasource.api.oneinch.models.SwapErrorDto -import com.tangem.datasource.di.NetworkMoshi -import retrofit2.HttpException -import retrofit2.Response -import javax.inject.Inject - -class OneInchErrorsHandler @Inject constructor(@NetworkMoshi moshi: Moshi) { - - private val errorMoshiAdapter = moshi.adapter(SwapErrorDto::class.java) - - @Throws(OneIncResponseException::class) - fun handleOneInchResponse(response: Response): T { - return if (response.isSuccessful) { - response.body() ?: error("response body is null") - } else { - when (response.code()) { - HTTP_CODE_400 -> { - val swapErrorDto = errorMoshiAdapter.fromJson(response.errorBody()?.string() ?: "") - throw OneIncResponseException(swapErrorDto ?: throw HttpException(response)) - } - else -> { - throw HttpException(response) - } - } - } - } - - companion object { - private const val HTTP_CODE_400 = 400 - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/errors/OneIncResponseException.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/errors/OneIncResponseException.kt deleted file mode 100644 index 94c94f6e32..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/errors/OneIncResponseException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.tangem.datasource.api.oneinch.errors - -import com.tangem.datasource.api.oneinch.models.SwapErrorDto - -class OneIncResponseException(val data: SwapErrorDto) : Exception() \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/AllowanceResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/AllowanceResponse.kt deleted file mode 100644 index 16956f907e..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/AllowanceResponse.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -data class AllowanceResponse( - @Json(name = "allowance") val allowance: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveCalldataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveCalldataResponse.kt deleted file mode 100644 index f57fab94fd..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveCalldataResponse.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Approve calldata response - * - * @property data The encoded data to call the approve method on the swapped token contract - * @property gasPrice Gas price for fast transaction processing - * @property toAddress Token address that will be allowed to exchange through 1inch router - * @property value Native token value in WEI (for approve is always 0) - */ -data class ApproveCalldataResponse( - @Json(name = "data") val data: String, - @Json(name = "gasPrice") val gasPrice: String, - @Json(name = "to") val toAddress: String, - @Json(name = "value") val value: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveSpenderResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveSpenderResponse.kt deleted file mode 100644 index 0ee9d5b2ac..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ApproveSpenderResponse.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Approve spender response - * - * @property address Address of the 1inch router that must be trusted to spend funds for the exchange - */ -data class ApproveSpenderResponse( - @Json(name = "address") val address: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/PathViewDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/PathViewDto.kt deleted file mode 100644 index 03c23155ff..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/PathViewDto.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Path view dto - */ -data class PathViewDto( - @Json(name = "name") val name: String, - @Json(name = "part") val part: Int, - @Json(name = "fromTokenAddress") val fromTokenAddress: String, - @Json(name = "toTokenAddress") val toTokenAddress: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ProtocolsResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ProtocolsResponse.kt deleted file mode 100644 index b083cd1ce8..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/ProtocolsResponse.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Protocols response - * - * @property protocols List of protocols that are available for routing in the 1inch Aggregation protocol - */ -data class ProtocolsResponse( - @Json(name = "protocols") val protocols: List, -) - -/** - * Protocol image - * - * @property id Protocol id - * @property title Protocol title - * @property image Protocol logo image - * @property imageColor Protocol logo image in color - */ -data class ProtocolImageDto( - @Json(name = "id") val id: String, - @Json(name = "title") val title: String, - @Json(name = "img") val image: String, - @Json(name = "img_color") val imageColor: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt deleted file mode 100644 index 692f036f24..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/QuoteResponse.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Quote response - * - * @property toToken Destination token info - * @property toTokenAmount Expected amount of destination token - */ -data class QuoteResponse( - @Json(name = "toToken") val toToken: TokenOneInchDto, - @Json(name = "toAmount") val toTokenAmount: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/StatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/StatusResponse.kt deleted file mode 100644 index 576851a3b5..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/StatusResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -data class StatusResponse( - @Json(name = "status") val status: String, - @Json(name = "provider") val provider: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapErrorDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapErrorDto.kt deleted file mode 100644 index fe342de231..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapErrorDto.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Swap error dto - * - * One of the following errors: - * - * -Insufficient liquidity - * -Cannot estimate - * -You may not have enough ETH balance for gas fee - * -FromTokenAddress cannot be equals to toTokenAddress - * -Cannot estimate. Don't forget about miner fee. Try to leave the buffer of ETH for gas - * -Not enough balance - * -Not enough allowance - * - * @property statusCode HTTP code - * @property error Error code description - * @property description Error description (one of the following) - * @property requestId Request id - * @property meta Meta information - * @constructor Create empty Swap error dto - */ -data class SwapErrorDto( - @Json(name = "statusCode") val statusCode: Int, - @Json(name = "error") val error: String, - @Json(name = "description") val description: String, - @Json(name = "requestId") val requestId: String, - @Json(name = "meta") val meta: List, -) - -/** - * Nest error meta - * - * @property type Type of field - * @property value Value of field - */ -data class NestErrorMeta( - @Json(name = "type") val type: String, - @Json(name = "value") val value: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt deleted file mode 100644 index bc1f668360..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/SwapResponse.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -data class SwapResponse( - @Json(name = "fromToken") val fromToken: TokenOneInchDto, - @Json(name = "toToken") val toToken: TokenOneInchDto, - @Json(name = "toAmount") val toTokenAmount: String, - @Json(name = "tx") val transaction: TransactionDto, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokenOneInchDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokenOneInchDto.kt deleted file mode 100644 index 0c06f15d84..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokenOneInchDto.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Token one inch - * - * @property symbol token symbol - * @property name token name - * @property address token address - * @property decimals token decimals - * @property logoURI token logo image url - */ -data class TokenOneInchDto( - @Json(name = "symbol") val symbol: String, - @Json(name = "name") val name: String, - @Json(name = "address") val address: String, - @Json(name = "decimals") val decimals: Int, - @Json(name = "logoURI") val logoURI: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokensResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokensResponse.kt deleted file mode 100644 index ca1b369949..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TokensResponse.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Tokens response - * - * @property tokens List of supported tokens - */ -data class TokensResponse( - @Json(name = "tokens") val tokens: Map, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TransactionDto.kt b/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TransactionDto.kt deleted file mode 100644 index a32e827601..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/api/oneinch/models/TransactionDto.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.tangem.datasource.api.oneinch.models - -import com.squareup.moshi.Json - -/** - * Transaction dto - * - * @property fromAddress transactions will be sent from this address - * @property toAddress transactions will be sent to our(1inch) contract address - * @property data The encoded data to call the approve method on the swapped token contract - * @property value Native token value in WEI (for approve is always 0) - * @property gasPrice maximum amount of gas for a swap default: 11500000; max: 11500000 - * @property gas estimated amount of the gas limit, increase this value by 25% - * @constructor Create empty Transaction dto - */ -data class TransactionDto( - @Json(name = "from") val fromAddress: String, - @Json(name = "to") val toAddress: String, - @Json(name = "data") val data: String, - @Json(name = "value") val value: String, - @Json(name = "gasPrice") val gasPrice: String, - @Json(name = "gas") val gas: String, -) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt index 3771cc18a6..4ac0edf918 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/ConfigManagerImpl.kt @@ -103,11 +103,9 @@ internal class ConfigManagerImpl @Inject constructor() : ConfigManager { amplitudeApiKey = configValues.amplitudeApiKey, shopify = configValues.shopifyShop, sprinklr = configValues.sprinklr, - swapReferrerAccount = configValues.swapReferrerAccount, walletConnectProjectId = configValues.walletConnectProjectId, tangemComAuthorization = configValues.tangemComAuthorization, - tangemExpressApiKey = configValues.tangemExpressApiKey, - oneInchApiKey = configValues.oneInchApiKey, + express = configValues.express, ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt index 5ddc1a7187..8490f44237 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/Config.kt @@ -16,9 +16,7 @@ data class Config( val isCreatingTwinCardsAllowed: Boolean = false, val shopify: ShopifyShop? = null, val sprinklr: SprinklrConfig? = null, - val swapReferrerAccount: SwapReferrerAccount? = null, val walletConnectProjectId: String = "", val tangemComAuthorization: String? = null, - val tangemExpressApiKey: String = "", - val oneInchApiKey: String = "", + val express: ExpressModel? = null, ) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/ExpressModel.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/ExpressModel.kt new file mode 100644 index 0000000000..19f97e4d8e --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/ExpressModel.kt @@ -0,0 +1,12 @@ +package com.tangem.datasource.config.models + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ExpressModel( + @Json(name = "apiKey") + val apiKey: String, + @Json(name = "signVerifierPublicKey") + val signVerifierPublicKey: String, +) \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt index d63f6527da..e953742589 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/config/models/JsonModels.kt @@ -35,14 +35,12 @@ class ConfigValueModel( val sprinklr: SprinklrConfig?, val tronGridApiKey: String, val amplitudeApiKey: String, - val swapReferrerAccount: SwapReferrerAccount?, val kaspaSecondaryApiUrl: String, val walletConnectProjectId: String, val tangemComAuthorization: String?, val chiaFireAcademyApiKey: String?, val chiaTangemApiKey: String?, - val tangemExpressApiKey: String, - val oneInchApiKey: String, + val express: ExpressModel?, ) @JsonClass(generateAdapter = true) @@ -84,11 +82,6 @@ data class AppsFlyer( val appsFlyerAppID: String, ) -data class SwapReferrerAccount( - val address: String, - val fee: String, -) - class ConfigModel( val features: FeatureModel?, val configValues: ConfigValueModel?, diff --git a/core/datasource/src/main/java/com/tangem/datasource/crypto/DataSignatureVerifier.kt b/core/datasource/src/main/java/com/tangem/datasource/crypto/DataSignatureVerifier.kt new file mode 100644 index 0000000000..1c3a8b68df --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/crypto/DataSignatureVerifier.kt @@ -0,0 +1,6 @@ +package com.tangem.datasource.crypto + +interface DataSignatureVerifier { + + fun verifySignature(signature: String, data: String): Boolean +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt new file mode 100644 index 0000000000..dc2c197c50 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/crypto/Sha256SignatureVerifier.kt @@ -0,0 +1,17 @@ +package com.tangem.datasource.crypto + +import com.tangem.common.extensions.hexToBytes +import com.tangem.crypto.CryptoUtils +import com.tangem.datasource.config.ConfigManager + +internal class Sha256SignatureVerifier(private val configManager: ConfigManager) : DataSignatureVerifier { + + override fun verifySignature(signature: String, data: String): Boolean { + val pubKey = configManager.config.express?.signVerifierPublicKey ?: return false + return CryptoUtils.verify( + publicKey = pubKey.hexToBytes().takeLast(n = 65).toByteArray(), + message = data.toByteArray(), + signature = signature.hexToBytes(), + ) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt deleted file mode 100644 index c1ffbe4e87..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/di/OneInchApisModule.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.tangem.datasource.di - -import android.content.Context -import com.squareup.moshi.Moshi -import com.tangem.datasource.api.oneinch.OneInchApi -import com.tangem.datasource.api.oneinch.OneInchApiFactory -import com.tangem.datasource.utils.RequestHeader -import com.tangem.datasource.utils.addHeaders -import com.tangem.datasource.utils.addLoggers -import com.tangem.lib.auth.AuthBearerProvider -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext -import dagger.hilt.components.SingletonComponent -import okhttp3.OkHttpClient -import retrofit2.Retrofit -import retrofit2.converter.moshi.MoshiConverterFactory -import javax.inject.Singleton - -@Module -@InstallIn(SingletonComponent::class) -class OneInchApisModule { - - @Provides - @Singleton - fun provideOneInchApiFactory( - @NetworkMoshi moshi: Moshi, - @ApplicationContext context: Context, - auth1Inch: AuthBearerProvider, - ): OneInchApiFactory { - val networks = mapOf( - ETH_NETWORK to ONE_INCH_ETH_PATH, - BSC_NETWORK to ONE_INCH_BSC_PATH, - POLYGON_NETWORK to ONE_INCH_POLYGON_PATH, - OPTIMISM_NETWORK to ONE_INCH_OPTIMISM_PATH, - ARBITRUM_NETWORK to ONE_INCH_ARBITRUM_PATH, - GNOSIS_NETWORK to ONE_INCH_GNOSIS_PATH, - AVALANCHE_NETWORK to ONE_INCH_AVALANCHE_PATH, - FANTOM_NETWORK to ONE_INCH_FANTOM_PATH, - ) - - val apiFactory = OneInchApiFactory() - - for ((network, path) in networks) { - apiFactory.putApi( - networkId = network, - api = createOneInchApiWithUrl("$ONE_INCH_BASE_URL$path", moshi, context, auth1Inch), - ) - } - - return apiFactory - } - - private fun createOneInchApiWithUrl( - url: String, - moshi: Moshi, - context: Context, - auth1Inch: AuthBearerProvider, - ): OneInchApi { - return Retrofit.Builder() - .addConverterFactory( - MoshiConverterFactory.create(moshi), - ) - .baseUrl(url) - .client( - OkHttpClient.Builder() - .addHeaders(RequestHeader.AuthBearerHeader(auth1Inch)) - .addLoggers(context) - .build(), - ) - .build() - .create(OneInchApi::class.java) - } - - companion object { - private const val ONE_INCH_BASE_URL = "https://api.1inch.dev/swap/v5.2/" - private const val ONE_INCH_ETH_PATH = "1/" - private const val ONE_INCH_BSC_PATH = "56/" - private const val ONE_INCH_POLYGON_PATH = "137/" - private const val ONE_INCH_OPTIMISM_PATH = "10/" - private const val ONE_INCH_ARBITRUM_PATH = "42161/" - private const val ONE_INCH_GNOSIS_PATH = "100/" - private const val ONE_INCH_AVALANCHE_PATH = "43114/" - private const val ONE_INCH_FANTOM_PATH = "250/" - - private const val ETH_NETWORK = "ethereum" - private const val BSC_NETWORK = "binance-smart-chain" - private const val POLYGON_NETWORK = "polygon-pos" - private const val OPTIMISM_NETWORK = "optimistic-ethereum" - private const val ARBITRUM_NETWORK = "arbitrum-one" - private const val GNOSIS_NETWORK = "xdai" - private const val AVALANCHE_NETWORK = "avalanche" - private const val FANTOM_NETWORK = "fantom" - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt new file mode 100644 index 0000000000..b5c2f07c49 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/di/SecurityModule.kt @@ -0,0 +1,21 @@ +package com.tangem.datasource.di + +import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.crypto.DataSignatureVerifier +import com.tangem.datasource.crypto.Sha256SignatureVerifier +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +internal object SecurityModule { + + @Provides + @Singleton + fun provideDataSignatureVerifier(configManager: ConfigManager): DataSignatureVerifier { + return Sha256SignatureVerifier(configManager) + } +} \ No newline at end of file diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index a2b37ee319..12693598d2 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -37,6 +37,9 @@ dependencies { /** Tangem SDKs */ implementation(deps.tangem.blockchain) + /** Others */ + implementation(deps.timber) + /** DI */ implementation(deps.hilt.android) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt similarity index 89% rename from features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt rename to features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index c95e9b4ddf..7a4c0df381 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/SwapRepositoryImpl.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -5,6 +5,7 @@ import arrow.core.left import arrow.core.raise.catch import arrow.core.raise.either import arrow.core.right +import com.squareup.moshi.Moshi import com.tangem.blockchain.common.* import com.tangem.blockchain.extensions.Result import com.tangem.data.tokens.utils.CryptoCurrencyFactory @@ -13,12 +14,12 @@ import com.tangem.datasource.api.common.response.ApiResponseError import com.tangem.datasource.api.common.response.getOrThrow import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.request.PairsRequestBody +import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails import com.tangem.datasource.api.express.models.response.SwapPair import com.tangem.datasource.api.express.models.response.SwapPairsWithProviders -import com.tangem.datasource.api.oneinch.OneInchApi -import com.tangem.datasource.api.oneinch.OneInchApiFactory +import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency @@ -34,20 +35,23 @@ import com.tangem.feature.swap.domain.models.domain.* import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.coroutines.async import kotlinx.coroutines.withContext +import timber.log.Timber +import java.io.IOException import java.math.BigDecimal +import java.util.UUID import javax.inject.Inject import com.tangem.datasource.api.express.models.request.LeastTokenInfo as NetworkLeastTokenInfo @Suppress("LongParameterList", "LargeClass") -internal class SwapRepositoryImpl @Inject constructor( +internal class DefaultSwapRepository @Inject constructor( private val tangemTechApi: TangemTechApi, private val tangemExpressApi: TangemExpressApi, - private val oneInchApiFactory: OneInchApiFactory, private val coroutineDispatcher: CoroutineDispatcherProvider, - private val configManager: ConfigManager, private val walletManagersFacade: WalletManagersFacade, private val walletsStateHolder: WalletsStateHolder, private val errorsDataConverter: ErrorsDataConverter, + private val dataSignatureVerifier: DataSignatureVerifier, + moshi: Moshi, ) : SwapRepository { private val tokensConverter = TokensConverter() @@ -56,6 +60,7 @@ internal class SwapRepositoryImpl @Inject constructor( private val swapPairInfoConverter = SwapPairInfoConverter() private val cryptoCurrencyFactory = CryptoCurrencyFactory() private val exchangeStatusConverter = ExchangeStatusConverter() + private val txDetailsMoshiAdapter = moshi.adapter(TxDetails::class.java) override suspend fun getPairs( initialCurrency: LeastTokenInfo, @@ -253,12 +258,6 @@ internal class SwapRepositoryImpl @Inject constructor( } } - override suspend fun addressForTrust(networkId: String): String { - return withContext(coroutineDispatcher.io) { - getOneInchApi(networkId).approveSpender().address - } - } - override suspend fun getExchangeData( fromContractAddress: String, fromNetwork: String, @@ -273,6 +272,7 @@ internal class SwapRepositoryImpl @Inject constructor( ): Either { return withContext(coroutineDispatcher.io) { try { + val requestId = UUID.randomUUID().toString() val response = tangemExpressApi.getExchangeData( fromContractAddress = fromContractAddress, fromNetwork = fromNetwork, @@ -284,16 +284,36 @@ internal class SwapRepositoryImpl @Inject constructor( providerId = providerId, rateType = rateType.name.lowercase(), toAddress = toAddress, + requestId = requestId, ).getOrThrow() - expressDataConverter.convert(response).right() + if (dataSignatureVerifier.verifySignature(response.signature, response.txDetailsJson)) { + val txDetails = parseTxDetails(response.txDetailsJson) + ?: return@withContext DataError.UnknownError.left() + if (txDetails.requestId != requestId) { + return@withContext DataError.InvalidRequestIdError().left() + } + expressDataConverter.convert( + ExchangeDataResponseWithTxDetails( + dataResponse = response, + txDetails = txDetails, + ), + ).right() + } else { + DataError.InvalidSignatureError().left() + } } catch (ex: Exception) { getDataError(ex).left() } } } - override fun getTangemFee(): Double { - return configManager.config.swapReferrerAccount?.fee?.toDoubleOrNull() ?: 0.0 + private fun parseTxDetails(txDetailsJson: String): TxDetails? { + return try { + txDetailsMoshiAdapter.fromJson(txDetailsJson) + } catch (e: IOException) { + Timber.e(e, "error parsing txDetailsJson") + null + } } override suspend fun getAllowance( @@ -367,10 +387,6 @@ internal class SwapRepositoryImpl @Inject constructor( ) } - private fun getOneInchApi(networkId: String): OneInchApi { - return oneInchApiFactory.getApi(networkId) - } - override fun getNativeTokenForNetwork(networkId: String): CryptoCurrency { val blockchain = requireNotNull(Blockchain.fromNetworkId(networkId)) { "blockchain not found" } diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt index 86a75abcb7..d260ec0ef8 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExpressDataConverter.kt @@ -1,36 +1,42 @@ package com.tangem.feature.swap.converters import com.tangem.datasource.api.express.models.response.ExchangeDataResponse +import com.tangem.datasource.api.express.models.response.ExchangeDataResponseWithTxDetails +import com.tangem.datasource.api.express.models.response.TxDetails import com.tangem.datasource.api.express.models.response.TxType import com.tangem.feature.swap.domain.models.createFromAmountWithOffset import com.tangem.feature.swap.domain.models.domain.ExpressTransactionModel import com.tangem.feature.swap.domain.models.domain.SwapDataModel import com.tangem.utils.converter.Converter -class ExpressDataConverter : Converter { +internal class ExpressDataConverter : Converter { - override fun convert(value: ExchangeDataResponse): SwapDataModel { + override fun convert(value: ExchangeDataResponseWithTxDetails): SwapDataModel { + val data = value.dataResponse return SwapDataModel( - toTokenAmount = createFromAmountWithOffset(value.toAmount, value.toDecimals), - transaction = convertTransaction(value), + toTokenAmount = createFromAmountWithOffset(data.toAmount, data.toDecimals), + transaction = convertTransaction(value.txDetails, data), ) } - private fun convertTransaction(transactionDto: ExchangeDataResponse): ExpressTransactionModel { + private fun convertTransaction( + transactionDto: TxDetails, + dataResponse: ExchangeDataResponse, + ): ExpressTransactionModel { return if (transactionDto.txType == TxType.SWAP) { ExpressTransactionModel.DEX( - fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals), - toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals), - txId = transactionDto.txId, + fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals), + toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals), + txId = dataResponse.txId, txTo = transactionDto.txTo, txFrom = requireNotNull(transactionDto.txFrom), txData = requireNotNull(transactionDto.txData), ) } else { ExpressTransactionModel.CEX( - fromAmount = createFromAmountWithOffset(transactionDto.fromAmount, transactionDto.fromDecimals), - toAmount = createFromAmountWithOffset(transactionDto.toAmount, transactionDto.toDecimals), - txId = transactionDto.txId, + fromAmount = createFromAmountWithOffset(dataResponse.fromAmount, dataResponse.fromDecimals), + toAmount = createFromAmountWithOffset(dataResponse.toAmount, dataResponse.toDecimals), + txId = dataResponse.txId, txTo = transactionDto.txTo, externalTxId = requireNotNull(transactionDto.externalTxId), externalTxUrl = requireNotNull(transactionDto.externalTxUrl), diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt index 83ddab6b62..f48ab71c3a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/di/SwapDataModule.kt @@ -3,18 +3,17 @@ package com.tangem.feature.swap.di import com.squareup.moshi.Moshi import com.tangem.datasource.api.express.TangemExpressApi import com.tangem.datasource.api.express.models.response.ExpressErrorResponse -import com.tangem.datasource.api.oneinch.OneInchApiFactory import com.tangem.datasource.api.tangemTech.TangemTechApi -import com.tangem.datasource.config.ConfigManager +import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.datasource.di.NetworkMoshi import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder -import com.tangem.feature.swap.SwapRepositoryImpl -import com.tangem.feature.swap.converters.ErrorsDataConverter import com.tangem.feature.swap.DefaultSwapTransactionRepository -import com.tangem.feature.swap.domain.api.SwapRepository +import com.tangem.feature.swap.DefaultSwapRepository +import com.tangem.feature.swap.converters.ErrorsDataConverter import com.tangem.feature.swap.domain.SwapTransactionRepository +import com.tangem.feature.swap.domain.api.SwapRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -31,22 +30,22 @@ internal class SwapDataModule { internal fun provideSwapRepository( tangemTechApi: TangemTechApi, tangemExpressApi: TangemExpressApi, - oneInchApiFactory: OneInchApiFactory, coroutineDispatcher: CoroutineDispatcherProvider, - configManager: ConfigManager, + dataSignature: DataSignatureVerifier, walletManagerFacade: WalletManagersFacade, walletsStateHolder: WalletsStateHolder, errorsDataConverter: ErrorsDataConverter, + @NetworkMoshi moshi: Moshi, ): SwapRepository { - return SwapRepositoryImpl( + return DefaultSwapRepository( tangemTechApi = tangemTechApi, tangemExpressApi = tangemExpressApi, - oneInchApiFactory = oneInchApiFactory, coroutineDispatcher = coroutineDispatcher, - configManager = configManager, walletManagersFacade = walletManagerFacade, walletsStateHolder = walletsStateHolder, errorsDataConverter = errorsDataConverter, + dataSignatureVerifier = dataSignature, + moshi = moshi, ) } diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index c1310c961c..e0182e8a5e 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -33,19 +33,6 @@ interface SwapRepository { rateType: RateType, ): Either - /** - * Returns address of 1inch router that must be trusted - * - * @return address - */ - suspend fun addressForTrust(networkId: String): String - - /** - * Returns a tangem fee for swap in percents - * Example: 0.35% - */ - fun getTangemFee(): Double - @Suppress("LongParameterList") @Throws(IllegalStateException::class) suspend fun getAllowance( diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index 829cb5e7e4..9c0ca6b268 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -32,6 +32,10 @@ sealed class DataError { data class UnknownErrorWithCode(override val code: Int) : DataError() + data class InvalidSignatureError(override val code: Int = 990) : DataError() + + data class InvalidRequestIdError(override val code: Int = 991) : DataError() + object UnknownError : DataError() { override val code: Int = -1 } diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index 6711df346c..ae50b2dd38 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -25,7 +25,6 @@ sealed interface SwapState { val permissionState: PermissionDataState = PermissionDataState.Empty, val swapDataModel: SwapDataModel? = null, val txFee: TxFeeState, - val tangemFee: Double, ) : SwapState data class EmptyAmountState( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 34c9756d26..19a88c1d4a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -653,11 +653,6 @@ internal class SwapInteractorImpl @Inject constructor( return repository.getNativeTokenForNetwork(networkId) } - @Deprecated("used in old swap mechanism") - private fun getTangemFee(): Double { - return repository.getTangemFee() - } - private fun getTokenDecimals(token: CryptoCurrency): Int { return if (token is CryptoCurrency.Token) { token.decimals @@ -1013,7 +1008,6 @@ internal class SwapInteractorImpl @Inject constructor( ), networkCurrency = userWalletManager.getNetworkCurrency(networkId), swapDataModel = swapData, - tangemFee = getTangemFee(), txFee = txFeeState, ) } 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 5452767ef8..f02e5006fe 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 @@ -99,7 +99,7 @@ internal class StateBuilder( return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( type = TransactionCardType.ReadOnly( - headerResId = R.string.exchange_send_view_header + headerResId = R.string.exchange_send_view_header, ), amountTextFieldValue = TextFieldValue( text = "0", From 1b5518e8b236452eeae5068404e9eecb2cb3ddb5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 21 Dec 2023 20:06:20 +0200 Subject: [PATCH 063/102] Updated on 2026-08-14 --- .../java/com/tangem/tap/di/domain/MnemonicModule.kt | 6 +++--- .../tap/di/domain/WalletManagersFacadeModule.kt | 3 +++ .../tap/features/demo/DemoTransactionSender.kt | 2 +- .../walletmanager/DefaultWalletManagersFacade.kt | 12 +++++++++++- .../onboarding/data/DefaultMnemonicRepository.kt | 2 +- 5 files changed, 19 insertions(+), 6 deletions(-) rename features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt => app/src/main/java/com/tangem/tap/di/domain/MnemonicModule.kt (77%) diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MnemonicModule.kt similarity index 77% rename from features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt rename to app/src/main/java/com/tangem/tap/di/domain/MnemonicModule.kt index a853bf6c77..8f62a7e255 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MnemonicModule.kt @@ -1,4 +1,4 @@ -package com.tangem.feature.onboarding.data.di +package com.tangem.tap.di.domain import android.content.Context import com.tangem.crypto.bip39.Wordlist @@ -14,11 +14,11 @@ import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) -class OnboardingDataModule { +class MnemonicModule { @Provides @Singleton - fun provideSeedPhraseSdkRepository(@ApplicationContext context: Context): MnemonicRepository { + fun provideMnemonicRepository(@ApplicationContext context: Context): MnemonicRepository { return DefaultMnemonicRepository(Wordlist.getWordlist(context)) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt index 63afaaeaf2..02a52ce342 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/WalletManagersFacadeModule.kt @@ -8,6 +8,7 @@ import com.tangem.datasource.local.userwallet.UserWalletsStore import com.tangem.datasource.local.walletmanager.WalletManagersStore import com.tangem.domain.walletmanager.DefaultWalletManagersFacade import com.tangem.domain.walletmanager.WalletManagersFacade +import com.tangem.feature.onboarding.data.MnemonicRepository import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -24,6 +25,7 @@ internal object WalletManagersFacadeModule { walletManagersStore: WalletManagersStore, userWalletsStore: UserWalletsStore, configManager: ConfigManager, + mnemonicRepository: MnemonicRepository, assetReader: AssetReader, @SdkMoshi moshi: Moshi, ): WalletManagersFacade { @@ -33,6 +35,7 @@ internal object WalletManagersFacadeModule { configManager = configManager, assetReader = assetReader, moshi = moshi, + mnemonic = mnemonicRepository.generateDefaultMnemonic(), ) } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt index 17714e011b..faecf5aac1 100644 --- a/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt +++ b/app/src/main/java/com/tangem/tap/features/demo/DemoTransactionSender.kt @@ -21,7 +21,7 @@ class DemoTransactionSender(private val walletManager: WalletManager) : Transact ) } - override suspend fun estimateFee(amount: Amount): Result { + override suspend fun estimateFee(amount: Amount, destination: String): Result { return getFee(amount, walletManager.wallet.address) } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index cdffab603a..03cdd7a611 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -10,11 +10,13 @@ import com.tangem.blockchain.blockchains.solana.RentProvider import com.tangem.blockchain.common.* import com.tangem.blockchain.common.address.Address import com.tangem.blockchain.common.address.AddressType +import com.tangem.blockchain.common.address.EstimationFeeAddressFactory import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.common.transaction.TransactionFee import com.tangem.blockchain.common.txhistory.TransactionHistoryRequest import com.tangem.blockchain.extensions.Result import com.tangem.blockchain.extensions.SimpleResult +import com.tangem.crypto.bip39.Mnemonic import com.tangem.crypto.hdWallet.DerivationPath import com.tangem.datasource.asset.AssetReader import com.tangem.datasource.config.ConfigManager @@ -45,6 +47,7 @@ class DefaultWalletManagersFacade( private val walletManagersStore: WalletManagersStore, private val userWalletsStore: UserWalletsStore, configManager: ConfigManager, + mnemonic: Mnemonic, assetReader: AssetReader, moshi: Moshi, ) : WalletManagersFacade { @@ -55,6 +58,7 @@ class DefaultWalletManagersFacade( private val sdkTokenConverter by lazy { SdkTokenConverter() } private val txHistoryStateConverter by lazy { SdkTransactionHistoryStateConverter() } private val txHistoryItemConverter by lazy { SdkTransactionHistoryItemConverter(assetReader, moshi) } + private val estimationFeeAddressFactory by lazy { EstimationFeeAddressFactory(mnemonic) } override suspend fun update( userWalletId: UserWalletId, @@ -444,7 +448,13 @@ class DefaultWalletManagersFacade( blockchain = blockchain, derivationPath = network.derivationPath.value, ) - return (walletManager as? TransactionSender)?.estimateFee(amount = amount) + + val destination = estimationFeeAddressFactory.makeAddress(blockchain) + + return (walletManager as? TransactionSender)?.estimateFee( + amount = amount, + destination = destination, + ) } override suspend fun validateTransaction( diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt index 9cb238b99e..71ac3393b7 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt @@ -8,7 +8,7 @@ import com.tangem.crypto.bip39.Wordlist /** [REDACTED_AUTHOR] */ -internal class DefaultMnemonicRepository( +class DefaultMnemonicRepository( private val bip39Wordlist: Wordlist, ) : MnemonicRepository { From 64c7ee661861e6a5fcbbb48f8602bff2eef77595 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Dec 2023 12:53:20 +0300 Subject: [PATCH 064/102] Updated on 2026-08-14 --- .../api/express/models/response/ExchangeDataResponse.kt | 3 +++ .../main/java/com/tangem/feature/swap/DefaultSwapRepository.kt | 3 +++ .../java/com/tangem/feature/swap/domain/models/DataError.kt | 2 ++ 3 files changed, 8 insertions(+) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt index 40c3483f16..7496a0a8e2 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeDataResponse.kt @@ -31,6 +31,9 @@ data class ExchangeDataResponse( ) data class TxDetails( + @Json(name = "payoutAddress") + val payoutAddress: String, + @Json(name = "requestId") val requestId: String, diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 7a4c0df381..772502d688 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -292,6 +292,9 @@ internal class DefaultSwapRepository @Inject constructor( if (txDetails.requestId != requestId) { return@withContext DataError.InvalidRequestIdError().left() } + if (toAddress != txDetails.payoutAddress) { + return@withContext DataError.InvalidPayoutAddressError().left() + } expressDataConverter.convert( ExchangeDataResponseWithTxDetails( dataResponse = response, diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt index 9c0ca6b268..0da33b0bcf 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/DataError.kt @@ -36,6 +36,8 @@ sealed class DataError { data class InvalidRequestIdError(override val code: Int = 991) : DataError() + data class InvalidPayoutAddressError(override val code: Int = 992) : DataError() + object UnknownError : DataError() { override val code: Int = -1 } From bd9ecb82cef4684be45019f06110684d65aeafc6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Dec 2023 13:01:30 +0300 Subject: [PATCH 065/102] Updated on 2026-08-14 --- .../feature/swap/viewmodels/SwapViewModel.kt | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) 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 a511e6ab43..84cde6431c 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 @@ -12,8 +12,11 @@ import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.tokens.GetCryptoCurrencyStatusSyncUseCase +import com.tangem.domain.tokens.UpdateDelayedNetworkStatusUseCase import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.swap.analytics.SwapEvents import com.tangem.feature.swap.domain.BlockchainInteractor import com.tangem.feature.swap.domain.SwapInteractor @@ -32,6 +35,7 @@ import com.tangem.feature.swap.ui.StateBuilder import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -57,6 +61,7 @@ internal class SwapViewModel @Inject constructor( private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase, private val getSelectedAppCurrencyUseCase: GetSelectedAppCurrencyUseCase, private val getCryptoCurrencyStatusUseCase: GetCryptoCurrencyStatusSyncUseCase, + private val updateDelayedCurrencyStatusUseCase: UpdateDelayedNetworkStatusUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel(), DefaultLifecycleObserver { @@ -448,6 +453,7 @@ internal class SwapViewModel @Inject constructor( }.onSuccess { when (it) { is TxState.TxSent -> { + updateWalletBalance() uiState = stateBuilder.createSuccessState( uiState = uiState, txState = it, @@ -957,10 +963,32 @@ internal class SwapViewModel @Inject constructor( } } + private fun updateWalletBalance() { + swapInteractor.getSelectedWallet()?.let { userWallet -> + dataState.fromCryptoCurrency?.currency?.network?.let { network -> + viewModelScope.launch { + withContext(NonCancellable) { + updateForBalance(userWallet, network) + } + } + } + } + } + + private suspend fun updateForBalance(userWallet: UserWallet, network: Network) { + updateDelayedCurrencyStatusUseCase( + userWalletId = userWallet.walletId, + network = network, + delayMillis = UPDATE_BALANCE_DELAY_MILLIS, + refresh = true, + ) + } + companion object { private const val loggingTag = "SwapViewModel" private const val INITIAL_AMOUNT = "" private const val UPDATE_DELAY = 10000L private const val DEBOUNCE_AMOUNT_DELAY = 1000L + private const val UPDATE_BALANCE_DELAY_MILLIS = 11000L } } \ No newline at end of file From 950aafe4edbfdca9bd3ee60627316c69a7e0da2a Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 22 Dec 2023 19:00:55 +0200 Subject: [PATCH 066/102] Updated on 2026-08-14 --- .../src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt index 0aec67858b..45d37e07de 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/TransactionCard.kt @@ -418,7 +418,7 @@ private fun TokenIcon( contentAlignment = Alignment.Center, ) { Image( - modifier = Modifier.padding(all = TangemTheme.dimens.spacing0_5), + modifier = Modifier.padding(all = TangemTheme.dimens.spacing2), painter = painterResource(id = networkIconRes), contentDescription = null, ) From e0bc66f30c19abb44c77d8ebf352e5fddaf18146 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 09:19:37 +0200 Subject: [PATCH 067/102] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 1 + .../models/ui/TokensDataStateExpress.kt | 5 +-- .../feature/swap/domain/SwapInteractorImpl.kt | 1 + .../swap/converters/TokensDataConverter.kt | 1 + .../swap/models/SwapSelectTokenStateHolder.kt | 1 + .../feature/swap/ui/SwapSelectTokenScreen.kt | 35 ++++++++++++++++--- .../feature/swap/viewmodels/SwapViewModel.kt | 3 ++ 8 files changed, 42 insertions(+), 6 deletions(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 42f9f8b591..c2cdf27727 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -193,6 +193,7 @@ Вы отправляете Мои токены У вас нет добавленных токенов. Добавьте токены для обмена + Токены не найдены. Пожалуйста, попробуйте другой запрос Недоступен для обмена с %s Кроме того, в курс обмена включена комиссия сети за отправку обмененных средств на ваш адрес Статус diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 1150c0349b..94f6ed4c2e 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -192,6 +192,7 @@ You send My tokens You haven\'t added any tokens yet. Add tokens via Market to swap + No tokens found. Please try another request Cannot be swapped for %s Additionally, the network fee for sending the exchanged funds back to your address is included in the rate Status diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index 60c8e749bd..d761ad97fb 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -11,8 +11,8 @@ data class TokensDataStateExpress( companion object { val EMPTY = TokensDataStateExpress( - fromGroup = CurrenciesGroup(emptyList(), emptyList()), - toGroup = CurrenciesGroup(emptyList(), emptyList()), + fromGroup = CurrenciesGroup(emptyList(), emptyList(), false), + toGroup = CurrenciesGroup(emptyList(), emptyList(), false), allProviders = emptyList(), ) } @@ -21,4 +21,5 @@ data class TokensDataStateExpress( data class CurrenciesGroup( val available: List, val unavailable: List, + val afterSearch: Boolean, ) \ No newline at end of file diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 19a88c1d4a..d38fc7f54a 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -148,6 +148,7 @@ internal class SwapInteractorImpl @Inject constructor( return CurrenciesGroup( available = availableCryptoCurrencies, unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) }, + afterSearch = false ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index 895fcb1ea5..d475bad6f2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -49,6 +49,7 @@ class TokensDataConverter( .toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, + afterSearch = value.afterSearch, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt index ac6762d7b7..585afb1921 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapSelectTokenStateHolder.kt @@ -7,6 +7,7 @@ import kotlinx.collections.immutable.ImmutableList data class SwapSelectTokenStateHolder( val availableTokens: ImmutableList, val unavailableTokens: ImmutableList, + val afterSearch: Boolean, val onSearchEntered: (String) -> Unit, val onTokenSelected: (String) -> Unit, ) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 4057aba9af..87a82413f1 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -42,10 +42,16 @@ fun SwapSelectTokenScreen(state: SwapSelectTokenStateHolder, onBack: () -> Unit) .background(color = TangemTheme.colors.background.secondary), content = { padding -> val modifier = Modifier.padding(padding) - if (state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty()) { - EmptyTokensList(modifier) - } else { - ListOfTokens(state = state, modifier = modifier) + when { + state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() && state.afterSearch -> { + TokensNotFound(modifier) + } + state.availableTokens.isEmpty() && state.unavailableTokens.isEmpty() && !state.afterSearch -> { + EmptyTokensList(modifier) + } + else -> { + ListOfTokens(state = state, modifier = modifier) + } } }, topBar = { @@ -91,6 +97,26 @@ private fun EmptyTokensList(modifier: Modifier = Modifier) { } } +@Composable +private fun TokensNotFound(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .background(TangemTheme.colors.background.secondary) + .fillMaxSize(), + ) { + Text( + modifier = Modifier + .padding(top = TangemTheme.dimens.spacing32) + .padding(horizontal = TangemTheme.dimens.spacing30) + .align(Alignment.TopCenter), + text = stringResource(id = R.string.express_token_list_empty_search), + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.tertiary, + textAlign = TextAlign.Center, + ) + } +} + @Composable private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = Modifier) { val screenBackgroundColor = TangemTheme.colors.background.secondary @@ -283,6 +309,7 @@ private fun TokenScreenPreview() { state = SwapSelectTokenStateHolder( availableTokens = listOf(title, token, token, token).toImmutableList(), unavailableTokens = listOf(title, token, token, token).toImmutableList(), + afterSearch = false, onSearchEntered = {}, onTokenSelected = {}, ), 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 84cde6431c..f021a40ab2 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 @@ -586,13 +586,16 @@ internal class SwapViewModel @Inject constructor( fromGroup = tokenDataState.fromGroup.copy( available = available, unavailable = unavailable, + afterSearch = true, ), + ) } else { tokenDataState.copy( toGroup = tokenDataState.toGroup.copy( available = available, unavailable = unavailable, + afterSearch = true, ), ) } From d0869cc149abadc02df7696e0a8030ea43df04fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 10:03:27 +0200 Subject: [PATCH 068/102] Updated on 2026-08-14 --- .../domain/models/ui/TokensDataStateExpress.kt | 2 +- .../feature/swap/domain/SwapInteractorImpl.kt | 2 +- .../feature/swap/viewmodels/SwapViewModel.kt | 17 +++++++++++++---- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt index d761ad97fb..1aa9ba51d1 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/TokensDataStateExpress.kt @@ -12,7 +12,7 @@ data class TokensDataStateExpress( val EMPTY = TokensDataStateExpress( fromGroup = CurrenciesGroup(emptyList(), emptyList(), false), - toGroup = CurrenciesGroup(emptyList(), emptyList(), false), + toGroup = CurrenciesGroup(emptyList(), emptyList(), false), allProviders = emptyList(), ) } diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index d38fc7f54a..55d82743e6 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -148,7 +148,7 @@ internal class SwapInteractorImpl @Inject constructor( return CurrenciesGroup( available = availableCryptoCurrencies, unavailable = unavailableCryptoCurrencies.map { CryptoCurrencySwapInfo(it, emptyList()) }, - afterSearch = false + afterSearch = false, ) } 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 f021a40ab2..ee03e66695 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 @@ -98,6 +98,11 @@ internal class SwapViewModel @Inject constructor( private var isOrderReversed = false private val lastAmount = mutableStateOf(INITIAL_AMOUNT) private var swapRouter: SwapRouter by Delegates.notNull() + + private val isExchangeTooSmallAmountError: (SwapState) -> Boolean = { + it is SwapState.SwapError && it.error is DataError.ExchangeTooSmallAmountError + } + val currentScreen: SwapNavScreen get() = swapRouter.currentScreen @@ -385,13 +390,17 @@ internal class SwapViewModel @Inject constructor( } private fun selectProvider(state: Map): SwapProvider { - val stateSuccess = state.getLastLoadedSuccessStates() - return if (stateSuccess.isNotEmpty()) { + val consideredProviders = state.filter { + it.value is SwapState.QuotesLoadedState || isExchangeTooSmallAmountError(it.value) + } + + return if (consideredProviders.isNotEmpty()) { val currentSelected = dataState.selectedProvider - if (currentSelected != null && stateSuccess.keys.contains(currentSelected)) { + if (currentSelected != null && consideredProviders.keys.contains(currentSelected)) { currentSelected } else { - findBestQuoteProvider(stateSuccess) ?: stateSuccess.keys.first() + findBestQuoteProvider(consideredProviders.getLastLoadedSuccessStates()) + ?: consideredProviders.keys.first() } } else { state.keys.first() From f03f9d1b0dcd0ff442b7db79bfd7a497a9637880 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 10:37:24 +0200 Subject: [PATCH 069/102] Updated on 2026-08-14 --- .../com/tangem/feature/swap/models/SwapStateHolder.kt | 1 - .../main/java/com/tangem/feature/swap/ui/ProviderItem.kt | 4 +++- .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 9 ++------- .../java/com/tangem/feature/swap/ui/SwapScreenContent.kt | 5 +---- 4 files changed, 6 insertions(+), 13 deletions(-) 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 83d86eee5b..577c2e96af 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 @@ -72,7 +72,6 @@ sealed class SwapCardState { data class SwapButton( val enabled: Boolean, - val loading: Boolean = false, val onClick: () -> Unit, ) 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 4a217bcbfe..c1a94da8f7 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 @@ -260,7 +260,9 @@ private fun ProviderLoadingState(modifier: Modifier = Modifier) { ), ) { CircularProgressIndicator( - modifier = Modifier.size(TangemTheme.dimens.size16), + modifier = Modifier + .size(TangemTheme.dimens.size16) + .align(Alignment.CenterVertically), color = TangemTheme.colors.icon.informative, strokeWidth = TangemTheme.dimens.size2, ) 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 f02e5006fe..fab1b174c2 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 @@ -79,7 +79,7 @@ internal class StateBuilder( ), fee = FeeItemState.Empty, networkCurrency = networkInfo.blockchainCurrency, - swapButton = SwapButton(enabled = false, loading = true, onClick = {}), + swapButton = SwapButton(enabled = false, onClick = {}), onRefresh = {}, onBackClicked = actions.onBackClicked, onChangeCardsClicked = actions.onChangeCardsClicked, @@ -138,7 +138,6 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, - loading = false, onClick = { }, ), changeCardsButtonState = ChangeCardsButtonState.DISABLED, @@ -187,7 +186,7 @@ internal class StateBuilder( ), warnings = emptyList(), fee = FeeItemState.Empty, - swapButton = SwapButton(enabled = false, loading = true, onClick = {}), + swapButton = SwapButton(enabled = false, onClick = {}), providerState = ProviderState.Loading(), permissionState = uiStateHolder.permissionState, changeCardsButtonState = ChangeCardsButtonState.UPDATE_IN_PROGRESS, @@ -260,7 +259,6 @@ internal class StateBuilder( fee = feeState, swapButton = SwapButton( enabled = getSwapButtonEnabled(quoteModel.preparedSwapConfigState), - loading = false, onClick = actions.onSwapClick, ), changeCardsButtonState = if (isReverseSwapPossible) { @@ -454,7 +452,6 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, - loading = false, onClick = actions.onSwapClick, ), changeCardsButtonState = if (isReverseSwapPossible) { @@ -566,7 +563,6 @@ internal class StateBuilder( fee = FeeItemState.Empty, swapButton = SwapButton( enabled = false, - loading = false, onClick = { }, ), changeCardsButtonState = if (isReverseSwapPossible) { @@ -582,7 +578,6 @@ internal class StateBuilder( fun createSwapInProgressState(uiState: SwapStateHolder): SwapStateHolder { return uiState.copy( swapButton = uiState.swapButton.copy( - loading = true, enabled = false, ), ) 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 b211130172..571a1b51f1 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 @@ -399,7 +399,6 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.swapping_insufficient_funds), enabled = false, - showProgress = state.swapButton.loading, onClick = state.swapButton.onClick, ) } @@ -408,7 +407,6 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U modifier = Modifier.fillMaxWidth(), text = stringResource(id = R.string.swapping_give_permission), enabled = true, - showProgress = state.swapButton.loading, onClick = onPermissionWarningClick, ) } @@ -418,7 +416,6 @@ private fun MainButton(state: SwapStateHolder, onPermissionWarningClick: () -> U text = stringResource(id = R.string.common_swap), iconResId = R.drawable.ic_tangem_24, enabled = state.swapButton.enabled, - showProgress = state.swapButton.loading, onClick = state.swapButton.onClick, ) } @@ -487,7 +484,7 @@ private val state = SwapStateHolder( ), ), networkCurrency = "MATIC", - swapButton = SwapButton(enabled = true, loading = false, onClick = {}), + swapButton = SwapButton(enabled = true, onClick = {}), onRefresh = {}, onBackClicked = {}, onChangeCardsClicked = {}, From 20bd61d4f4db2b779ced5aee4954fce7ef150c1a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 11:54:39 +0300 Subject: [PATCH 070/102] Updated on 2026-08-14 --- .../swap/converters/ExchangeStatusConverter.kt | 2 +- .../domain/models/domain/ExchangeStatus.kt | 2 +- .../feature/swap/domain/models/ui/TxState.kt | 3 +-- .../swap/domain/BlockchainInteractor.kt | 2 +- .../swap/domain/DefaultBlockchainInteractor.kt | 4 ++-- .../feature/swap/domain/SwapInteractorImpl.kt | 18 ++++++------------ .../feature/swap/domain/di/SwapDomainModule.kt | 2 -- .../com/tangem/feature/swap/ui/StateBuilder.kt | 3 ++- .../feature/swap/viewmodels/SwapViewModel.kt | 10 +++++++--- ...kenDetailsSwapTransactionsStateConverter.kt | 8 ++++---- 10 files changed, 25 insertions(+), 29 deletions(-) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt index f5901e8c69..30ea8ec4cd 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ExchangeStatusConverter.kt @@ -13,7 +13,7 @@ internal class ExchangeStatusConverter : Converter { allowPermissionsHandler.addAddressToInProgress(permissionOptions.forTokenContractAddress) TxState.TxSent( - txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), + txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), timestamp = System.currentTimeMillis(), ) } @@ -463,7 +462,7 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet.symbol, ), toAmountValue = swapData.toTokenAmount.value, - txAddress = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), + txHash = userWalletManager.getLastTransactionHash(networkId, derivationPath).orEmpty(), timestamp = System.currentTimeMillis(), ) } @@ -537,10 +536,6 @@ internal class SwapInteractorImpl @Inject constructor( }, ifRight = { val timestamp = System.currentTimeMillis() - val txUrl = blockchainInteractor.getExplorerTransactionLink( - networkId = currencyToSend.currency.network.backendId, - txAddress = exchangeData.transaction.txTo, - ) storeSwapTransaction( currencyToSend = currencyToSend, currencyToGet = currencyToGet, @@ -548,7 +543,7 @@ internal class SwapInteractorImpl @Inject constructor( swapProvider = swapProvider, swapDataModel = exchangeData, timestamp = timestamp, - txUrl = txUrl, + txExternalUrl = externalUrl.orEmpty(), ) storeLastCryptoCurrencyId(currencyToGet.currency) TxState.TxSent( @@ -562,12 +557,11 @@ internal class SwapInteractorImpl @Inject constructor( currencyToGet.currency.symbol, ), toAmountValue = exchangeData.toTokenAmount.value, - txAddress = userWalletManager.getLastTransactionHash( + txHash = userWalletManager.getLastTransactionHash( currencyToSend.currency.network.backendId, derivationPath, ).orEmpty(), txExternalUrl = externalUrl, - txUrl = txUrl, timestamp = timestamp, ) }, @@ -602,7 +596,7 @@ internal class SwapInteractorImpl @Inject constructor( swapProvider: SwapProvider, swapDataModel: SwapDataModel, timestamp: Long, - txUrl: String, + txExternalUrl: String, ) { swapTransactionRepository.storeTransaction( userWalletId = UserWalletId(userWalletManager.getWalletId()), @@ -618,7 +612,7 @@ internal class SwapInteractorImpl @Inject constructor( providerId = swapProvider.providerId, status = ExchangeStatus.New, txId = swapDataModel.transaction.txId, - txUrl = txUrl, + txExternalUrl = txExternalUrl, ), ), ) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt index 518d7c9fc8..d299f7824e 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/di/SwapDomainModule.kt @@ -42,7 +42,6 @@ class SwapDomainModule { walletManagersFacade: WalletManagersFacade, coroutineDispatcherProvider: CoroutineDispatcherProvider, initialToCurrencyResolver: InitialToCurrencyResolver, - blockchainInteractor: BlockchainInteractor, ): SwapInteractor { return SwapInteractorImpl( transactionManager = transactionManager, @@ -57,7 +56,6 @@ class SwapDomainModule { dispatcher = coroutineDispatcherProvider, swapTransactionRepository = swapTransactionRepository, initialToCurrencyResolver = initialToCurrencyResolver, - blockchainInteractor = blockchainInteractor, ) } 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 fab1b174c2..b7c7418dae 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 @@ -732,6 +732,7 @@ internal class StateBuilder( dataState: SwapProcessDataState, onExploreClick: () -> Unit, onStatusClick: () -> Unit, + txUrl: String, ): SwapStateHolder { val fee = requireNotNull(dataState.selectedFee) val fromCryptoCurrency = requireNotNull(dataState.fromCryptoCurrency) @@ -746,7 +747,7 @@ internal class StateBuilder( return uiState.copy( successState = SwapSuccessStateHolder( timestamp = txState.timestamp, - txUrl = txState.txUrl.orEmpty(), + txUrl = txUrl, providerName = stringReference(providerState.name), providerType = stringReference(providerState.type), showStatusButton = providerState.type == ExchangeProviderType.CEX.name, 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 ee03e66695..955d5e5f74 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 @@ -462,15 +462,19 @@ internal class SwapViewModel @Inject constructor( }.onSuccess { when (it) { is TxState.TxSent -> { + val url = blockchainInteractor.getExplorerTransactionLink( + networkId = fromCurrency.currency.network.backendId, + txHash = it.txHash, + ) updateWalletBalance() uiState = stateBuilder.createSuccessState( uiState = uiState, txState = it, dataState = dataState, + txUrl = url, onExploreClick = { - val txUrl = it.txUrl - if (!txUrl.isNullOrBlank()) { - swapRouter.openUrl(txUrl) + if (it.txHash.isNotEmpty()) { + swapRouter.openUrl(url) } analyticsEventHandler.send( event = SwapEvents.ButtonExplore(initialCryptoCurrency.symbol), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index fd13879bc9..514afc7709 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -66,7 +66,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( SwapTransactionsState( txId = transaction.txId, provider = transaction.provider, - txUrl = transaction.status?.txUrl, + txUrl = transaction.status?.txExternalUrl, timestamp = TextReference.Str("${timestamp.toDateFormat()}, ${timestamp.toTimeFormat()}"), fiatSymbol = appCurrency.symbol, statuses = getStatuses(transaction.status?.status), @@ -74,7 +74,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( activeStatus = transaction.status?.status, notification = getNotification( transaction.status?.status, - transaction.status?.txUrl, + transaction.status?.txExternalUrl, ), toCryptoCurrencyId = toCurrency.currency.id, toCryptoAmount = BigDecimalFormatter.formatCryptoAmount( @@ -112,9 +112,9 @@ internal class TokenDetailsSwapTransactionsStateConverter( return tx.copy( activeStatus = statusModel.status, hasFailed = hasFailed, - notification = getNotification(statusModel.status, statusModel.txUrl), + notification = getNotification(statusModel.status, statusModel.txExternalUrl), statuses = getStatuses(statusModel.status, hasFailed), - txUrl = statusModel.txUrl, + txUrl = statusModel.txExternalUrl, ) } From b024f91f192f4a5af2bc1335294eb0a49c669c34 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 16:12:47 +0300 Subject: [PATCH 071/102] Updated on 2026-08-14 --- .../src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt index b7c7418dae..326f09752c 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 @@ -302,6 +302,7 @@ internal class StateBuilder( ) } + @Suppress("LongMethod", "CyclomaticComplexMethod") private fun getWarningsForSuccessState( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, @@ -327,7 +328,8 @@ internal class StateBuilder( else -> Unit } if (!quoteModel.preparedSwapConfigState.isFeeEnough && - quoteModel.preparedSwapConfigState.isBalanceEnough + quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.permissionState !is PermissionDataState.PermissionLoading ) { warnings.add( SwapWarning.UnableToCoverFeeWarning( From 7507f59feec1cccc3e0f88fce481741ab21c1ec1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 16:55:43 +0300 Subject: [PATCH 072/102] Updated on 2026-08-14 --- .../com/tangem/feature/swap/viewmodels/SwapViewModel.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index 955d5e5f74..34f4e0f818 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 @@ -853,14 +853,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) }, ) } From a1943321d9d97d7de0d82fe4e76ed6864f3ef542 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 18:25:48 +0300 Subject: [PATCH 073/102] Updated on 2026-08-14 --- .../tangem/feature/swap/viewmodels/SwapViewModel.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 34f4e0f818..23f24834f8 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 @@ -420,16 +420,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 From 6fe140bb45cc6f9e708961ab26d52e5df6144d82 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 18:45:36 +0300 Subject: [PATCH 074/102] Updated on 2026-08-14 --- .../tangem/feature/swap/ui/StateBuilder.kt | 42 +++++++++++-------- .../feature/swap/viewmodels/SwapViewModel.kt | 12 ++++-- 2 files changed, 33 insertions(+), 21 deletions(-) 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 326f09752c..dc89739051 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 @@ -209,7 +209,7 @@ internal class StateBuilder( fromToken: CryptoCurrency, swapProvider: SwapProvider, bestRatedProviderId: String, - isManyProviders: Boolean, + isNeedBestRateBadge: Boolean, selectedFeeType: FeeType, isReverseSwapPossible: Boolean, ): SwapStateHolder { @@ -270,7 +270,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, ), @@ -327,19 +327,7 @@ internal class StateBuilder( ) else -> Unit } - if (!quoteModel.preparedSwapConfigState.isFeeEnough && - quoteModel.preparedSwapConfigState.isBalanceEnough && - quoteModel.permissionState !is PermissionDataState.PermissionLoading - ) { - 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 @@ -379,6 +367,26 @@ internal class StateBuilder( return warnings } + private fun addUnableCoverFeeWarning( + quoteModel: SwapState.QuotesLoadedState, + fromToken: CryptoCurrency, + warnings: MutableList, + ) { + if (!quoteModel.preparedSwapConfigState.isFeeEnough && + quoteModel.preparedSwapConfigState.isBalanceEnough && + quoteModel.permissionState !is PermissionDataState.PermissionLoading + ) { + warnings.add( + SwapWarning.UnableToCoverFeeWarning( + createUnableToCoverFeeNotificationConfig( + fromToken = fromToken, + onBuyClick = actions.onBuyClick, + ), + ), + ) + } + } + private fun getSwapButtonEnabled(preparedSwapConfigState: PreparedSwapConfigState): Boolean { if (preparedSwapConfigState.hasOutgoingTransaction) return false return when (preparedSwapConfigState.includeFeeInAmount) { @@ -1123,7 +1131,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( @@ -1133,7 +1141,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 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 23f24834f8..8dc1e1a382 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 @@ -326,7 +326,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 +390,7 @@ internal class SwapViewModel @Inject constructor( } private fun selectProvider(state: Map): SwapProvider { - val consideredProviders = state.filter { - it.value is SwapState.QuotesLoadedState || isExchangeTooSmallAmountError(it.value) - } + val consideredProviders = state.consideredProvidersStates() return if (consideredProviders.isNotEmpty()) { val currentSelected = dataState.selectedProvider @@ -948,6 +946,12 @@ internal class SwapViewModel @Inject constructor( .mapValues { it.value as SwapState.QuotesLoadedState } } + private fun Map.consideredProvidersStates(): Map { + return this.filter { + it.value is SwapState.QuotesLoadedState || isExchangeTooSmallAmountError(it.value) + } + } + private fun isReverseSwapPossible(): Boolean { val from = dataState.fromCryptoCurrency ?: return false val to = dataState.toCryptoCurrency ?: return false From c170dd95d5c3da4bfcedefa7aa4eab88a64f6f7a Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 17:09:56 +0200 Subject: [PATCH 075/102] Updated on 2026-08-14 --- .../tangem/feature/swap/ui/StateBuilder.kt | 8 ++------ .../feature/swap/viewmodels/SwapViewModel.kt | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 14 deletions(-) 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 dc89739051..5b22001266 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -98,12 +98,8 @@ internal class StateBuilder( if (uiStateHolder.sendCardData !is SwapCardState.SwapCardData) return uiStateHolder return uiStateHolder.copy( sendCardData = SwapCardState.SwapCardData( - type = TransactionCardType.ReadOnly( - headerResId = R.string.exchange_send_view_header, - ), - amountTextFieldValue = TextFieldValue( - text = "0", - ), + type = requireNotNull(uiStateHolder.sendCardData.type as? TransactionCardType.Inputtable), + amountTextFieldValue = null, amountEquivalent = "0 ${appCurrencyProvider.invoke().symbol}", token = fromToken, tokenIconUrl = uiStateHolder.sendCardData.tokenIconUrl, 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 8dc1e1a382..ccccda01cc 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 @@ -692,19 +692,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), + ) + } } } } From 90e5684d027f9742e340dc7e909817fd645586ab Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 18:24:21 +0200 Subject: [PATCH 076/102] Updated on 2026-08-14 --- .../com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 0621d0a12a..c77ff9d577 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 @@ -79,7 +79,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, + ), ) } } From 7e5473b8c562a37c6bd201a62a9a5b8249048a9b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 19:30:33 +0300 Subject: [PATCH 077/102] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 3 +++ core/res/src/main/res/values/strings.xml | 3 ++- .../ui/components/exchange/ExchangeStatusBottomSheet.kt | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index c2cdf27727..c2434991f2 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -732,4 +732,7 @@ Сканировать карту Используйте %s или отсканируйте карту для входа в приложение C возвращением! + Обмен через %s + Данные провайдера. Сумма к получению может измениться в зависимости от рыночных условий. + Статус обмена diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 94f6ed4c2e..7998107847 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -205,6 +205,7 @@ Operation failed by provider Visit provider’s website for verification KYC verification required by provider + Cancelled Confirmed Confirming Confirming… @@ -219,7 +220,7 @@ Sending to you Sending to you… Sent - Provider-sourced data. Estimated amount subject to change. + Provider-sourced data. Estimated amount subject to change due to market conditions. Exchange status Verified Verification required diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index 7cd58e238d..ea31904d30 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import com.tangem.core.ui.R import com.tangem.core.ui.components.SpacerH10 import com.tangem.core.ui.components.SpacerH12 @@ -50,6 +51,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC text = stringResource(id = R.string.express_exchange_status_subtitle), style = TangemTheme.typography.caption2, color = TangemTheme.colors.text.secondary, + textAlign = TextAlign.Center, modifier = Modifier .align(CenterHorizontally), ) From 9ae8e54953acbe3bbd9884646b6b3a76fbe2fa3d Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 25 Dec 2023 19:30:50 +0300 Subject: [PATCH 078/102] Updated on 2026-08-14 --- .../models/response/ExchangeStatusResponse.kt | 3 + core/res/src/main/res/values-ru/strings.xml | 1 + core/res/src/main/res/values/strings.xml | 2 +- .../domain/models/domain/ExchangeStatus.kt | 1 + .../state/SwapTransactionsState.kt | 1 + ...enDetailsSwapTransactionsStateConverter.kt | 58 +++++++++++++------ .../exchange/ExchangeStatusBlock.kt | 20 +++++-- .../exchange/ExchangeStatusBottomSheet.kt | 2 +- .../exchange/ExchangeStatusItems.kt | 8 ++- .../viewmodels/TokenDetailsViewModel.kt | 3 +- 10 files changed, 70 insertions(+), 29 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index 94cd9cbf1f..3be2a6ebec 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -48,6 +48,9 @@ enum class ExchangeStatus { @Json(name = "verifying") VERIFYING, + + @Json(name = "cancelled") + CANCELLED, } data class ExchangeStatusError( diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index c2434991f2..afc6fda219 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -205,6 +205,7 @@ Операция не выполнена провайдером Посетите сайт провайдера для проверки Провайдер запрашивает прохождение верификации + Отменен Подтверждено Подтверждение Подтверждение... diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 7998107847..6548a846ee 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -205,7 +205,7 @@ Operation failed by provider Visit provider’s website for verification KYC verification required by provider - Cancelled + Canceled Confirmed Confirming Confirming… diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt index e412c74751..1e3727b208 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/ExchangeStatus.kt @@ -17,4 +17,5 @@ enum class ExchangeStatus { Sending, Finished, Refunded, + Cancelled, } \ No newline at end of file diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt index d55821009b..c1e0f67210 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/SwapTransactionsState.kt @@ -29,6 +29,7 @@ internal data class SwapTransactionsState( val fromCryptoSymbol: String, val fromFiatAmount: String, val fromCurrencyIcon: TokenIconState, + val showProviderLink: Boolean, val onClick: () -> Unit, val onGoToProviderClick: (String) -> Unit, ) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt index 514afc7709..c5069303c7 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsSwapTransactionsStateConverter.kt @@ -62,6 +62,8 @@ internal class TokenDetailsSwapTransactionsStateConverter( val toFiatAmount = toAmount.multiply(toCurrency.value.fiatRate) val fromFiatAmount = fromAmount.multiply(fromCurrency.value.fiatRate) val timestamp = transaction.timestamp + val notifications = getNotification(transaction.status?.status, transaction.status?.txExternalUrl) + val showProviderLink = getShowProviderLink(notifications, transaction.status) result.add( SwapTransactionsState( txId = transaction.txId, @@ -92,6 +94,7 @@ internal class TokenDetailsSwapTransactionsStateConverter( fromCryptoSymbol = fromCurrency.currency.symbol, fromFiatAmount = getFiatAmount(fromFiatAmount), fromCurrencyIcon = iconStateConverter.convert(fromCurrency), + showProviderLink = showProviderLink, onClick = { clickIntents.onSwapTransactionClick(transaction.txId) }, onGoToProviderClick = { url -> analyticsEventsHandlerProvider().send( @@ -109,12 +112,15 @@ internal class TokenDetailsSwapTransactionsStateConverter( fun updateTxStatus(tx: SwapTransactionsState, statusModel: ExchangeStatusModel?): SwapTransactionsState { if (statusModel == null || tx.activeStatus == statusModel.status) return tx val hasFailed = tx.hasFailed || statusModel.status == ExchangeStatus.Failed + val notifications = getNotification(statusModel.status, statusModel.txExternalUrl) + val showProviderLink = getShowProviderLink(notifications, statusModel) return tx.copy( activeStatus = statusModel.status, hasFailed = hasFailed, - notification = getNotification(statusModel.status, statusModel.txExternalUrl), + notification = notifications, statuses = getStatuses(statusModel.status, hasFailed), txUrl = statusModel.txExternalUrl, + showProviderLink = showProviderLink, ) } @@ -149,6 +155,9 @@ internal class TokenDetailsSwapTransactionsStateConverter( } } + private fun getShowProviderLink(notifications: ExchangeStatusNotifications?, status: ExchangeStatusModel?) = + notifications == null && status?.txExternalUrl != null && status.status != ExchangeStatus.Cancelled + private fun getStatuses(status: ExchangeStatus?, hasFailed: Boolean = false): ImmutableList { if (status == null) return persistentListOf() val isWaiting = status == ExchangeStatus.New || status == ExchangeStatus.Waiting @@ -164,26 +173,37 @@ internal class TokenDetailsSwapTransactionsStateConverter( val isExchangingDone = !isExchanging && isConfirmingDone val isSendingDone = !isSending && !isVerifying && !isFailed && isExchangingDone - return listOf( - waitStep(isWaiting, isWaitingDone), - confirmStep(isConfirming, isConfirmingDone), - exchangeStep( - isExchanging = isExchanging, - isExchangingDone = isExchangingDone, - isRefunded = isRefunded, - hasFailed = hasFailed, - isVerifying = isVerifying, - isFailed = isFailed, - ), - sendStep( - isSending = isSending, - isSendingDone = isSendingDone, - isRefunded = isRefunded, - hasFailed = hasFailed, - ), - ).toPersistentList() + return if (status == ExchangeStatus.Cancelled) { + listOf(cancelledStep()) + } else { + listOf( + waitStep(isWaiting, isWaitingDone), + confirmStep(isConfirming, isConfirmingDone), + exchangeStep( + isExchanging = isExchanging, + isExchangingDone = isExchangingDone, + isRefunded = isRefunded, + hasFailed = hasFailed, + isVerifying = isVerifying, + isFailed = isFailed, + ), + sendStep( + isSending = isSending, + isSendingDone = isSendingDone, + isRefunded = isRefunded, + hasFailed = hasFailed, + ), + ) + }.toPersistentList() } + private fun cancelledStep() = ExchangeStatusState( + status = ExchangeStatus.Cancelled, + text = TextReference.Res(R.string.express_exchange_status_canceled), + isActive = true, + isDone = true, + ) + private fun waitStep(isNew: Boolean, isNewDone: Boolean) = ExchangeStatusState( status = ExchangeStatus.New, text = when { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt index 1574bb2db6..ac5edc4fc4 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBlock.kt @@ -75,11 +75,15 @@ internal fun ExchangeStatusBlock( } } - statuses.forEachIndexed { index, item -> - ExchangeStatusStep( - stepStatus = item, - isLast = index == statuses.lastIndex, - ) + AnimatedContent(targetState = statuses.lastIndex, label = "Exchange Status List Change") { + Column { + statuses.forEachIndexed { index, item -> + ExchangeStatusStep( + stepStatus = item, + isLast = index == it, + ) + } + } } } } @@ -101,6 +105,11 @@ private fun ExchangeStatusStep( .size(TangemTheme.dimens.size20), ) { when { + it.status == ExchangeStatus.Cancelled -> ExchangeStep( + iconRes = R.drawable.ic_close_24, + color = TangemTheme.colors.icon.warning, + isDone = false, + ) it.status == ExchangeStatus.Failed -> ExchangeStep( iconRes = R.drawable.ic_close_24, color = TangemTheme.colors.icon.warning, @@ -131,6 +140,7 @@ private fun ExchangeStatusStep( @Composable private fun ExchangeStatusStepText(stepStatus: ExchangeStatusState) { val textColor = when { + stepStatus.status == ExchangeStatus.Cancelled -> TangemTheme.colors.icon.warning stepStatus.status == ExchangeStatus.Failed && !stepStatus.isDone -> TangemTheme.colors.icon.warning stepStatus.status == ExchangeStatus.Verifying && !stepStatus.isDone -> TangemTheme.colors.icon.attention stepStatus.isDone -> TangemTheme.colors.text.primary1 diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt index ea31904d30..da9113487a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusBottomSheet.kt @@ -76,7 +76,7 @@ private fun ExchangeStatusBottomSheetContent(content: ExchangeStatusBottomSheetC SpacerH12() ExchangeStatusBlock( statuses = config.statuses, - showLink = config.notification == null && config.txUrl != null, + showLink = config.showProviderLink, onClick = { config.onGoToProviderClick(config.txUrl.orEmpty()) }, ) AnimatedContent( diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt index c32011f436..94c1d12187 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/ui/components/exchange/ExchangeStatusItems.kt @@ -4,7 +4,9 @@ import androidx.annotation.DrawableRes import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -42,7 +44,9 @@ internal fun LazyListScope.swapTransactionsItems( val item = swapTxs[it] val (iconRes, tint) = when (item.activeStatus) { ExchangeStatus.Verifying -> R.drawable.ic_alert_triangle_20 to TangemTheme.colors.icon.attention - ExchangeStatus.Failed -> R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning + ExchangeStatus.Failed, ExchangeStatus.Cancelled -> { + R.drawable.ic_alert_circle_24 to TangemTheme.colors.icon.warning + } else -> null to null } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 1c3f77a105..fee7ee1a64 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -125,6 +125,7 @@ internal class TokenDetailsViewModel @Inject constructor( clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, analyticsEventsHandlerProvider = Provider { analyticsEventsHandler }, + currentStateProvider = Provider { uiState }, userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, ) @@ -229,7 +230,7 @@ internal class TokenDetailsViewModel @Inject constructor( delay = EXCHANGE_STATUS_UPDATE_DELAY, task = { runCatching(dispatchers.io) { - exchangeStatusFactory.updateSwapTxStatuses(swapTxs) + exchangeStatusFactory.updateSwapTxStatuses(uiState.swapTxs) } }, onSuccess = ::updateSwapTx, From 2e886954d89fcc3e32d793c990c2265934e385e6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 09:38:33 +0200 Subject: [PATCH 079/102] Updated on 2026-08-14 --- .../java/com/tangem/feature/swap/ui/SwapScreenContent.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index 571a1b51f1..f0d49bdd3c 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -131,7 +131,7 @@ private fun MainInfo(state: SwapStateHolder) { }, onSelectTokenClick = state.onSelectTokenClick, ) - val marginCard = TangemTheme.dimens.spacing16 + val marginCard = TangemTheme.dimens.spacing12 TransactionCardData( priceImpact = priceImpact, swapCardState = state.receiveCardData, @@ -140,7 +140,7 @@ private fun MainInfo(state: SwapStateHolder) { }, onSelectTokenClick = state.onSelectTokenClick, ) - val marginButton = TangemTheme.dimens.spacing32 + val marginButton = TangemTheme.dimens.spacing30 SwapButton( state, modifier = Modifier.constrainAs(button) { @@ -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 = {}, ), From a5ac876d006a7176961e5e403747896de867cf1f Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 13:01:29 +0200 Subject: [PATCH 080/102] Updated on 2026-08-14 --- .../core/ui/components/appbar/AppBarWithSearch.kt | 1 - .../feature/swap/ui/ChooseProviderBottomSheet.kt | 11 +++-------- .../tangem/feature/swap/ui/SwapSelectTokenScreen.kt | 2 -- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt index 867774adfb..4dfd01d36d 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/appbar/AppBarWithSearch.kt @@ -95,7 +95,6 @@ private fun CollapsedSearchView( modifier = Modifier .background(TangemTheme.colors.background.secondary) .fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16), verticalAlignment = Alignment.CenterVertically, ) { Icon( diff --git a/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 c77ff9d577..07091dfb12 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 @@ -36,14 +36,13 @@ fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { @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), ) Text( text = stringResource(R.string.express_choose_providers_subtitle), @@ -51,8 +50,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( @@ -91,8 +89,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), @@ -100,8 +96,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC color = TangemTheme.colors.icon.informative, modifier = Modifier .padding(top = TangemTheme.dimens.spacing6, bottom = TangemTheme.dimens.spacing16) - .padding(horizontal = TangemTheme.dimens.spacing56) - .align(Alignment.CenterHorizontally), + .padding(horizontal = TangemTheme.dimens.spacing56), textAlign = TextAlign.Center, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index 87a82413f1..ed54834868 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -127,8 +127,6 @@ private fun ListOfTokens(state: SwapSelectTokenStateHolder, modifier: Modifier = .imePadding(), horizontalAlignment = Alignment.CenterHorizontally, ) { - item { SpacerH8() } - tokensToSelectItems(state.availableTokens, state.onTokenSelected) item { SpacerH12() } From f2ad0bd917aabdd4b9fb97f44e8912190c058420 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 14:16:04 +0300 Subject: [PATCH 081/102] Updated on 2026-08-14 --- .../settings/DefaultSwapPromoRepository.kt | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt index dd94a2e07c..8ba9fe211e 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSwapPromoRepository.kt @@ -40,16 +40,23 @@ class DefaultSwapPromoRepository( ) } - private fun checkPromoPeriod(): Boolean { - val currentDate = Calendar.getInstance() - return currentDate.get(Calendar.DATE) in START_DATE_KEY..END_DATE_KEY && - currentDate.get(Calendar.MONTH) == MONTH_KEY && currentDate.get(Calendar.YEAR) == YEAR_KEY + private suspend fun checkPromoPeriod(): Boolean { + val calendar = Calendar.getInstance() + val currentTime = calendar.timeInMillis + calendar.set(END_YEAR_KEY, END_MONTH_KEY, END_DAY_KEY, 0, 0, 0) + val endTime = calendar.timeInMillis + + val shouldShow = endTime - currentTime > 0 + if (!shouldShow) { + setNeverToShowToken() + setNeverToShowWallet() + } + return shouldShow } companion object { - private const val START_DATE_KEY = 15 - private const val END_DATE_KEY = 31 - private const val MONTH_KEY = 11 // December - private const val YEAR_KEY = 2023 // just in case + private const val END_DAY_KEY = 1 + private const val END_MONTH_KEY = 1 // February + private const val END_YEAR_KEY = 2024 } } \ No newline at end of file From 1995610c6a778f219dcfd9c12061e3663e9bc3e1 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 15:10:32 +0300 Subject: [PATCH 082/102] Updated on 2026-08-14 --- .../swap/ui/ChooseProviderBottomSheet.kt | 2 +- .../viewmodels/ExchangeStatusFactory.kt | 53 ++++++++++++------- .../viewmodels/TokenDetailsViewModel.kt | 5 ++ 3 files changed, 39 insertions(+), 21 deletions(-) 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 07091dfb12..36c705d5fb 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 @@ -42,7 +42,7 @@ private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetC style = TangemTheme.typography.subtitle1, color = TangemTheme.colors.text.primary1, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing10) + .padding(top = TangemTheme.dimens.spacing10), ) Text( text = stringResource(R.string.express_choose_providers_subtitle), diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 4c78d11155..8ff627eaec 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -18,7 +18,9 @@ import com.tangem.feature.swap.domain.models.domain.ExchangeStatus import com.tangem.feature.swap.domain.models.domain.ExchangeStatusModel import com.tangem.feature.swap.domain.models.domain.SavedSwapTransactionListModel import com.tangem.feature.tokendetails.presentation.tokendetails.state.SwapTransactionsState +import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState import com.tangem.feature.tokendetails.presentation.tokendetails.state.factory.TokenDetailsSwapTransactionsStateConverter +import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.exchange.ExchangeStatusBottomSheetConfig import com.tangem.utils.coroutines.CoroutineDispatcherProvider import kotlinx.collections.immutable.PersistentList import kotlinx.collections.immutable.persistentListOf @@ -41,6 +43,7 @@ internal class ExchangeStatusFactory( private val clickIntents: TokenDetailsClickIntents, private val appCurrencyProvider: Provider, private val analyticsEventsHandlerProvider: Provider, + private val currentStateProvider: Provider, private val userWalletId: UserWalletId, private val cryptoCurrency: CryptoCurrency, ) { @@ -64,6 +67,27 @@ internal class ExchangeStatusFactory( ) } + suspend fun removeTransactionOnBottomSheetClosed(): TokenDetailsState { + val state = currentStateProvider() + val bottomSheetConfig = state.bottomSheetConfig?.content as? ExchangeStatusBottomSheetConfig ?: return state + val selectedTx = bottomSheetConfig.value + + return if (selectedTx.activeStatus.isTerminal()) { + swapTransactionRepository.removeTransaction( + userWalletId = userWalletId, + fromCryptoCurrencyId = selectedTx.fromCryptoCurrencyId, + toCryptoCurrencyId = selectedTx.toCryptoCurrencyId, + txId = selectedTx.txId, + ) + val filteredTxs = state.swapTxs + .filterNot { it.txId == selectedTx.txId } + .toPersistentList() + state.copy(swapTxs = filteredTxs) + } else { + state + } + } + private fun getWalletCryptoCurrencies() = flow { val selectedWallet = getSelectedWalletSyncUseCase().fold( ifLeft = { null }, @@ -80,14 +104,16 @@ internal class ExchangeStatusFactory( suspend fun updateSwapTxStatuses(swapTxList: PersistentList) = withContext(dispatchers.io) { swapTxList.map { tx -> async { - val statusModel = getExchangeStatus(tx.txId) - swapTransactionsStateConverter - .updateTxStatus(tx, statusModel) - .removeIfFinished(statusModel?.status) + if (tx.activeStatus.isTerminal()) { + tx + } else { + val statusModel = getExchangeStatus(tx.txId) + swapTransactionsStateConverter + .updateTxStatus(tx, statusModel) + } } } .awaitAll() - .filterNotNull() .toPersistentList() } @@ -130,21 +156,8 @@ internal class ExchangeStatusFactory( ) } - private suspend fun SwapTransactionsState.removeIfFinished(status: ExchangeStatus?) = when (status) { - null -> null // not found - ExchangeStatus.Refunded, ExchangeStatus.Finished -> { - swapTransactionRepository.removeTransaction( - userWalletId = userWalletId, - fromCryptoCurrencyId = fromCryptoCurrencyId, - toCryptoCurrencyId = toCryptoCurrencyId, - txId = txId, - ) - null - } - else -> { - this - } - } + private fun ExchangeStatus?.isTerminal() = + this == ExchangeStatus.Refunded || this == ExchangeStatus.Finished || this == ExchangeStatus.Cancelled private fun toAnalyticStatus(status: ExchangeStatus?): ExchangeAnalyticsStatus? { return when (status) { diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index fee7ee1a64..b2d4ba321e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -548,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() } From c7aabfa5d32166643d411bdd954ecfec5a0c86a4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 16:07:33 +0400 Subject: [PATCH 083/102] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 237bd3e4a9..805056d1a5 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-423" +tangemBlockchainSdk = "release-app_5.4-434" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.4-315" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From c626a0c35f673539ec3dd6d8d9abc421826ed836 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 15:28:35 +0300 Subject: [PATCH 084/102] Updated on 2026-08-14 --- core/res/src/main/res/values-ru/strings.xml | 6 ++--- .../src/main/res/values-zh-rTW/strings.xml | 2 -- core/res/src/main/res/values/strings.xml | 6 ++--- .../core/ui/utils/BigDecimalFormatter.kt | 8 +++++- .../swap/converters/TokensDataConverter.kt | 18 ++++++++----- .../models/CurrenciesGroupWithFromCurrency.kt | 9 +++++++ .../feature/swap/models/SwapStateHolder.kt | 4 +-- .../swap/ui/ChooseProviderBottomSheet.kt | 1 + .../tangem/feature/swap/ui/StateBuilder.kt | 26 ++++++++++++------- .../feature/swap/ui/SwapScreenContent.kt | 2 +- .../feature/swap/ui/SwapSelectTokenScreen.kt | 1 + .../feature/swap/ui/SwapSuccessScreen.kt | 4 +-- .../feature/swap/viewmodels/SwapViewModel.kt | 5 ++-- .../components/TokenDetailsActionButton.kt | 2 +- .../viewmodels/TokenDetailsViewModel.kt | 1 - .../state/components/WalletManageButton.kt | 2 +- .../state/factory/TokenActionsProvider.kt | 2 +- 17 files changed, 61 insertions(+), 38 deletions(-) create mode 100644 features/swap/presentation/src/main/java/com/tangem/feature/swap/models/CurrenciesGroupWithFromCurrency.kt diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index afc6fda219..f9f34ddd5f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -189,8 +189,6 @@ Условия использования К сожалению, текущая версия приложения не готова к работе с этой картой, проверьте наличие обновлений Вы использовали карту от другого кошелька. Приложите карту, связанную с этим кошельком. - Вы получаете - Вы отправляете Мои токены У вас нет добавленных токенов. Добавьте токены для обмена Токены не найдены. Пожалуйста, попробуйте другой запрос @@ -524,6 +522,7 @@ Подтверждения считаются отраслевым стандартом для всех децентрализованных бирж и защищают ваш кошелек от доступа со стороны смарт-контракта без вашего разрешения. По замыслу смарт-контракты не могут получить доступ к вашим токенам, если вы не одобрите доступ со своей стороны. «Разблокируя» свои токены, вы даете смарт-контракту 1inch разрешение тратить ваши активы. Майнеры сети получают компенсацию за газ (оплачиваемый вами) за запись этого действия в блокчейне. Как только разрешение будет предоставлено, вы сможете обменять свой токен. Подтвердить Ошибка: %s + Вы отправляете Произошла ошибка. Пожалуйста, попробуйте еще раз. Дать разрешение Сильные колебания цены! @@ -542,13 +541,12 @@ Ваш кошелек Чтобы продолжить, вам нужно разрешить смарт-контракту 1inch использовать ваш %s Безлимитно - Вы отправляете - Вы получите Открыть в обозревателе В процессе Обменять Обмен %s на Котировки включают дополнительную комиссию Tangem в размере %s. Это помогает нам предоставлять первоклассный продукт. + Вы получите Другие токены Выберите токен Ваши токены diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index f595e68773..adb5903417 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -137,8 +137,6 @@ 服務條款 糟糕,當前版本的應用程序無法使用此卡,請檢查更新 您使用了另一個錢包中的卡。點按與此錢包關聯的卡片 - 您收到 - 您發送 以下信息是可選的。如果不想共享,可以將其刪除 告訴我們您缺少哪些功能,我們會盡力幫助您 請告訴我們你有什麼卡 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 6548a846ee..bf64538d4b 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -188,8 +188,6 @@ Terms of Service Oops, the current version of the application is not ready to work with this card, please check for updates. You have used a card from another wallet. Tap the card associated with this wallet - You receive - You send My tokens You haven\'t added any tokens yet. Add tokens via Market to swap No tokens found. Please try another request @@ -535,6 +533,7 @@ All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval. Approve Error: %s + You swap There was an error. Please try again. Give Permission High price impact! @@ -553,13 +552,12 @@ Your wallet To continue, grant 1inch smart contracts permission to use your %s Unlimited - You swap - You receive View in Explorer In progress Swap Swap %s for Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product. + You receive Other tokens Choose token Your tokens diff --git a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt index c64c860e28..e2d0c8dc42 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/utils/BigDecimalFormatter.kt @@ -22,7 +22,13 @@ object BigDecimalFormatter { roundingMode = RoundingMode.DOWN } - return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency" + return formatter.format(cryptoAmount).let { + if (cryptoCurrency.isEmpty()) { + it + } else { + it + "\u2009$cryptoCurrency" + } + } } fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String { diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt index d475bad6f2..936254d3fa 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/converters/TokensDataConverter.kt @@ -8,7 +8,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.feature.swap.domain.models.domain.CryptoCurrencySwapInfo -import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup +import com.tangem.feature.swap.models.CurrenciesGroupWithFromCurrency import com.tangem.feature.swap.models.SwapSelectTokenStateHolder import com.tangem.feature.swap.models.TokenBalanceData import com.tangem.feature.swap.models.TokenToSelectState @@ -21,17 +21,21 @@ class TokensDataConverter( private val onTokenSelected: (String) -> Unit, private val isBalanceHiddenProvider: Provider, private val appCurrencyProvider: Provider, -) : Converter { +) : Converter { - override fun convert(value: CurrenciesGroup): SwapSelectTokenStateHolder { + override fun convert(value: CurrenciesGroupWithFromCurrency): SwapSelectTokenStateHolder { + val group = value.group val availableTitle = TokenToSelectState.Title( resourceReference(R.string.exchange_tokens_available_tokens_header), ) val unavailableTitle = TokenToSelectState.Title( - resourceReference(R.string.exchange_tokens_available_tokens_header), + resourceReference( + R.string.exchange_tokens_unavailable_tokens_header, + wrappedList(value.fromCurrency.name), + ), ) return SwapSelectTokenStateHolder( - availableTokens = value.available.map { tokenWithBalanceToTokenToSelect(it, true) } + availableTokens = group.available.map { tokenWithBalanceToTokenToSelect(it, true) } .toMutableList() .apply { if (this.isNotEmpty()) { @@ -39,7 +43,7 @@ class TokensDataConverter( } } .toImmutableList(), - unavailableTokens = value.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } + unavailableTokens = group.unavailable.map { tokenWithBalanceToTokenToSelect(it, false) } .toMutableList() .apply { if (this.isNotEmpty()) { @@ -49,7 +53,7 @@ class TokensDataConverter( .toImmutableList(), onSearchEntered = onSearchEntered, onTokenSelected = onTokenSelected, - afterSearch = value.afterSearch, + afterSearch = group.afterSearch, ) } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/CurrenciesGroupWithFromCurrency.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/CurrenciesGroupWithFromCurrency.kt new file mode 100644 index 0000000000..8251580bb8 --- /dev/null +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/CurrenciesGroupWithFromCurrency.kt @@ -0,0 +1,9 @@ +package com.tangem.feature.swap.models + +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.feature.swap.domain.models.ui.CurrenciesGroup + +data class CurrenciesGroupWithFromCurrency( + val group: CurrenciesGroup, + val fromCurrency: CryptoCurrency, +) \ No newline at end of file diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt index 577c2e96af..2ccc7c9571 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/models/SwapStateHolder.kt @@ -105,12 +105,12 @@ sealed interface TransactionCardType { data class Inputtable( val onAmountChanged: ((String) -> Unit), val onFocusChanged: ((Boolean) -> Unit), - @StringRes override val headerResId: Int = R.string.exchange_send_view_header, + @StringRes override val headerResId: Int = R.string.swapping_from_title, ) : TransactionCardType data class ReadOnly( val highPriceImpact: String? = null, - @StringRes override val headerResId: Int = R.string.exchange_receive_view_header, + @StringRes override val headerResId: Int = R.string.swapping_to_title, ) : TransactionCardType } diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt index 36c705d5fb..d30d7591f2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/ChooseProviderBottomSheet.kt @@ -34,6 +34,7 @@ fun ChooseProviderBottomSheet(config: TangemBottomSheetConfig) { } } +@Suppress("LongMethod") @Composable private fun ChooseProviderBottomSheetContent(content: ChooseProviderBottomSheetConfig) { Column(horizontalAlignment = Alignment.CenterHorizontally) { 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 5b22001266..5246e8ed36 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/StateBuilder.kt @@ -107,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(), ), @@ -227,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( @@ -241,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, @@ -437,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( @@ -451,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, @@ -589,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, + ), ), ) } @@ -1220,10 +1228,10 @@ internal class StateBuilder( ) } - private fun CryptoCurrencyStatus.getFormattedAmount(): String { + private fun CryptoCurrencyStatus.getFormattedAmount(isNeedSymbol: Boolean): String { val amount = value.amount ?: return UNKNOWN_AMOUNT_SIGN - - return BigDecimalFormatter.formatCryptoAmount(amount, currency.symbol, currency.decimals) + val symbol = if (isNeedSymbol) currency.symbol else "" + return BigDecimalFormatter.formatCryptoAmount(amount, symbol, currency.decimals) } @Suppress("UnusedPrivateMember") diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt index f0d49bdd3c..e3c408bde2 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapScreenContent.kt @@ -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, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt index ed54834868..930039d697 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSelectTokenScreen.kt @@ -180,6 +180,7 @@ private fun TitleHeader(item: TokenToSelectState.Title, modifier: Modifier = Mod Text( text = item.title.resolveReference().uppercase(), style = TangemTheme.typography.overline, + color = TangemTheme.colors.text.tertiary, modifier = Modifier .padding( top = TangemTheme.dimens.spacing16, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index d540514a81..4b44fb9b18 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -60,7 +60,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad TransactionDoneTitle(titleRes = R.string.swapping_success_view_title, date = state.timestamp) SpacerH16() InputRowImage( - title = TextReference.Res(R.string.swapping_success_from_title), + title = TextReference.Res(R.string.swapping_from_title), subtitle = state.fromTokenAmount, caption = state.fromTokenFiatAmount, tokenIconState = state.fromTokenIconState ?: TokenIconState.Loading, @@ -70,7 +70,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad ) SpacerH16() InputRowImage( - title = TextReference.Res(R.string.swapping_success_to_title), + title = TextReference.Res(R.string.swapping_to_title), subtitle = state.toTokenAmount, caption = state.toTokenFiatAmount, tokenIconState = state.toTokenIconState ?: TokenIconState.Loading, diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt index ccccda01cc..a18226212a 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt @@ -216,11 +216,12 @@ internal class SwapViewModel @Inject constructor( } } - private fun updateTokensState(dataState: TokensDataStateExpress) { - val tokensDataState = if (isOrderReversed) dataState.fromGroup else dataState.toGroup + private fun updateTokensState(tokenDataState: TokensDataStateExpress) { + val tokensDataState = if (isOrderReversed) tokenDataState.fromGroup else tokenDataState.toGroup uiState = stateBuilder.addTokensToState( uiState = uiState, tokensDataState = tokensDataState, + fromToken = dataState.fromCryptoCurrency?.currency ?: initialCryptoCurrency, ) } diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index 8af1d13a13..3575d02e0f 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -78,7 +78,7 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { */ data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_swap), + text = TextReference.Res(id = R.string.common_exchange), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, enabled = enabled, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index b2d4ba321e..68e840d39a 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -125,7 +125,6 @@ internal class TokenDetailsViewModel @Inject constructor( clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, analyticsEventsHandlerProvider = Provider { analyticsEventsHandler }, - currentStateProvider = Provider { uiState }, userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt index fabf18401f..2628469375 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt @@ -88,7 +88,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Swap(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_swap), + text = TextReference.Res(id = R.string.common_exchange), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, enabled = enabled, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt index df989ef9b8..7a452a255c 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -78,7 +78,7 @@ internal class TokenActionsProvider( action = { clickIntents.onMultiCurrencySendClick(cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Swap -> { - title = resourceReference(R.string.common_swap) + title = resourceReference(R.string.common_exchange) icon = R.drawable.ic_exchange_horizontal_24 action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } } From 41a7398497ae19b45ec5ab8326b13b8ca7c3a2dd Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 15:53:58 +0300 Subject: [PATCH 085/102] Updated on 2026-08-14 --- .../tokendetails/viewmodels/TokenDetailsViewModel.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt index 68e840d39a..b2d4ba321e 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/TokenDetailsViewModel.kt @@ -125,6 +125,7 @@ internal class TokenDetailsViewModel @Inject constructor( clickIntents = this, appCurrencyProvider = Provider { selectedAppCurrencyFlow.value }, analyticsEventsHandlerProvider = Provider { analyticsEventsHandler }, + currentStateProvider = Provider { uiState }, userWalletId = userWalletId, cryptoCurrency = cryptoCurrency, ) From a7e98802e9833fba813193400e87dbf0124689ec Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 16:20:10 +0300 Subject: [PATCH 086/102] Updated on 2026-08-14 --- .../core/ui/components/buttons/common/TangemButtonSize.kt | 2 +- .../presentation/common/component/NetworkGroupItem.kt | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt index 0370fe0efe..bfae5cb313 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/buttons/common/TangemButtonSize.kt @@ -111,7 +111,7 @@ internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconP TangemButtonSize.Default, TangemButtonSize.WideAction, TangemButtonSize.TwoLines, - -> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32 + -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16 TangemButtonSize.Text -> when (icon) { is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16 is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing14 to TangemTheme.dimens.spacing16 diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt index ae4d06b0dc..c5af5a2cf6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/common/component/NetworkGroupItem.kt @@ -19,10 +19,7 @@ import org.burnoutcrew.reorderable.detectReorder @Composable internal fun NetworkGroupItem(networkName: String, modifier: Modifier = Modifier) { - InternalNetworkGroupItem( - modifier = modifier, - networkName = networkName, - ) + InternalNetworkGroupItem(modifier = modifier, networkName = networkName) } @Composable @@ -69,12 +66,13 @@ private fun InternalNetworkGroupItem( .background(TangemTheme.colors.background.primary) .padding(horizontal = TangemTheme.dimens.spacing14) .fillMaxWidth() - .heightIn(min = TangemTheme.dimens.size48), + .heightIn(min = TangemTheme.dimens.size40), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Text( text = stringResource(id = R.string.wallet_network_group_title, networkName), + modifier = Modifier.padding(top = TangemTheme.dimens.spacing12, bottom = TangemTheme.dimens.spacing8), style = TangemTheme.typography.subtitle2, color = TangemTheme.colors.text.tertiary, ) From 318d3edfa19e3bb6fae9f526eeae01f3104aaadc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 14:34:05 +0200 Subject: [PATCH 087/102] Updated on 2026-08-14 --- .../datasource/di/StatusCodeInterceptor.kt | 13 ++--- .../swap/converters/ErrorsDataConverter.kt | 52 +++++++++++++------ 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt index 7a6362efe3..4da4c8b17d 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StatusCodeInterceptor.kt @@ -36,13 +36,14 @@ class StatusCodeInterceptor : Interceptor { } private fun getBody(): String { - return "\"error\": {\n" + - " \"code\": 2290,\n" + - " \"description\": \"Core: receivedDecimals is not equal to expressDecimals\",\n" + + return "{\n" + + " \"error\": {\n" + + " \"code\": 2250,\n" + + " \"description\": \"Core: exchange too small amount\",\n" + " \"message\": \"Not valid\",\n" + - " \"receivedToDecimals\": 5,\n" + - " \"expressToDecimals\": 5\n" + - " }" + " \"minAmount\": 5\n" + + " }\n" + + "}" } companion object { diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt index 6ca55d7f04..fa28cf44f2 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/converters/ErrorsDataConverter.kt @@ -1,6 +1,7 @@ package com.tangem.feature.swap.converters import com.squareup.moshi.JsonAdapter +import com.tangem.datasource.api.express.models.response.ExpressError import com.tangem.datasource.api.express.models.response.ExpressErrorResponse import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.createFromAmountWithOffset @@ -21,28 +22,47 @@ internal class ErrorsDataConverter( 2220 -> DataError.ExchangeProviderNotActiveError(code = error.code) 2230 -> DataError.ExchangeProviderNotAvailableError(code = error.code) 2240 -> DataError.ExchangeNotPossibleError(code = error.code) - 2250 -> DataError.ExchangeTooSmallAmountError( - code = error.code, - amount = createFromAmountWithOffset( - requireNotNull(error.value?.minAmount), - requireNotNull(error.value?.decimals), - ), - ) - 2260 -> DataError.ExchangeNotEnoughAllowanceError( - code = error.code, - currentAllowance = requireNotNull(error.value?.currentAllowance), - ) + 2250 -> tryParseExchangeTooSmallAmountError(error = error) + 2260 -> tryParseExchangeNotEnoughAllowanceError(error = error) 2270 -> DataError.ExchangeNotEnoughBalanceError(code = error.code) 2280 -> DataError.ExchangeInvalidAddressError(code = error.code) - 2290 -> DataError.ExchangeInvalidFromDecimalsError( - code = error.code, - receivedFromDecimals = requireNotNull(error.value?.receivedFromDecimals), - expressFromDecimals = requireNotNull(error.value?.expressFromDecimals), - ) + 2290 -> tryParseExchangeInvalidFromDecimalsError(error = error) else -> DataError.UnknownErrorWithCode(error.code) } } catch (e: Exception) { return DataError.UnknownError } } + + private fun tryParseExchangeTooSmallAmountError(error: ExpressError): DataError { + val minAmount = error.value?.minAmount ?: return DataError.UnknownErrorWithCode(error.code) + val decimals = error.value?.decimals ?: return DataError.UnknownErrorWithCode(error.code) + + return DataError.ExchangeTooSmallAmountError( + code = error.code, + amount = createFromAmountWithOffset(minAmount, decimals), + ) + } + + private fun tryParseExchangeNotEnoughAllowanceError(error: ExpressError): DataError { + val currentAllowance = error.value?.currentAllowance ?: return DataError.UnknownErrorWithCode(error.code) + + return DataError.ExchangeNotEnoughAllowanceError( + code = error.code, + currentAllowance = currentAllowance, + ) + } + + private fun tryParseExchangeInvalidFromDecimalsError(error: ExpressError): DataError { + val receivedFromDecimals = error.value?.receivedFromDecimals ?: return DataError.UnknownErrorWithCode( + code = error.code, + ) + val expressFromDecimals = error.value?.expressFromDecimals ?: return DataError.UnknownErrorWithCode(error.code) + + return DataError.ExchangeInvalidFromDecimalsError( + code = error.code, + receivedFromDecimals = receivedFromDecimals, + expressFromDecimals = expressFromDecimals, + ) + } } \ No newline at end of file From 0f3bca4b5bb727312a92e8071cc0875737de0327 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 19:11:56 +0300 Subject: [PATCH 088/102] Updated on 2026-08-14 --- .../feature/swap/DefaultSwapRepository.kt | 5 ++++ .../feature/swap/domain/api/SwapRepository.kt | 3 +++ .../swap/domain/models/domain/Warning.kt | 8 ++++++ .../swap/domain/models/ui/SwapState.kt | 2 ++ .../feature/swap/domain/SwapInteractorImpl.kt | 13 ++++++++++ .../tangem/feature/swap/ui/StateBuilder.kt | 25 +++++++++++++++++++ 6 files changed, 56 insertions(+) create mode 100644 features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index 772502d688..e176e30ad1 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -23,6 +23,7 @@ import com.tangem.datasource.crypto.DataSignatureVerifier import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.legacy.WalletsStateHolder import com.tangem.domain.wallets.models.UserWalletId @@ -310,6 +311,10 @@ internal class DefaultSwapRepository @Inject constructor( } } + override suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? { + return walletManagersFacade.getExistentialDeposit(userWalletId, network) + } + private fun parseTxDetails(txDetailsJson: String): TxDetails? { return try { txDetailsMoshiAdapter.fromJson(txDetailsJson) diff --git a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt index e0182e8a5e..f22a1cef7b 100644 --- a/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt +++ b/features/swap/domain/api/src/main/java/com/tangem/feature/swap/domain/api/SwapRepository.kt @@ -2,6 +2,7 @@ package com.tangem.feature.swap.domain.api import arrow.core.Either import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.Network import com.tangem.domain.wallets.models.UserWalletId import com.tangem.feature.swap.domain.models.DataError import com.tangem.feature.swap.domain.models.domain.* @@ -70,4 +71,6 @@ interface SwapRepository { ): Either fun getNativeTokenForNetwork(networkId: String): CryptoCurrency + + suspend fun getExistentialDeposit(userWalletId: UserWalletId, network: Network): BigDecimal? } \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt new file mode 100644 index 0000000000..b52f4c5586 --- /dev/null +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/domain/Warning.kt @@ -0,0 +1,8 @@ +package com.tangem.feature.swap.domain.models.domain + +import java.math.BigDecimal + +sealed class Warning { + + data class ExistentialDepositWarning(val existentialDeposit: BigDecimal) : Warning() +} \ No newline at end of file diff --git a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt index ae50b2dd38..a7c1ff4892 100644 --- a/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt +++ b/features/swap/domain/models/src/main/java/com/tangem/feature/swap/domain/models/ui/SwapState.kt @@ -6,6 +6,7 @@ import com.tangem.feature.swap.domain.models.SwapAmount import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount import com.tangem.feature.swap.domain.models.domain.PreparedSwapConfigState import com.tangem.feature.swap.domain.models.domain.SwapDataModel +import com.tangem.feature.swap.domain.models.domain.Warning import java.math.BigDecimal sealed interface SwapState { @@ -25,6 +26,7 @@ sealed interface SwapState { val permissionState: PermissionDataState = PermissionDataState.Empty, val swapDataModel: SwapDataModel? = null, val txFee: TxFeeState, + val warnings: List = emptyList(), ) : SwapState data class EmptyAmountState( diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index aa1167d436..13b4415145 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -353,6 +353,16 @@ internal class SwapInteractorImpl @Inject constructor( ) } + private suspend fun manageWarnings(fromToken: CryptoCurrency): List { + val userWalletId = getSelectedWallet()?.walletId ?: return emptyList() + val existentialDeposit = repository.getExistentialDeposit(userWalletId, fromToken.network) + val warnings = mutableListOf() + if (existentialDeposit != null) { + warnings.add(Warning.ExistentialDepositWarning(existentialDeposit)) + } + return warnings + } + override suspend fun onSwap( swapProvider: SwapProvider, swapData: SwapDataModel?, @@ -782,6 +792,8 @@ internal class SwapInteractorImpl @Inject constructor( swapData = null, txFeeState = txFee, exchangeProviderType = exchangeProviderType, + ).copy( + warnings = manageWarnings(fromToken.currency), ) when (exchangeProviderType) { @@ -939,6 +951,7 @@ internal class SwapInteractorImpl @Inject constructor( ) swapState.copy( permissionState = PermissionDataState.Empty, + warnings = manageWarnings(fromToken.currency), preparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = true, isBalanceEnough = isBalanceIncludeFeeEnough, 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 5246e8ed36..cadb2e5087 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 @@ -304,6 +304,7 @@ internal class StateBuilder( fromToken: CryptoCurrency, ): List { val warnings = mutableListOf() + addDomainWarnings(quoteModel, warnings) if (!quoteModel.preparedSwapConfigState.isAllowedToSpend && quoteModel.preparedSwapConfigState.isFeeEnough && quoteModel.permissionState is PermissionDataState.PermissionReadyForRequest @@ -363,6 +364,30 @@ internal class StateBuilder( return warnings } + private fun addDomainWarnings(quoteModel: SwapState.QuotesLoadedState, warnings: MutableList) { + quoteModel.warnings.forEach { + when (it) { + is Warning.ExistentialDepositWarning -> { + warnings.add( + SwapWarning.GeneralWarning( + NotificationConfig( + title = resourceReference(R.string.warning_existential_deposit_title), + subtitle = resourceReference( + R.string.warning_existential_deposit_message, + wrappedList( + quoteModel.fromTokenInfo.cryptoCurrencyStatus.currency.name, + it.existentialDeposit.toPlainString(), + ), + ), + iconResId = R.drawable.img_attention_20, + ), + ), + ) + } + } + } + } + private fun addUnableCoverFeeWarning( quoteModel: SwapState.QuotesLoadedState, fromToken: CryptoCurrency, From be704a7f0b1069fbd6adf1db9864e6142cfecf96 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 21:46:50 +0300 Subject: [PATCH 089/102] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index 13b4415145..a1420cc27f 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -353,12 +353,18 @@ internal class SwapInteractorImpl @Inject constructor( ) } - private suspend fun manageWarnings(fromToken: CryptoCurrency): List { + private suspend fun manageWarnings(fromToken: CryptoCurrency, amount: SwapAmount): List { val userWalletId = getSelectedWallet()?.walletId ?: return emptyList() val existentialDeposit = repository.getExistentialDeposit(userWalletId, fromToken.network) val warnings = mutableListOf() if (existentialDeposit != null) { - warnings.add(Warning.ExistentialDepositWarning(existentialDeposit)) + val nativeBalance = userWalletManager.getNativeTokenBalance( + fromToken.network.backendId, + fromToken.network.derivationPath.value, + ) ?: ProxyAmount.empty() + if (nativeBalance.value.minus(amount.value) < existentialDeposit) { + warnings.add(Warning.ExistentialDepositWarning(existentialDeposit)) + } } return warnings } @@ -793,7 +799,7 @@ internal class SwapInteractorImpl @Inject constructor( txFeeState = txFee, exchangeProviderType = exchangeProviderType, ).copy( - warnings = manageWarnings(fromToken.currency), + warnings = manageWarnings(fromToken.currency, amount), ) when (exchangeProviderType) { @@ -951,7 +957,7 @@ internal class SwapInteractorImpl @Inject constructor( ) swapState.copy( permissionState = PermissionDataState.Empty, - warnings = manageWarnings(fromToken.currency), + warnings = manageWarnings(fromToken.currency, amount), preparedSwapConfigState = PreparedSwapConfigState( isAllowedToSpend = true, isBalanceEnough = isBalanceIncludeFeeEnough, From 3cb4019a305d84e82c06c709bccccfca957ce4fc Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 26 Dec 2023 22:05:51 +0300 Subject: [PATCH 090/102] Updated on 2026-08-14 --- .../com/tangem/feature/swap/models/SwapStateHolder.kt | 1 + .../main/java/com/tangem/feature/swap/ui/StateBuilder.kt | 4 ++-- .../java/com/tangem/feature/swap/ui/SwapScreenContent.kt | 8 +++++++- 3 files changed, 10 insertions(+), 3 deletions(-) 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 2ccc7c9571..d5e794a1a1 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 @@ -145,6 +145,7 @@ sealed interface SwapWarning { data class TooSmallAmountWarning(val notificationConfig: NotificationConfig) : SwapWarning data class UnableToCoverFeeWarning(val notificationConfig: NotificationConfig) : SwapWarning data class GeneralWarning(val notificationConfig: NotificationConfig) : SwapWarning + data class GeneralInformational(val notificationConfig: NotificationConfig) : SwapWarning data class TransactionInProgressWarning(val title: TextReference, val description: TextReference) : SwapWarning } 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 cadb2e5087..e761bee9de 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 @@ -369,7 +369,7 @@ internal class StateBuilder( when (it) { is Warning.ExistentialDepositWarning -> { warnings.add( - SwapWarning.GeneralWarning( + SwapWarning.GeneralInformational( NotificationConfig( title = resourceReference(R.string.warning_existential_deposit_title), subtitle = resourceReference( @@ -379,7 +379,7 @@ internal class StateBuilder( it.existentialDeposit.toPlainString(), ), ), - iconResId = R.drawable.img_attention_20, + iconResId = R.drawable.ic_alert_circle_24, ), ), ) 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 e3c408bde2..cb4d02dbf9 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 @@ -313,7 +313,7 @@ private fun SwapButton(state: SwapStateHolder, modifier: Modifier = Modifier) { } } -@Suppress("LongMethod") +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable private fun SwapWarnings(warnings: List) { Column( @@ -369,6 +369,12 @@ private fun SwapWarnings(warnings: List) { config = warning.notificationConfig, ) } + is SwapWarning.GeneralInformational -> { + Notification( + config = warning.notificationConfig, + iconTint = TangemTheme.colors.icon.accent, + ) + } is SwapWarning.TransactionInProgressWarning -> { CardWithIcon( title = warning.title.resolveReference(), From f280dff8061ead9665521debb7b3079549129b78 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Dec 2023 14:50:03 +0300 Subject: [PATCH 091/102] Updated on 2026-08-14 --- .../datasource/local/swaptx/SwapTransactionStatusStore.kt | 1 + core/res/src/main/res/values/strings.xml | 2 +- .../tokendetails/viewmodels/ExchangeStatusFactory.kt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt index 59bd30030e..2bcb9a3dbb 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/swaptx/SwapTransactionStatusStore.kt @@ -15,4 +15,5 @@ enum class ExchangeAnalyticsStatus(val value: String) { Fail("Fail"), KYC("KYC"), Refunded("Refunded"), + Cancelled("Canceled"), } \ No newline at end of file diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index bf64538d4b..8665fe2aa8 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -579,7 +579,7 @@ No rate Exchange this token for other assets in your portfolio Introducing cross-chain swaps - Exchange now + Swap now contract: %s You don\'t have any transactions yet Failed to load transaction history.\nClick on reload button to update the information. diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt index 8ff627eaec..ffb90e01de 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/viewmodels/ExchangeStatusFactory.kt @@ -171,6 +171,7 @@ internal class ExchangeStatusFactory( ExchangeStatus.Failed -> ExchangeAnalyticsStatus.Fail ExchangeStatus.Finished -> ExchangeAnalyticsStatus.Done ExchangeStatus.Refunded -> ExchangeAnalyticsStatus.Refunded + ExchangeStatus.Cancelled -> ExchangeAnalyticsStatus.Cancelled else -> null } } From 5b62695db4733f366cf392805af23cdad4fead61 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Dec 2023 16:14:44 +0300 Subject: [PATCH 092/102] Updated on 2026-08-14 --- .../tokendetails/state/components/TokenDetailsActionButton.kt | 2 +- .../presentation/wallet/state/components/WalletManageButton.kt | 2 +- .../presentation/wallet/state/factory/TokenActionsProvider.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index 3575d02e0f..8af1d13a13 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -78,7 +78,7 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { */ data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_exchange), + text = TextReference.Res(id = R.string.common_swap), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, enabled = enabled, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt index 2628469375..fabf18401f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt @@ -88,7 +88,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Swap(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_exchange), + text = TextReference.Res(id = R.string.common_swap), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, enabled = enabled, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt index 7a452a255c..df989ef9b8 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -78,7 +78,7 @@ internal class TokenActionsProvider( action = { clickIntents.onMultiCurrencySendClick(cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Swap -> { - title = resourceReference(R.string.common_exchange) + title = resourceReference(R.string.common_swap) icon = R.drawable.ic_exchange_horizontal_24 action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } } From 73bd0bf0839ad8c0190d3c80123b695cba3e37f4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 27 Dec 2023 17:23:49 +0300 Subject: [PATCH 093/102] Updated on 2026-08-14 --- core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt index d2c890aad3..f87f80fe61 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemTheme.kt @@ -175,7 +175,7 @@ private fun darkThemeColors(): TangemColors { key = TangemColorPalette.White, ), stroke = TangemColors.Stroke( - primary = TangemColorPalette.Dark5, + primary = TangemColorPalette.Dark4, secondary = TangemColorPalette.Dark1, transparency = TangemColorPalette.Dark6, ), From f93b22f7a40c6f8783df5a94bc492a0b591062b8 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Dec 2023 12:24:30 +0300 Subject: [PATCH 094/102] Updated on 2026-08-14 --- .../api/express/models/response/ExchangeStatusResponse.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt index 3be2a6ebec..6af6ee2409 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/express/models/response/ExchangeStatusResponse.kt @@ -49,7 +49,7 @@ enum class ExchangeStatus { @Json(name = "verifying") VERIFYING, - @Json(name = "cancelled") + @Json(name = "expired") CANCELLED, } From 555ee2535c20648ad9d14284d34b931918187904 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Dec 2023 13:46:27 +0300 Subject: [PATCH 095/102] Updated on 2026-08-14 --- .../feature/swap/domain/SwapInteractorImpl.kt | 43 +++++++++++-------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt index a1420cc27f..38f55b3561 100644 --- a/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt +++ b/features/swap/domain/src/main/java/com/tangem/feature/swap/domain/SwapInteractorImpl.kt @@ -852,19 +852,12 @@ internal class SwapInteractorImpl @Inject constructor( amount: SwapAmount, fromToken: CryptoCurrency, ): IncludeFeeInAmount { - if (fromToken is CryptoCurrency.Token) { - return IncludeFeeInAmount.Excluded - } - val tokenForFeeBalance = userWalletManager.getNativeTokenBalance( networkId, fromToken.network.derivationPath.value, ) ?: ProxyAmount.empty() - if (amount.value > tokenForFeeBalance.value) { - return IncludeFeeInAmount.BalanceNotEnough - } val feeValue = when (txFee) { TxFeeState.Empty -> BigDecimal.ZERO is TxFeeState.MultipleFeeState -> txFee.priorityFee.feeValue @@ -872,19 +865,33 @@ internal class SwapInteractorImpl @Inject constructor( } val amountWithFee = amount.value + feeValue - return if (amountWithFee < tokenForFeeBalance.value) { - IncludeFeeInAmount.Excluded - } else { - if (feeValue < amount.value) { - IncludeFeeInAmount.Included( - SwapAmount( - tokenForFeeBalance.value - feeValue, - transactionManager.getNativeTokenDecimals(networkId), - ), - ) - } else { + + return when { + fromToken is CryptoCurrency.Token -> { + if (feeValue > tokenForFeeBalance.value) { + IncludeFeeInAmount.BalanceNotEnough + } else { + IncludeFeeInAmount.Excluded + } + } + amount.value > tokenForFeeBalance.value -> { IncludeFeeInAmount.BalanceNotEnough } + amountWithFee < tokenForFeeBalance.value -> { + IncludeFeeInAmount.Excluded + } + else -> { + if (feeValue < amount.value) { + IncludeFeeInAmount.Included( + SwapAmount( + tokenForFeeBalance.value - feeValue, + transactionManager.getNativeTokenDecimals(networkId), + ), + ) + } else { + IncludeFeeInAmount.BalanceNotEnough + } + } } } From 4bb40b2aa6909296f8a6b977e30a3d77451f94a5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Dec 2023 16:04:39 +0300 Subject: [PATCH 096/102] Updated on 2026-08-14 --- .../main/java/com/tangem/feature/swap/DefaultSwapRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt index e176e30ad1..c241793b1a 100644 --- a/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt +++ b/features/swap/data/src/main/java/com/tangem/feature/swap/DefaultSwapRepository.kt @@ -293,7 +293,7 @@ internal class DefaultSwapRepository @Inject constructor( if (txDetails.requestId != requestId) { return@withContext DataError.InvalidRequestIdError().left() } - if (toAddress != txDetails.payoutAddress) { + if (!toAddress.equals(txDetails.payoutAddress, ignoreCase = true)) { return@withContext DataError.InvalidPayoutAddressError().left() } expressDataConverter.convert( From 9cefe0b59664adc7f992290c964fe6d13c74b2ce Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 28 Dec 2023 17:15:25 +0400 Subject: [PATCH 097/102] Updated on 2026-08-14 --- .../walletmanager/DefaultWalletManagersFacade.kt | 2 +- .../walletmanager/utils/WalletManagerFactory.kt | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 03cdd7a611..72290294eb 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -251,7 +251,7 @@ class DefaultWalletManagersFacade( derivationPath = derivationPath, ) if (walletManager == null || blockchain == Blockchain.Unknown) { - Timber.w("Unable to get a wallet manager for blockchain: $blockchain") + Timber.w("Unable to create or find a wallet manager for $blockchain") return UpdateWalletManagerResult.Unreachable } diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt index cd0b2a670b..9e83e20e3a 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/utils/WalletManagerFactory.kt @@ -10,6 +10,7 @@ import com.tangem.domain.common.DerivationStyleProvider import com.tangem.domain.common.extensions.makeWalletManagerForApp import com.tangem.domain.common.util.derivationStyleProvider import com.tangem.domain.models.scan.ScanResponse +import timber.log.Timber internal class WalletManagerFactory( private val configManager: ConfigManager, @@ -26,11 +27,16 @@ internal class WalletManagerFactory( ): WalletManager? { val derivationParams = getDerivationParams(derivationPath, scanResponse.derivationStyleProvider) - return sdkWalletManagerFactory.makeWalletManagerForApp( - scanResponse = scanResponse, - blockchain = blockchain, - derivationParams = derivationParams, - ) + return try { + sdkWalletManagerFactory.makeWalletManagerForApp( + scanResponse = scanResponse, + blockchain = blockchain, + derivationParams = derivationParams, + ) + } catch (e: Throwable) { + Timber.w(e, "Failed to create wallet manager for $blockchain") + null + } } private fun getDerivationParams( From 287109ec57c8408115f10546c7e65662f1410936 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Dec 2023 16:07:14 +0300 Subject: [PATCH 098/102] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- .../tokendetails/state/components/TokenDetailsActionButton.kt | 2 +- .../presentation/wallet/state/components/WalletManageButton.kt | 2 +- .../presentation/wallet/state/factory/TokenActionsProvider.kt | 2 +- .../converter/MultiWalletCurrencyActionsConverter.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 0be5650f8f..41e1c8f27f 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 0be5650f8fbcdff22c23fe2e2c7d754c4275a6f6 +Subproject commit 41e1c8f27f8243263797d2530f1650089c3b1a4b diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt index 8af1d13a13..63130c7889 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/components/TokenDetailsActionButton.kt @@ -78,7 +78,7 @@ internal sealed class TokenDetailsActionButton(val config: ActionButtonConfig) { */ data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : TokenDetailsActionButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_swap), + text = TextReference.Res(id = R.string.swapping_swap_action), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, enabled = enabled, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt index fabf18401f..797aaa2711 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/components/WalletManageButton.kt @@ -88,7 +88,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) { */ data class Swap(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton( config = ActionButtonConfig( - text = TextReference.Res(id = R.string.common_swap), + text = TextReference.Res(id = R.string.swapping_swap_action), iconResId = R.drawable.ic_exchange_vertical_24, onClick = onClick, enabled = enabled, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt index df989ef9b8..4089953d56 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/factory/TokenActionsProvider.kt @@ -78,7 +78,7 @@ internal class TokenActionsProvider( action = { clickIntents.onMultiCurrencySendClick(cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Swap -> { - title = resourceReference(R.string.common_swap) + title = resourceReference(R.string.swapping_swap_action) icon = R.drawable.ic_exchange_horizontal_24 action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt index 270fa05c9c..e43e0fb2a6 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state2/transformers/converter/MultiWalletCurrencyActionsConverter.kt @@ -69,7 +69,7 @@ internal class MultiWalletCurrencyActionsConverter( action = { clickIntents.onSendClick(cryptoCurrencyStatus) } } is TokenActionsState.ActionState.Swap -> { - title = resourceReference(R.string.common_swap) + title = resourceReference(R.string.swapping_swap_action) icon = R.drawable.ic_exchange_horizontal_24 action = { clickIntents.onSwapClick(cryptoCurrencyStatus) } } From eea6703cb104cc68a8530450e24d3a3ae5bca891 Mon Sep 17 00:00:00 2001 From: Tangem Date: Fri, 29 Dec 2023 16:44:09 +0300 Subject: [PATCH 099/102] Updated on 2026-08-14 --- app/src/main/assets/tangem-app-config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index 41e1c8f27f..4a1be08512 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit 41e1c8f27f8243263797d2530f1650089c3b1a4b +Subproject commit 4a1be08512fe2b45504f2bdaa0e35f5c11e11319 From 1b61e8bcae8dd017e6f1ca994f278271918c6285 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jan 2024 11:09:29 +0100 Subject: [PATCH 100/102] Updated on 2026-08-14 --- .../core/ui/components/transactions/TransactionDoneTitle.kt | 2 +- .../main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt index d576eafe66..2adcbdc369 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/transactions/TransactionDoneTitle.kt @@ -44,7 +44,7 @@ fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifie style = TangemTheme.typography.h3, color = TangemTheme.colors.text.primary1, modifier = Modifier - .padding(top = TangemTheme.dimens.spacing32), + .padding(top = TangemTheme.dimens.spacing16), ) Text( text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()), diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt index 4b44fb9b18..902a000aef 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/SwapSuccessScreen.kt @@ -67,6 +67,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), + showNetworkIcon = true, ) SpacerH16() InputRowImage( @@ -77,6 +78,7 @@ private fun SwapSuccessScreenContent(state: SwapSuccessStateHolder, padding: Pad modifier = Modifier .clip(TangemTheme.shapes.roundedCornersXMedium) .background(TangemTheme.colors.background.action), + showNetworkIcon = true, ) SpacerH16() InputRowBestRate( From 59dde62364b0bfa84b758fd6df53beb57767a0e5 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jan 2024 14:24:27 +0300 Subject: [PATCH 101/102] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 805056d1a5..de4e143ada 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,7 +88,7 @@ spr-client = "3.6.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.4-434" +tangemBlockchainSdk = "release-app_5.4-439" #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 ^ From a73df2ac777c3612133546ad0a8e807b5f212f50 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 10 Jan 2024 16:32:16 +0300 Subject: [PATCH 102/102] Updated on 2026-08-14 --- .../domain/walletconnect/WalletConnectSdkHelper.kt | 2 +- .../com/tangem/tap/proxy/TransactionManagerImpl.kt | 12 ++++++------ .../tangem/domain/transaction/error/GetFeeError.kt | 1 - .../domain/transaction/usecase/EstimateFeeUseCase.kt | 5 +++-- .../com/tangem/feature/swap/ui/WebViewBottomSheet.kt | 2 +- .../tangem/feature/swap/viewmodels/SwapViewModel.kt | 4 ++-- .../analytics/utils/WalletWarningsAnalyticsSender.kt | 1 + 7 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt index a4b9510d38..2db93bfead 100644 --- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt +++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt @@ -4,7 +4,7 @@ import com.tangem.Message import com.tangem.blockchain.blockchains.ethereum.EthereumGasLoader import com.tangem.blockchain.blockchains.ethereum.EthereumTransactionExtras import com.tangem.blockchain.blockchains.ethereum.EthereumUtils -import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.Companion.toKeccak +import com.tangem.blockchain.blockchains.ethereum.EthereumUtils.toKeccak import com.tangem.blockchain.common.* import com.tangem.blockchain.common.transaction.Fee import com.tangem.blockchain.extensions.Result diff --git a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt index 90b2da621e..a7899eace5 100644 --- a/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt +++ b/app/src/main/java/com/tangem/tap/proxy/TransactionManagerImpl.kt @@ -1,5 +1,6 @@ package com.tangem.tap.proxy +import androidx.core.text.isDigitsOnly import com.google.firebase.crashlytics.FirebaseCrashlytics import com.tangem.Message import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras @@ -23,8 +24,6 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.card.repository.CardSdkConfigRepository import com.tangem.domain.common.extensions.fromNetworkId import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.features.send.impl.presentation.viewmodel.XlmMemoType -import com.tangem.features.send.impl.presentation.viewmodel.determineXlmMemoType import com.tangem.lib.crypto.TransactionManager import com.tangem.lib.crypto.models.* import com.tangem.lib.crypto.models.transactions.SendTxResult @@ -153,11 +152,12 @@ class TransactionManagerImpl( if (memo == null) return null return when (blockchain) { Blockchain.Stellar -> { - val xmlMemo = when (determineXlmMemoType(memo)) { - XlmMemoType.TEXT -> StellarMemo.Text(memo) - XlmMemoType.ID -> StellarMemo.Id(memo.toBigInteger()) + val xlmMemo = if (memo.isNotEmpty() && memo.isDigitsOnly()) { + StellarMemo.Id(memo.toBigInteger()) + } else { + StellarMemo.Text(memo) } - StellarTransactionExtras(xmlMemo) + StellarTransactionExtras(xlmMemo) } Blockchain.Binance -> BinanceTransactionExtras(memo) Blockchain.XRP -> memo.toLongOrNull()?.let { XrpTransactionBuilder.XrpTransactionExtras(it) } diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt index e6669368f9..f2e3f7f60f 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/error/GetFeeError.kt @@ -2,6 +2,5 @@ package com.tangem.domain.transaction.error sealed class GetFeeError { data class DataError(val cause: Throwable?) : GetFeeError() - object UnknownError : GetFeeError() } \ No newline at end of file diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt index 28f48358a0..a33088d612 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/EstimateFeeUseCase.kt @@ -29,7 +29,7 @@ class EstimateFeeUseCase( amount: BigDecimal, userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): Flow> { + ): Flow> { return flow { val result = walletManagersFacade.estimateFee( amount = convertCryptoCurrencyToAmount(cryptoCurrency, amount), @@ -39,7 +39,8 @@ class EstimateFeeUseCase( val maybeFee = when (result) { is Result.Success -> result.data.right() - else -> GetFeeError.DataError.left() + is Result.Failure -> GetFeeError.DataError(result.error).left() + null -> GetFeeError.UnknownError.left() } emit(maybeFee) }.flowOn(dispatcher.io) diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt index 4a068cddbd..eb2df9fe19 100644 --- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt +++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/ui/WebViewBottomSheet.kt @@ -20,7 +20,7 @@ import com.tangem.feature.swap.models.states.WebViewBottomSheetConfig fun WebViewBottomSheet(config: TangemBottomSheetConfig) { TangemBottomSheet( config = config, - contentColor = TangemTheme.colors.background.tertiary, + containerColor = TangemTheme.colors.background.tertiary, ) { content: WebViewBottomSheetConfig -> WebViewBottomSheetContent( WebViewBottomSheetConfig(url = content.url), 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 cde4df0693..c081db8e65 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 @@ -5,7 +5,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.* import arrow.core.getOrElse -import com.tangem.common.Provider import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.core.ui.utils.InputNumberFormatter import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase @@ -32,6 +31,7 @@ import com.tangem.feature.swap.presentation.SwapFragment import com.tangem.feature.swap.router.SwapNavScreen import com.tangem.feature.swap.router.SwapRouter import com.tangem.feature.swap.ui.StateBuilder +import com.tangem.utils.Provider import com.tangem.utils.coroutines.* import com.tangem.utils.isNullOrZero import dagger.hilt.android.lifecycle.HiltViewModel @@ -606,7 +606,7 @@ internal class SwapViewModel @Inject constructor( afterSearch = true, ), - ) + ) } else { tokenDataState.copy( toGroup = tokenDataState.toGroup.copy( diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt index 4cece938af..bb4427955e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/analytics/utils/WalletWarningsAnalyticsSender.kt @@ -28,6 +28,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.LowSignatures, is WalletNotification.Warning.NetworksUnreachable, is WalletNotification.Warning.SomeNetworksUnreachable, + is WalletNotification.SwapPromo, -> null } }