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 40ba3c5084..5990835735 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 @@ -370,4 +370,16 @@ internal object TokensDomainModule { networksRepository = networksRepository, ) } + + @Provides + @Singleton + fun provideRefreshMultiCurrencyWalletQuotesUseCase( + currenciesRepository: CurrenciesRepository, + quotesRepository: QuotesRepository, + ): RefreshMultiCurrencyWalletQuotesUseCase { + return RefreshMultiCurrencyWalletQuotesUseCase( + currenciesRepository = currenciesRepository, + quotesRepository = quotesRepository, + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 34944cf67a..d725069217 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -370,6 +370,8 @@ internal class AddCustomTokenViewModel @Inject constructor( viewModelScope.launch(dispatchers.main) { runCatching(dispatchers.io) { val tokenAddress = convertTokenAddress(selectedNetwork, address) + ?: error("TokenAddress is invalid") + featureInteractor.findToken(address = tokenAddress, blockchain = selectedNetwork) } .onSuccess { token -> @@ -602,7 +604,8 @@ internal class AddCustomTokenViewModel @Inject constructor( val contractAddress = convertTokenAddress( blockchain = networkSelectorValue, address = uiState.form.contractAddressInputField.value, - ) + ) ?: return false // invalid address can't be "already added" + return currentCryptoCurrencies .filterIsInstance() .any { token -> @@ -859,7 +862,8 @@ internal class AddCustomTokenViewModel @Inject constructor( val contractAddress = convertTokenAddress( blockchain = blockchain, address = foundToken?.network?.contractAddress ?: uiState.form.contractAddressInputField.value, - ) + ) ?: error("Contract address is invalid") // impossible to add a token with invalid address + CustomCurrency.CustomToken( token = Token( name = uiState.form.tokenNameInputField.value, @@ -897,7 +901,8 @@ internal class AddCustomTokenViewModel @Inject constructor( } } - private fun convertTokenAddress(blockchain: Blockchain, address: String): String { + /** Convert [address] to single address for specific [blockchain] or return null if invalid */ + private fun convertTokenAddress(blockchain: Blockchain, address: String): String? { return when (blockchain) { Blockchain.Hedera, Blockchain.HederaTestnet -> hederaAddressConverter.convertToTokenId(address) Blockchain.Cardano -> { 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 8b224858c1..bda2f0e17f 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 @@ -23,6 +23,9 @@ data class ExchangeProvider( @Json(name = "privacyPolicy") val privacyPolicy: String?, + + @Json(name = "recommended") + val isRecommended: Boolean = false, ) 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 453bf3dd9b..e70467d63f 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -239,6 +239,7 @@ Доступно с %s Недоступно для этой пары Требуется разрешение + Рекомендовано Условиями использования Токены не найдены. Пожалуйста, попробуйте другой запрос ID: %s diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index c712fdab72..40b0b45e01 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -238,6 +238,7 @@ Available from %s Unavailable for this pair Permission Required + Recommended Terms of Use No tokens found. Please try another request ID: %s diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt index 1dee0062ce..f6c7ade8b4 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultQuotesRepository.kt @@ -51,6 +51,19 @@ internal class DefaultQuotesRepository( .flowOn(dispatchers.io) } + override suspend fun fetchQuotes(currenciesIds: Set) { + withContext(dispatchers.io) { + val selectedAppCurrency = requireNotNull( + value = appPreferencesStore.getObjectSyncOrNull( + key = PreferencesKeys.SELECTED_APP_CURRENCY_KEY, + ), + lazyMessage = { "Unable to get selected application currency to update quotes" }, + ) + + fetchExpiredQuotes(currenciesIds, selectedAppCurrency.id, true) + } + } + override suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set { return withContext(dispatchers.io) { val selectedAppCurrency = requireNotNull( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt new file mode 100644 index 0000000000..e108b21819 --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/RefreshMultiCurrencyWalletQuotesUseCase.kt @@ -0,0 +1,53 @@ +package com.tangem.domain.tokens + +import arrow.core.Either +import arrow.core.getOrElse +import arrow.core.raise.catch +import arrow.core.raise.either +import com.tangem.domain.tokens.error.QuotesError +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.repository.CurrenciesRepository +import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.models.UserWalletId +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope + +class RefreshMultiCurrencyWalletQuotesUseCase( + private val quotesRepository: QuotesRepository, + private val currenciesRepository: CurrenciesRepository, +) { + + suspend operator fun invoke(userWalletId: UserWalletId): Either { + return either { + val currencies = fetchCurrencies(userWalletId = userWalletId) + .getOrElse { raise(QuotesError.DataError(it)) } + + coroutineScope { + val fetchQuotes = async { + fetchQuotes( + currenciesIds = currencies.mapTo(destination = hashSetOf(), transform = CryptoCurrency::id), + ) + } + + awaitAll(fetchQuotes) + } + } + } + + private suspend fun fetchCurrencies(userWalletId: UserWalletId): Either> { + return either { + catch( + block = { currenciesRepository.getMultiCurrencyWalletCurrenciesSync(userWalletId, false) }, + catch = { raise(it) }, + ) + } + } + + private suspend fun fetchQuotes(currenciesIds: Set) { + catch( + block = { quotesRepository.fetchQuotes(currenciesIds) }, + catch = { /* Ignore error */ }, + ) + } +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt new file mode 100644 index 0000000000..f5601eb6db --- /dev/null +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/error/QuotesError.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.tokens.error + +sealed class QuotesError { + data class DataError(val cause: Throwable) : QuotesError() +} \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt index 64a0d29a12..4ebd284421 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/QuotesRepository.kt @@ -31,4 +31,6 @@ interface QuotesRepository { suspend fun getQuotesSync(currenciesIds: Set, refresh: Boolean): Set suspend fun getQuoteSync(currencyId: CryptoCurrency.ID): Quote? + + suspend fun fetchQuotes(currenciesIds: Set) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt index ae960f191a..3d45f98879 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockQuotesRepository.kt @@ -25,4 +25,6 @@ internal class MockQuotesRepository( return quotes.map { it.getOrElse { e -> throw e } }.first() .first { it.rawCurrencyId == currencyId.rawCurrencyId } } + + override suspend fun fetchQuotes(currenciesIds: Set) {} } \ No newline at end of file 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 cdaa277dac..ec5a88e56e 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 @@ -45,6 +45,7 @@ class SwapPairInfoConverter : Converter BestTradeItem( - Modifier.padding(start = TangemTheme.dimens.spacing4), - ) - ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem( - Modifier.padding(start = TangemTheme.dimens.spacing4), - ) - ProviderState.AdditionalBadge.Empty -> { - // no-op - } + ProviderState.AdditionalBadge.BestTrade -> BestTradeItem(badgeModifier) + ProviderState.AdditionalBadge.PermissionRequired -> PermissionBadgeItem(badgeModifier) + ProviderState.AdditionalBadge.Recommended -> RecommendedItem(badgeModifier) + ProviderState.AdditionalBadge.Empty -> Unit } } Row( @@ -385,6 +382,23 @@ private fun PermissionBadgeItem(modifier: Modifier = Modifier) { } } +@Composable +private fun RecommendedItem(modifier: Modifier = Modifier) { + Box( + modifier = modifier.background( + color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f), + shape = TangemTheme.shapes.roundedCornersLarge, + ), + ) { + Text( + text = stringResource(id = R.string.express_provider_recommended), + style = TangemTheme.typography.caption1, + color = TangemTheme.colors.icon.accent, + modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing6), + ) + } +} + // region Preview @Preview(showBackground = true, widthDp = 360) @Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) 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 880518dcdb..da536bd046 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 @@ -1541,7 +1541,9 @@ internal class StateBuilder( val fromCurrencySymbol = fromTokenInfo.cryptoCurrencyStatus.currency.symbol val toCurrencySymbol = toTokenInfo.cryptoCurrencyStatus.currency.symbol val rateString = "1 $fromCurrencySymbol ≈ $rate $toCurrencySymbol" - val badge = if (isNeedBestRateBadge && isBestRate) { + val badge = if (isRecommended) { + ProviderState.AdditionalBadge.Recommended + } else if (isNeedBestRateBadge && isBestRate) { ProviderState.AdditionalBadge.BestTrade } else { ProviderState.AdditionalBadge.Empty @@ -1555,7 +1557,7 @@ internal class StateBuilder( additionalBadge = badge, selectionType = selectionType, percentLowerThenBest = PercentDifference.Empty, - namePrefix = ProviderState.PrefixType.PROVIDED_BY, + namePrefix = ProviderState.PrefixType.NONE, onProviderClick = onProviderClick, ) } @@ -1570,6 +1572,8 @@ internal class StateBuilder( val rateString = toTokenInfo.tokenAmount.getFormattedCryptoAmount(toTokenInfo.cryptoCurrencyStatus.currency) val additionalBadge = if (state.permissionState is PermissionDataState.PermissionReadyForRequest) { ProviderState.AdditionalBadge.PermissionRequired + } else if (isRecommended) { + ProviderState.AdditionalBadge.Recommended } else { ProviderState.AdditionalBadge.Empty } @@ -1619,11 +1623,7 @@ internal class StateBuilder( subtitle = alertText, additionalBadge = ProviderState.AdditionalBadge.Empty, percentLowerThenBest = PercentDifference.Empty, - namePrefix = if (selectionType != ProviderState.SelectionType.SELECT) { - ProviderState.PrefixType.PROVIDED_BY - } else { - ProviderState.PrefixType.NONE - }, + namePrefix = ProviderState.PrefixType.NONE, onProviderClick = onProviderClick, ) } 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 058a853a52..713453127a 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 @@ -4,11 +4,13 @@ import androidx.compose.runtime.MutableState import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import arrow.core.getOrElse import com.tangem.core.analytics.api.AnalyticsEventHandler import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase import com.tangem.domain.settings.CanUseBiometryUseCase import com.tangem.domain.settings.IsWalletsScrollPreviewEnabled import com.tangem.domain.settings.ShouldShowSaveWalletScreenUseCase +import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.usecase.GetSelectedWalletUseCase import com.tangem.domain.wallets.usecase.GetWalletsUseCase @@ -21,7 +23,6 @@ import com.tangem.feature.wallet.presentation.wallet.loaders.WalletScreenContent import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent import com.tangem.feature.wallet.presentation.wallet.state.model.WalletEvent.DemonstrateWalletsScrollPreview.Direction -import com.tangem.feature.wallet.presentation.wallet.state.model.WalletPullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.WalletScreenState import com.tangem.feature.wallet.presentation.wallet.state.transformers.* import com.tangem.feature.wallet.presentation.wallet.state.utils.WalletEventSender @@ -61,6 +62,7 @@ internal class WalletViewModel @Inject constructor( private val selectedWalletAnalyticsSender: SelectedWalletAnalyticsSender, private val walletDeepLinksHandler: WalletDeepLinksHandler, private val walletNameMigrationUseCase: WalletNameMigrationUseCase, + private val refreshMultiCurrencyWalletQuotesUseCase: RefreshMultiCurrencyWalletQuotesUseCase, ) : ViewModel() { val uiState: StateFlow = stateHolder.uiState @@ -178,7 +180,7 @@ internal class WalletViewModel @Inject constructor( } } - // We need to update the current wallet if the application was in the background for more than 10 seconds + // We need to update the current wallet quotes if the application was in the background for more than 10 seconds // and then returned to the foreground private fun subscribeToScreenBackgroundState() { screenLifecycleProvider.isBackgroundState @@ -186,7 +188,7 @@ internal class WalletViewModel @Inject constructor( refreshWalletJobHolder.cancel() when { isBackground -> needToRefreshTimer() - needToRefreshWallet && !isBackground -> triggerRefreshWallet() + needToRefreshWallet && !isBackground -> triggerRefreshWalletQuotes() } } .launchIn(viewModelScope) @@ -199,13 +201,15 @@ internal class WalletViewModel @Inject constructor( }.saveIn(refreshWalletJobHolder) } - private fun triggerRefreshWallet() { + private fun triggerRefreshWalletQuotes() { needToRefreshWallet = false val state = stateHolder.uiState.value val wallet = state.wallets.getOrNull(state.selectedWalletIndex) ?: return - wallet.pullToRefreshConfig.onRefresh.invoke( - WalletPullToRefreshConfig.ShowRefreshState(false), - ) + viewModelScope.launch { + refreshMultiCurrencyWalletQuotesUseCase(wallet.walletCardState.id).getOrElse { + Timber.e("Failed to refreshMultiCurrencyWalletQuotesUseCase $it") + } + }.saveIn(refreshWalletJobHolder) } private suspend fun updateWallets(action: WalletsUpdateActionResolver.Action) { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index a821c9080b..e73b861d62 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -87,7 +87,7 @@ markdown = "0.7.2" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.12-694" +tangemBlockchainSdk = "release-app_5.12-698" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.12-369" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^