From e8c5c35e5601472d96a9d5b11bd4fdd3c5ef2a92 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 11 Jun 2024 12:51:04 +0400 Subject: [PATCH] Updated on 2026-08-14 --- .../response/ApiResponseCallDelegate.kt | 8 +--- .../api/common/response/ApiResponseError.kt | 5 ++- .../api/common/response/ResponseExt.kt | 13 ++++-- .../data/common/api/ApiResponseRaise.kt | 40 ++++++++++++++----- .../repository/DefaultCurrenciesRepository.kt | 2 +- .../repository/DefaultQuotesRepository.kt | 22 +++++----- .../kotlin/com/tangem/domain/core/lce/Lce.kt | 11 +++-- .../domain/tokens/model/TotalFiatBalance.kt | 4 +- .../CurrenciesStatusesLceOperations.kt | 3 ++ .../subscribers/BasicTokenListSubscriber.kt | 9 ++++- .../MultiWalletTokenListSubscriber.kt | 16 +++++--- 11 files changed, 89 insertions(+), 44 deletions(-) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt index b69ef4e355..52a2e4cc6f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseCallDelegate.kt @@ -33,12 +33,8 @@ internal class ApiResponseCallDelegate( } override fun onFailure(call: Call, t: Throwable) { - val e = if (t.isNetworkException()) { - ApiResponseError.NetworkException - } else { - ApiResponseError.UnknownException(t) - } - val safeResponse = apiError(e) + val error = t.toApiError() + val safeResponse = apiError(error) responseCallback.onResponse(this@ApiResponseCallDelegate, Response.success(safeResponse)) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt index e342289f0c..cfd2146441 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ApiResponseError.kt @@ -73,7 +73,10 @@ sealed class ApiResponseError : Exception() { } /** Represents a network error, typically when there's no connectivity. */ - object NetworkException : ApiResponseError() + data object NetworkException : ApiResponseError() + + /** Represents a timeout error, typically when the server takes too long to respond. */ + data object TimeoutException : ApiResponseError() /** * Represents an unexpected exception that doesn't fall into one of the other categories. diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt index 718392b0d9..92a5af0ac5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/common/response/ResponseExt.kt @@ -1,8 +1,11 @@ package com.tangem.datasource.api.common.response +import kotlinx.coroutines.TimeoutCancellationException import retrofit2.Response import java.net.ConnectException +import java.net.SocketTimeoutException import java.net.UnknownHostException +import java.util.concurrent.TimeoutException import javax.net.ssl.SSLHandshakeException internal fun Response.toSafeApiResponse(): ApiResponse { @@ -23,10 +26,14 @@ internal fun Response.toSafeApiResponse(): ApiResponse { } } -internal fun Throwable.isNetworkException(): Boolean = when (this) { +internal fun Throwable.toApiError(): ApiResponseError = when (this) { is ConnectException, is UnknownHostException, is SSLHandshakeException, - -> true - else -> false + -> ApiResponseError.NetworkException + is TimeoutException, + is TimeoutCancellationException, + is SocketTimeoutException, + -> ApiResponseError.TimeoutException + else -> ApiResponseError.UnknownException(cause = this) } \ No newline at end of file diff --git a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt index b481d8e846..837e169c8b 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/api/ApiResponseRaise.kt @@ -4,7 +4,9 @@ import arrow.core.raise.Raise import arrow.core.raise.recover import com.tangem.datasource.api.common.response.ApiResponse import com.tangem.datasource.api.common.response.ApiResponseError +import kotlinx.coroutines.withTimeoutOrNull import timber.log.Timber +import kotlin.time.Duration /** * A wrapper around the [Raise] interface specific for [ApiResponseError]. It provides utility functions to @@ -28,6 +30,28 @@ value class ApiResponseRaise( } } +/** + * Attempts to execute an API call safely, providing error handling and a timeout. + * + * @param T The return type of the API call and the function. + * @param timeoutMillis The timeout in milliseconds for the API call. Default is 30 seconds. + * @param call The API call block to execute. + * @param onError A function to handle errors and return a fallback value of type [T]. + * + * @return The result of the API call or the fallback value provided by [onError] if an error occurs. + */ +suspend inline fun safeApiCallWithTimeout( + timeoutMillis: Duration = with(Duration) { 30.seconds }, + crossinline call: suspend ApiResponseRaise.() -> T, + crossinline onError: suspend (ApiResponseError) -> T, +): T = safeApiCall( + call = { + withTimeoutOrNull(timeoutMillis) { call() } + ?: raise(ApiResponseError.TimeoutException) + }, + onError = onError, +) + /** * Attempts to execute an API call safely, providing error handling. * @@ -40,12 +64,10 @@ value class ApiResponseRaise( suspend inline fun safeApiCall( crossinline call: suspend ApiResponseRaise.() -> T, crossinline onError: suspend (ApiResponseError) -> T, -): T { - return recover( - block = { call(ApiResponseRaise(raise = this)) }, - recover = { - Timber.w(it, "Unable to perform safe API call") - onError(it) - }, - ) -} \ No newline at end of file +): T = recover( + block = { call(ApiResponseRaise(raise = this)) }, + recover = { + Timber.w(it, "Unable to perform safe API call") + onError(it) + }, +) \ No newline at end of file diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt index f74f7c873d..89bb9aca17 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultCurrenciesRepository.kt @@ -249,7 +249,7 @@ internal class DefaultCurrenciesRepository( launch(dispatchers.io) { combine( - getMultiCurrencyWalletCurrencies(userWallet), + getMultiCurrencyWalletCurrencies(userWallet).distinctUntilChanged(), isMultiCurrencyWalletCurrenciesFetching.map { it.getOrElse(userWallet.walletId) { false } }, ) { currencies, isFetching -> send(currencies, isStillLoading = isFetching) 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 50dd420286..1dee0062ce 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 @@ -1,6 +1,6 @@ package com.tangem.data.tokens.repository -import com.tangem.data.common.api.safeApiCall +import com.tangem.data.common.api.safeApiCallWithTimeout import com.tangem.data.common.cache.CacheRegistry import com.tangem.data.tokens.utils.QuotesConverter import com.tangem.data.tokens.utils.QuotesUnsupportedCurrenciesIdAdapter @@ -91,30 +91,30 @@ internal class DefaultQuotesRepository( if (expiredCurrenciesIds.isEmpty()) return quotesFetchedForAppCurrency = appCurrencyId + fetchQuotes(expiredCurrenciesIds, appCurrencyId) } } private suspend fun fetchQuotes(rawCurrenciesIds: Set, appCurrencyId: String) { val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(rawCurrenciesIds) - val response = safeApiCall( + val response = safeApiCallWithTimeout( call = { val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") tangemTechApi.getQuotes(appCurrencyId, coinIds).bind() }, - onError = { + onError = { error -> cacheRegistry.invalidate(rawCurrenciesIds.map(::getQuoteCacheKey)) - null + + throw error }, ) - if (response != null) { - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( - response, - replacementIdsResult.idsFiltered, - ) - quotesStore.store(updatedResponse) - } + val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + response, + replacementIdsResult.idsFiltered, + ) + quotesStore.store(updatedResponse) } private suspend fun filterExpiredCurrenciesIds( diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt index 65ef8788fa..25029586ad 100644 --- a/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt +++ b/domain/core/src/main/kotlin/com/tangem/domain/core/lce/Lce.kt @@ -83,13 +83,16 @@ sealed class Lce { } /** - * Returns the content of this [Lce] if it's a [Lce.Content] or partial content if it's a [Lce.Loading], - * `null` if it's a [Lce.Error]. + * Returns the content of this [Lce] if it's a [Lce.Content] or partial content if it's a [Lce.Loading] + * and [isPartialContentAccepted] is `true`, `null` if it's a [Lce.Error]. + * + * @param isPartialContentAccepted A flag indicating whether partial content should be accepted + * and returned. Default is `true`. * * @return The content of this [Lce] or `null`. */ - fun getOrNull(): C? = fold( - ifLoading = ::identity, + fun getOrNull(isPartialContentAccepted: Boolean = true): C? = fold( + ifLoading = { if (isPartialContentAccepted) it else null }, ifContent = ::identity, ifError = { null }, ) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt index 62786117ab..98deb39ace 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TotalFiatBalance.kt @@ -10,13 +10,13 @@ sealed class TotalFiatBalance { * Represents the loading state of the fiat balance. * This state indicates that the fiat balance is currently being retrieved or calculated. */ - object Loading : TotalFiatBalance() + data object Loading : TotalFiatBalance() /** * Represents the failure state of the fiat balance. * This state indicates that an attempt to retrieve or calculate the fiat balance has failed. */ - object Failed : TotalFiatBalance() + data object Failed : TotalFiatBalance() /** * Represents the successfully loaded state of the fiat balance. diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt index fd87e0ad64..3dd2b0582b 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrenciesStatusesLceOperations.kt @@ -122,6 +122,9 @@ internal class CurrenciesStatusesLceOperations( val quotes = recover({ maybeQuotes?.bind()?.toNonEmptySetOrNull() }) { quotesRetrievingFailed = true null + }?.ifEmpty { + quotesRetrievingFailed = true + null } currencies.map { currency -> diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt index cf4c25c45f..c9bd3afc92 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/BasicTokenListSubscriber.kt @@ -63,7 +63,14 @@ internal abstract class BasicTokenListSubscriber( transform = { maybeTokenList, maybeAppCurrency -> val tokenList = maybeTokenList.getOrElse( ifLoading = { maybeContent -> - maybeContent ?: return@combine + val isRefreshing = stateHolder.getWalletState(userWallet.walletId) + ?.pullToRefreshConfig + ?.isRefreshing + ?: false + + maybeContent + ?.takeIf { !isRefreshing } + ?: return@combine }, ifError = { e -> Timber.e("Failed to load token list: $e") diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt index 595793c393..6b19dfd81d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/subscribers/MultiWalletTokenListSubscriber.kt @@ -53,9 +53,8 @@ internal class MultiWalletTokenListSubscriber( updateSortingIfNeeded(maybeTokenList) } - private suspend fun updateSortingIfNeeded(maybeTokenList: Lce) { - val tokenList = maybeTokenList.getOrNull() ?: return - if (!checkNeedSorting(tokenList)) return + private suspend fun updateSortingIfNeeded(maybeTokenList: Lce<*, TokenList>) { + val tokenList = getTokenList(maybeTokenList) ?: return applyTokenListSortingUseCase( userWalletId = userWallet.walletId, @@ -65,9 +64,14 @@ internal class MultiWalletTokenListSubscriber( ) } - private fun checkNeedSorting(tokenList: TokenList): Boolean { - return tokenList.totalFiatBalance !is TotalFiatBalance.Loading && - tokenList.sortedBy == TokenList.SortType.BALANCE + private fun getTokenList(lce: Lce<*, TokenList>): TokenList? { + val tokenList = lce.getOrNull(isPartialContentAccepted = false) + ?: return null + + return tokenList.takeIf { + tokenList.totalFiatBalance is TotalFiatBalance.Loaded && + tokenList.sortedBy == TokenList.SortType.BALANCE + } } private fun getCurrenciesIds(tokenList: TokenList): List {