diff --git a/common/ui-charts/build.gradle.kts b/common/ui-charts/build.gradle.kts index 0520d97bb3..5abca7eada 100644 --- a/common/ui-charts/build.gradle.kts +++ b/common/ui-charts/build.gradle.kts @@ -11,6 +11,7 @@ android { dependencies { /** Project - Core */ implementation(projects.core.ui) + implementation(projects.core.utils) /** Compose */ implementation(tangemDeps.vico.core) diff --git a/common/ui-charts/detekt-baseline-debug.xml b/common/ui-charts/detekt-baseline-debug.xml deleted file mode 100644 index 05694da92a..0000000000 --- a/common/ui-charts/detekt-baseline-debug.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - SuspendFunSwallowedCancellation:MarketChartDataProducer.kt$MarketChartDataProducer$runCatching - - diff --git a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt index 74adf8e118..3eaa12fe0d 100644 --- a/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt +++ b/common/ui-charts/src/main/kotlin/com/tangem/common/ui/charts/state/MarketChartDataProducer.kt @@ -6,6 +6,7 @@ import com.patrykandpatrick.vico.core.cartesian.data.LineCartesianLayerModel import com.tangem.common.ui.charts.state.converter.PointValuesConverter import com.tangem.common.ui.charts.state.converter.PriceAndTimePointValuesConverter import com.tangem.common.ui.charts.state.formatter.FormatterWrapWithCache +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.sync.Mutex @@ -126,7 +127,7 @@ class MarketChartDataProducer private constructor( currentCoroutineContext().ensureActive() - runCatching { + runSuspendCatching { modelProducer.runTransaction { add(LineCartesianLayerModel.Partial(series = listOf(entriesLocal))) } diff --git a/data/balance-hiding/detekt-baseline-debug.xml b/data/balance-hiding/detekt-baseline-debug.xml deleted file mode 100644 index eca9bec2a0..0000000000 --- a/data/balance-hiding/detekt-baseline-debug.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - CastNullableToNonNullableType:DefaultDeviceFlipDetector.kt$DefaultDeviceFlipDetector$as - - diff --git a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultDeviceFlipDetector.kt b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultDeviceFlipDetector.kt index f69cc30dd9..94dc429337 100644 --- a/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultDeviceFlipDetector.kt +++ b/data/balance-hiding/src/main/java/com/tangem/data/balancehiding/DefaultDeviceFlipDetector.kt @@ -16,11 +16,19 @@ import javax.inject.Singleton @Singleton class DefaultDeviceFlipDetector @Inject constructor( - @ApplicationContext context: Context, + @ApplicationContext private val context: Context, ) : DeviceFlipDetector, DefaultLifecycleObserver { - private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager - private val gravitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) + private val sensorManager: SensorManager by lazy { + val manager = context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager + + requireNotNull(manager) + } + + private val gravitySensor by lazy { + sensorManager.getDefaultSensor(Sensor.TYPE_GRAVITY) + } + private val isResumedState = AtomicBoolean(false) override fun onPause(owner: LifecycleOwner) { diff --git a/data/hot-wallet/detekt-baseline-debug.xml b/data/hot-wallet/detekt-baseline-debug.xml deleted file mode 100644 index e0f29a8922..0000000000 --- a/data/hot-wallet/detekt-baseline-debug.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - MultilineLambdaItParameter:DefaultHotWalletRepository.kt$DefaultHotWalletRepository${ it.setObjectMap( key = PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY, value = it.getObjectMap<Boolean>(PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY) .plus(userWalletId.stringValue to skipped), ) } - - diff --git a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt index d4064d0089..4670884954 100644 --- a/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt +++ b/data/hot-wallet/src/main/java/com/tangem/data/hotwallet/DefaultHotWalletRepository.kt @@ -17,10 +17,10 @@ internal class DefaultHotWalletRepository( .map { it[userWalletId.stringValue] == true } override suspend fun setAccessCodeSkipped(userWalletId: UserWalletId, skipped: Boolean) { - appPreferencesStore.editData { - it.setObjectMap( + appPreferencesStore.editData { mutablePreferences -> + mutablePreferences.setObjectMap( key = PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY, - value = it.getObjectMap(PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY) + value = mutablePreferences.getObjectMap(PreferencesKeys.ACCESS_CODE_SKIPPED_STATES_KEY) .plus(userWalletId.stringValue to skipped), ) } diff --git a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt index 1209bcfb5b..0ec3c30598 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/repository/DefaultNetworksRepository.kt @@ -79,6 +79,10 @@ internal class DefaultNetworksRepository( } } + override suspend fun hasCachedStatuses(userWalletId: UserWalletId): Boolean { + return networksStatusesStore.contains(userWalletId) + } + private suspend fun fetchPendingTransactions( userWalletId: UserWalletId, network: Network, diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt index 9cba360e2e..1131c43dfc 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/DefaultNetworksStatusesStore.kt @@ -124,6 +124,10 @@ internal class DefaultNetworksStatusesStore( } } + override suspend fun contains(userWalletId: UserWalletId): Boolean { + return runtimeStore.getSyncOrDefault(emptyMap()).containsKey(userWalletId.stringValue) + } + private suspend fun updateInRuntime( userWalletId: UserWalletId, networks: Set, diff --git a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStore.kt b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStore.kt index 680ee077ab..2e63a27f7b 100644 --- a/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStore.kt +++ b/data/networks/src/main/java/com/tangem/data/networks/store/NetworksStatusesStore.kt @@ -44,4 +44,7 @@ internal interface NetworksStatusesStore { /** Clear statuses of [networks] by [userWalletId] */ suspend fun clear(userWalletId: UserWalletId, networks: Set) + + /** Check if there are statuses for given [userWalletId] */ + suspend fun contains(userWalletId: UserWalletId): Boolean } \ No newline at end of file diff --git a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt index a0dada9a61..c8261d32ea 100644 --- a/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt +++ b/data/promo/src/main/java/com/tangem/data/promo/DefaultPromoRepository.kt @@ -19,10 +19,7 @@ import com.tangem.domain.promo.models.StoryContent import com.tangem.feature.referral.domain.ReferralRepository import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.* import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull @@ -41,23 +38,25 @@ internal class DefaultPromoRepository( return appPreferencesStore.get( key = PreferencesKeys.getShouldShowPromoKey(promoId = promoId.name), default = true, - ).map { shouldShow -> - when (promoId) { - PromoId.Referral -> runCatching { - !referralRepository.isReferralParticipant(userWalletId) && shouldShow - }.getOrDefault(false) - PromoId.Sepa -> { - val isActive = getSepaPromoBanner()?.isActive ?: false + ) + .distinctUntilChanged() + .map { shouldShow -> + when (promoId) { + PromoId.Referral -> runCatching { + !referralRepository.isReferralParticipant(userWalletId) && shouldShow + }.getOrDefault(false) + PromoId.Sepa -> { + val isActive = getSepaPromoBanner()?.isActive ?: false - isActive && shouldShow - } - PromoId.VisaPresale -> { - val isActive = getVisaPromoBanner()?.isActive ?: false + isActive && shouldShow + } + PromoId.VisaPresale -> { + val isActive = getVisaPromoBanner()?.isActive ?: false - isActive && shouldShow + isActive && shouldShow + } } } - } } override fun isReadyToShowTokenPromo(promoId: PromoId): Flow { diff --git a/data/quotes/detekt-baseline-debug.xml b/data/quotes/detekt-baseline-debug.xml deleted file mode 100644 index c69926fdf1..0000000000 --- a/data/quotes/detekt-baseline-debug.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - MultilineLambdaItParameter:DefaultMultiQuoteStatusFetcher.kt$DefaultMultiQuoteStatusFetcher${ Timber.e(it) quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } - - diff --git a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt index 618f75178d..1660472cb9 100644 --- a/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt +++ b/data/quotes/src/main/java/com/tangem/data/quotes/multi/DefaultMultiQuoteStatusFetcher.kt @@ -66,8 +66,8 @@ internal class DefaultMultiQuoteStatusFetcher @Inject constructor( quotesStatusesStore.store(values = updatedResponse.quotes) } - .onLeft { - Timber.e(it) + .onLeft { throwable -> + Timber.e(throwable) quotesStatusesStore.setSourceAsOnlyCache(currenciesIds = params.currenciesIds) } diff --git a/data/settings/detekt-baseline-debug.xml b/data/settings/detekt-baseline-debug.xml deleted file mode 100644 index 5489dc6b26..0000000000 --- a/data/settings/detekt-baseline-debug.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - SuspendFunSwallowedCancellation:DefaultSettingsRepository.kt$DefaultSettingsRepository$runCatching - - diff --git a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt index 9c1b8171f0..dea79f4d3a 100644 --- a/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt +++ b/data/settings/src/main/java/com/tangem/data/settings/DefaultSettingsRepository.kt @@ -13,6 +13,7 @@ import com.tangem.domain.settings.repositories.SettingsRepository import com.tangem.domain.settings.usercountry.models.GB_COUNTRY import com.tangem.domain.settings.usercountry.models.UserCountry import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.coroutines.runSuspendCatching import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -153,7 +154,7 @@ internal class DefaultSettingsRepository( } withContext(dispatchers.io) { - val country = runCatching { tangemTechApi.getUserCountryCode() } + val country = runSuspendCatching { tangemTechApi.getUserCountryCode() } .fold( onSuccess = GeoResponse::code, onFailure = { Locale.getDefault().country }, diff --git a/detekt_baseline_report.txt b/detekt_baseline_report.txt index dd34c82236..2017064be7 100644 --- a/detekt_baseline_report.txt +++ b/detekt_baseline_report.txt @@ -1,9 +1,13 @@ ========================================== Detekt Baseline Updater & Issue Counter ========================================== -Date: 2025-11-21 15:22:23 +Date: 2025-11-24 18:35:00 -Updating detekt baseline for debug variant... +Step 1: Running detekt to check for new issues... + +✓ Detekt passed - no new issues found + +Step 2: Updating detekt baseline for debug variant... Baseline updated successfully! @@ -13,13 +17,13 @@ Counting issues in baseline files... ========================================== Summary: - Total Issues: 1720 - Modules with Issues: 90 - Average Issues per Module: 19 + Total Issues: 1700 + Modules with Issues: 83 + Average Issues per Module: 20 Progress: - Fixed: 82 out of 1802 (4%) - Remaining: 1720 + Fixed: 102 out of 1802 (5%) + Remaining: 1700 ========================================== All Modules with Issues (sorted by count) @@ -27,7 +31,7 @@ All Modules with Issues (sorted by count) Module Issues ──────────────────────────────────────────────────────────────── -features/wallet/impl 170 +features/wallet/impl 169 features/markets/impl 155 features/onboarding-v2/impl 131 features/onramp/impl 89 @@ -43,10 +47,10 @@ features/manage-tokens/impl 45 domain/wallets 39 features/nft/impl 36 features/tester/impl 31 -features/yield-supply/impl 28 domain/tokens 28 features/swap/domain 27 core/ui 27 +features/yield-supply/impl 26 common/ui 26 data/visa 23 features/tangempay/details/impl 22 @@ -54,9 +58,9 @@ data/nft 20 data/wallets 18 features/swap/data 15 data/swap 13 -domain/account/status 12 features/token-recieve/impl 11 features/qr-scanning/impl 11 +domain/account/status 11 data/onramp 11 data/manage-tokens 11 core/datasource 11 @@ -104,16 +108,10 @@ domain/nft 2 data/express 2 core/ab-tests 2 common/test 2 -features/referral/data 1 features/manage-tokens/api 1 features/kyc/impl 1 features/hot-wallet/api 1 features/disclaimer/impl 1 domain/wallet-connect 1 domain/visa 1 -data/settings 1 -data/quotes 1 -data/hot-wallet 1 -data/balance-hiding 1 -common/ui-charts 1 ──────────────────────────────────────────────────────────────── \ No newline at end of file diff --git a/domain/account/status/detekt-baseline-debug.xml b/domain/account/status/detekt-baseline-debug.xml index 5b524e1585..1f66781ca0 100644 --- a/domain/account/status/detekt-baseline-debug.xml +++ b/domain/account/status/detekt-baseline-debug.xml @@ -2,7 +2,6 @@ - ExplicitCollectionElementAccessMethod:ManageCryptoCurrenciesUseCase.kt$ManageCryptoCurrenciesUseCase$mutableMap.put(id, currency) MultilineLambdaItParameter:AccountCryptoCurrencyStatusFinder.kt$AccountCryptoCurrencyStatusFinder${ val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@filter false cryptoPortfolio.derivationIndex.value in possibleAccountIndexes } MultilineLambdaItParameter:AccountCryptoCurrencyStatusFinder.kt$AccountCryptoCurrencyStatusFinder${ val cryptoPortfolio = it.account as? Account.CryptoPortfolio ?: return@firstOrNull false cryptoPortfolio.derivationIndex.value == possibleAccountIndex } MultilineLambdaItParameter:AccountCryptoCurrencyStatusFinder.kt$AccountCryptoCurrencyStatusFinder${ val currency = it.currency val isContractAddressMatch = contractAddress == null || currency.id.contractAddress.equals(contractAddress, ignoreCase = true) currency.network.rawId == networkId.rawId.value && currency.network.derivationPath.value == derivationPath.value && isContractAddressMatch } diff --git a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt index 8d8683c595..8b3efcfba9 100644 --- a/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt +++ b/domain/account/status/src/main/java/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducer.kt @@ -2,20 +2,24 @@ package com.tangem.domain.account.status.producer import arrow.core.Option import arrow.core.none +import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory import com.tangem.domain.account.supplier.SingleAccountListSupplier import com.tangem.domain.core.utils.lceContent +import com.tangem.domain.core.utils.lceLoading import com.tangem.domain.models.StatusSource import com.tangem.domain.models.TokensGroupType import com.tangem.domain.models.TokensSortType +import com.tangem.domain.models.TotalFiatBalance import com.tangem.domain.models.account.Account import com.tangem.domain.models.account.AccountStatus +import com.tangem.domain.models.currency.CryptoCurrencyStatus import com.tangem.domain.models.quote.PriceChange import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet +import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.domain.tokens.operations.PriceChangeCalculator import com.tangem.domain.tokens.operations.TokenListFactory import com.tangem.domain.tokens.operations.TotalFiatBalanceCalculator @@ -24,8 +28,10 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* import java.math.BigDecimal +import kotlin.time.Duration.Companion.milliseconds /** * Produces a flow of [AccountStatusList] for a single user wallet. @@ -43,6 +49,7 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo @Assisted private val params: SingleAccountStatusListProducer.Params, private val accountsCRUDRepository: AccountsCRUDRepository, private val singleAccountListSupplier: SingleAccountListSupplier, + private val networksRepository: NetworksRepository, private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory, private val dispatchers: CoroutineDispatcherProvider, ) : SingleAccountStatusListProducer { @@ -50,31 +57,11 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo override val fallback: Option = none() override fun produce(): Flow { - val accountListFlow = singleAccountListSupplier( - params = SingleAccountListProducer.Params(params.userWalletId), - ) - - return accountListFlow.flatMapLatest { accountList -> - val accountStatusFlows = accountList.accounts.mapNotNull { account -> - if (account !is Account.CryptoPortfolio) return@mapNotNull null - - if (account.cryptoCurrencies.isEmpty()) { - createEmptyAccountStatusFlow(account) - } else { - val userWallet = accountsCRUDRepository.getUserWallet(userWalletId = params.userWalletId) - - getAccountStatusFlow( - userWallet = userWallet, - account = account, - groupType = accountList.groupType, - sortType = accountList.sortType, - ) - } - .distinctUntilChanged() - } + return singleAccountListSupplier(userWalletId = params.userWalletId).flatMapLatest { accountList -> + val accountStatusFlows = createAccountStatusFlows(accountList) combine(accountStatusFlows) { accountStatuses -> - val balances = accountStatuses.map { it.tokenList.totalFiatBalance } + val balances = accountStatuses.flattenTotalFiatBalance() AccountStatusList( userWalletId = accountList.userWalletId, @@ -85,11 +72,34 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo groupType = accountList.groupType, ) } + .onStartCheckCachedNetworks(accountList) } .distinctUntilChanged() .flowOn(dispatchers.default) } + private fun createAccountStatusFlows(accountList: AccountList): List> { + return accountList.accounts.map { account -> + when (account) { + is Account.CryptoPortfolio -> { + if (account.cryptoCurrencies.isEmpty()) { + createEmptyAccountStatusFlow(account) + } else { + val userWallet = accountsCRUDRepository.getUserWallet(userWalletId = params.userWalletId) + + getAccountStatusFlow( + userWallet = userWallet, + account = account, + groupType = accountList.groupType, + sortType = accountList.sortType, + ) + } + .distinctUntilChanged() + } + } + } + } + private fun createEmptyAccountStatusFlow(account: Account.CryptoPortfolio): Flow { return flowOf( AccountStatus.CryptoPortfolio( @@ -109,25 +119,85 @@ internal class DefaultSingleAccountStatusListProducer @AssistedInject constructo groupType: TokensGroupType, sortType: TokensSortType, ): Flow { + val statusesFlows = getCryptoCurrencyStatusesFlow(userWallet, account) + + return statusesFlows + .map { statusList -> + AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenListFactory.create( + statuses = statusList, + groupType = groupType, + sortType = sortType, + ), + priceChangeLce = PriceChangeCalculator.calculate(statuses = statusList), + ) + } + .distinctUntilChanged() + } + + @OptIn(FlowPreview::class) + private fun getCryptoCurrencyStatusesFlow( + userWallet: UserWallet, + account: Account.CryptoPortfolio, + ): Flow> { val statusesFlows = account.cryptoCurrencies.map { currency -> cryptoCurrencyStatusesFlowFactory.create(userWallet = userWallet, currency = currency) + .onStart { emit(CryptoCurrencyStatus(currency = currency, value = CryptoCurrencyStatus.Loading)) } .distinctUntilChanged() } - return combine(statusesFlows) { statuses -> - val statusList = statuses.toList() - - AccountStatus.CryptoPortfolio( - account = account, - tokenList = TokenListFactory.create( - statuses = statusList, - groupType = groupType, - sortType = sortType, - ), - priceChangeLce = PriceChangeCalculator.calculate(statuses = statusList), - ) - } + return combine(statusesFlows) { it.toList() } .distinctUntilChanged() + .debounce(50.milliseconds) + } + + private fun Array.flattenTotalFiatBalance(): List { + return map { accountStatus -> + when (accountStatus) { + is AccountStatus.CryptoPortfolio -> accountStatus.tokenList.totalFiatBalance + } + } + } + + private fun Flow.onStartCheckCachedNetworks(accountList: AccountList): Flow { + return onStart { + val hasCachedNetworks = networksRepository.hasCachedStatuses(userWalletId = accountList.userWalletId) + + if (hasCachedNetworks) return@onStart + + val loading = createLoadingAccountStatusList(accountList) + emit(loading) + } + } + + private fun createLoadingAccountStatusList(accountList: AccountList): AccountStatusList { + return AccountStatusList( + userWalletId = accountList.userWalletId, + accountStatuses = accountList.accounts.map { account -> + when (account) { + is Account.CryptoPortfolio -> { + val currencyStatuses = account.cryptoCurrencies.map { + CryptoCurrencyStatus(currency = it, value = CryptoCurrencyStatus.Loading) + } + + AccountStatus.CryptoPortfolio( + account = account, + tokenList = TokenListFactory.create( + statuses = currencyStatuses, + groupType = accountList.groupType, + sortType = accountList.sortType, + ), + priceChangeLce = lceLoading(), + ) + } + } + }, + totalAccounts = accountList.totalAccounts, + totalFiatBalance = TotalFiatBalance.Loading, + sortType = accountList.sortType, + groupType = accountList.groupType, + ) } @AssistedFactory diff --git a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt index a02e8b9108..15c437d7aa 100644 --- a/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt +++ b/domain/account/status/src/test/kotlin/com/tangem/domain/account/status/producer/DefaultSingleAccountStatusListProducerTest.kt @@ -5,7 +5,6 @@ import com.google.common.truth.Truth import com.tangem.common.test.domain.token.MockCryptoCurrencyFactory import com.tangem.domain.account.models.AccountList import com.tangem.domain.account.models.AccountStatusList -import com.tangem.domain.account.producer.SingleAccountListProducer import com.tangem.domain.account.repository.AccountsCRUDRepository import com.tangem.domain.account.status.utils.CryptoCurrencyStatusesFlowFactory import com.tangem.domain.account.supplier.SingleAccountListSupplier @@ -20,9 +19,11 @@ import com.tangem.domain.models.quote.PriceChange import com.tangem.domain.models.tokenlist.TokenList import com.tangem.domain.models.wallet.UserWallet import com.tangem.domain.models.wallet.UserWalletId +import com.tangem.domain.networks.repository.NetworksRepository import com.tangem.test.core.getEmittedValues import com.tangem.utils.coroutines.TestingCoroutineDispatcherProvider import io.mockk.* +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest @@ -40,6 +41,7 @@ class DefaultSingleAccountStatusListProducerTest { private val accountsCRUDRepository: AccountsCRUDRepository = mockk() private val singleAccountListSupplier: SingleAccountListSupplier = mockk() + private val networksRepository: NetworksRepository = mockk() private val cryptoCurrencyStatusesFlowFactory: CryptoCurrencyStatusesFlowFactory = mockk() private val userWalletId = UserWalletId("011") @@ -51,13 +53,19 @@ class DefaultSingleAccountStatusListProducerTest { params = SingleAccountStatusListProducer.Params(userWalletId), accountsCRUDRepository = accountsCRUDRepository, singleAccountListSupplier = singleAccountListSupplier, + networksRepository = networksRepository, cryptoCurrencyStatusesFlowFactory = cryptoCurrencyStatusesFlowFactory, dispatchers = TestingCoroutineDispatcherProvider(), ) @AfterEach fun tearDown() { - clearMocks(accountsCRUDRepository, singleAccountListSupplier, cryptoCurrencyStatusesFlowFactory) + clearMocks( + accountsCRUDRepository, + singleAccountListSupplier, + networksRepository, + cryptoCurrencyStatusesFlowFactory, + ) } @Test @@ -65,9 +73,8 @@ class DefaultSingleAccountStatusListProducerTest { // Arrange val accountList = AccountList.empty(userWalletId = userWalletId) - every { - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) - } returns flowOf(accountList) + every { singleAccountListSupplier(userWalletId) } returns flowOf(accountList) + coEvery { networksRepository.hasCachedStatuses(userWalletId) } returns true // Act val actual = producer.produce().let(::getEmittedValues) @@ -89,8 +96,14 @@ class DefaultSingleAccountStatusListProducerTest { ) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) + coVerifySequence { + singleAccountListSupplier(userWalletId) + networksRepository.hasCachedStatuses(userWalletId) + } + + coVerify(inverse = true) { + accountsCRUDRepository.getUserWallet(userWalletId = any()) + cryptoCurrencyStatusesFlowFactory.create(userWallet = any(), currency = any()) } } @@ -102,9 +115,8 @@ class DefaultSingleAccountStatusListProducerTest { val accountListFlow = MutableStateFlow(value = accountList) - every { - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) - } returns accountListFlow + every { singleAccountListSupplier(userWalletId) } returns accountListFlow + coEvery { networksRepository.hasCachedStatuses(userWalletId) } returns true // Act (first emission) val actual1 = producer.produce().let(::getEmittedValues) @@ -147,9 +159,17 @@ class DefaultSingleAccountStatusListProducerTest { ) Truth.assertThat(actual2).containsExactly(expected2) - coVerify(ordering = Ordering.SEQUENCE) { - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) + coVerifySequence { + singleAccountListSupplier(userWalletId) + networksRepository.hasCachedStatuses(userWalletId) + networksRepository.hasCachedStatuses(userWalletId) + singleAccountListSupplier(userWalletId) + networksRepository.hasCachedStatuses(userWalletId) + } + + coVerify(inverse = true) { + accountsCRUDRepository.getUserWallet(userWalletId = any()) + cryptoCurrencyStatusesFlowFactory.create(userWallet = any(), currency = any()) } } @@ -159,9 +179,8 @@ class DefaultSingleAccountStatusListProducerTest { val accountList = AccountList.empty(userWalletId) val accountListFlow = MutableStateFlow(value = accountList) - every { - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) - } returns accountListFlow + every { singleAccountListSupplier(userWalletId) } returns accountListFlow + coEvery { networksRepository.hasCachedStatuses(userWalletId) } returns true val expected = AccountStatusList( userWalletId = userWalletId, @@ -191,9 +210,16 @@ class DefaultSingleAccountStatusListProducerTest { // Assert (second emission) Truth.assertThat(actual2).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) + coVerifySequence { + singleAccountListSupplier(userWalletId) + networksRepository.hasCachedStatuses(userWalletId) + singleAccountListSupplier(userWalletId) + networksRepository.hasCachedStatuses(userWalletId) + } + + coVerify(inverse = true) { + accountsCRUDRepository.getUserWallet(userWalletId = any()) + cryptoCurrencyStatusesFlowFactory.create(userWallet = any(), currency = any()) } } @@ -208,9 +234,8 @@ class DefaultSingleAccountStatusListProducerTest { coEvery { accountsCRUDRepository.getUserWallet(userWalletId = userWalletId) } returns userWallet - every { - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) - } returns flowOf(accountList) + every { singleAccountListSupplier(userWalletId) } returns flowOf(accountList) + coEvery { networksRepository.hasCachedStatuses(userWalletId) } returns true val ethereumStatus = CryptoCurrencyStatus( currency = cryptoCurrencyFactory.ethereum, @@ -229,7 +254,9 @@ class DefaultSingleAccountStatusListProducerTest { } returns flowOf(stellarStatus) // Act - val actual = producer.produce().let(::getEmittedValues) + val flow = producer.produce() + delay(1000) + val actual = flow.let(::getEmittedValues) // Assert val expected = AccountStatusList( @@ -252,8 +279,12 @@ class DefaultSingleAccountStatusListProducerTest { ) Truth.assertThat(actual).containsExactly(expected) - coVerify(ordering = Ordering.SEQUENCE) { - singleAccountListSupplier(params = SingleAccountListProducer.Params(userWalletId)) + coVerifySequence { + singleAccountListSupplier(userWalletId) + accountsCRUDRepository.getUserWallet(userWalletId = userWalletId) + cryptoCurrencyStatusesFlowFactory.create(userWallet = userWallet, currency = cryptoCurrencyFactory.ethereum) + cryptoCurrencyStatusesFlowFactory.create(userWallet = userWallet, currency = cryptoCurrencyFactory.stellar) + networksRepository.hasCachedStatuses(userWalletId) } } } \ No newline at end of file diff --git a/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt b/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt index be5ea651d0..e27c6b2b47 100644 --- a/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt +++ b/domain/networks/src/main/java/com/tangem/domain/networks/repository/NetworksRepository.kt @@ -22,5 +22,14 @@ interface NetworksRepository { */ suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network): List + /** + * Returns addresses and crypto currency + * + * @param userWalletId the unique identifier of the user wallet + * @param network network id + */ suspend fun getNetworkAddresses(userWalletId: UserWalletId, network: Network.RawID): List + + /** Checks if there are cached statuses for given [userWalletId] */ + suspend fun hasCachedStatuses(userWalletId: UserWalletId): Boolean } \ No newline at end of file diff --git a/features/wallet/impl/detekt-baseline-debug.xml b/features/wallet/impl/detekt-baseline-debug.xml index db463f017b..cf87ca958c 100644 --- a/features/wallet/impl/detekt-baseline-debug.xml +++ b/features/wallet/impl/detekt-baseline-debug.xml @@ -72,7 +72,6 @@ MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol)) clipboardManager.setText(text = it, isSensitive = true) } MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ analyticsEventHandler.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol)) shareManager.shareText(text = it) } MultilineLambdaItParameter:WalletCurrencyActionsClickIntents.kt$WalletCurrencyActionsClickIntentsImplementor${ onAddressTypeSelected( userWalletId = userWalletId, currency = currency, addressModel = it, ) } - MultilineLambdaItParameter:WalletDropDownItemsSubscriber.kt$WalletDropDownItemsSubscriber${ stateHolder.update( SetWalletCardDropDownItemsTransformer( dropdownEnabled = it, clickIntents = clickIntents, ), ) } MultilineLambdaItParameter:WalletLoaderStorage.kt$WalletLoaderStorage${ it.forEach(Job::cancel) loaders.remove(id) } MultilineLambdaItParameter:WalletModel.kt$WalletModel${ it .conflate() .distinctUntilChanged() .onEach { selectedWallet -> if (selectedWallet.isMultiCurrency) { selectedWalletAnalyticsSender.send(selectedWallet) } subscribeOnExpressTransactionsUpdates(selectedWallet) observeAndClearNFTCacheIfNeedUseCase(selectedWallet) } .flowOn(dispatchers.main) .launchIn(modelScope) } MultilineLambdaItParameter:WalletModel.kt$WalletModel${ walletScreenContentLoader.load( userWallet = it, clickIntents = clickIntents, coroutineScope = modelScope, isRefresh = true, ) } diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt index 4ef699e9c4..5d05c74f9f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/GetMultiWalletWarningsFactory.kt @@ -45,6 +45,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map import timber.log.Timber import javax.inject.Inject @@ -95,13 +96,16 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( return combine( // todo account just use it, after delete accountsFeatureToggles // accountStatusListFlow, - isReadyToShowRateAppUseCase(), - isNeedToBackupUseCase(userWallet.walletId), - seedPhraseNotificationUseCase(userWalletId = userWallet.walletId), - shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.VisaPresale), - shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa), - notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key), - getAccessCodeSkippedUseCase(userWallet.walletId), + isReadyToShowRateAppUseCase().distinctUntilChanged(), + isNeedToBackupUseCase(userWallet.walletId).distinctUntilChanged(), + seedPhraseNotificationUseCase(userWalletId = userWallet.walletId).distinctUntilChanged(), + shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.VisaPresale) + .distinctUntilChanged(), + shouldShowPromoWalletUseCase(userWalletId = userWallet.walletId, promoId = PromoId.Sepa) + .distinctUntilChanged(), + notificationsRepository.getShouldShowNotification(NotificationId.EnablePushesReminderNotification.key) + .distinctUntilChanged(), + getAccessCodeSkippedUseCase(userWallet.walletId).distinctUntilChanged(), ) { array -> array } .combine(tokenListFlow()) { array, any: Any -> arrayOf(any).plus(elements = array) } .map { array -> diff --git a/features/yield-supply/impl/detekt-baseline-debug.xml b/features/yield-supply/impl/detekt-baseline-debug.xml index 0fef2c630f..1063bc0890 100644 --- a/features/yield-supply/impl/detekt-baseline-debug.xml +++ b/features/yield-supply/impl/detekt-baseline-debug.xml @@ -5,12 +5,10 @@ BooleanPropertyNaming:YieldSupplyApyComponent.kt$YieldSupplyApyComponent$val state by loadingState.collectAsState() BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$private val handleNavigation = params.handleNavigation BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$val processing = uiState.value is YieldSupplyUM.Processing - BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$val showInfoIcon = cryptoCurrencyStatus.yieldSupplyNotAllAmountSupplied() BooleanPropertyNaming:YieldSupplyModel.kt$YieldSupplyModel$val showWarningIcon = !yieldSupplyStatus.isAllowedToSpend BooleanPropertyNaming:YieldSupplyUM.kt$YieldSupplyUM.Content$val showInfoIcon: Boolean BooleanPropertyNaming:YieldSupplyUM.kt$YieldSupplyUM.Content$val showWarningIcon: Boolean MultilineLambdaItParameter:YieldSupplyApproveModel.kt$YieldSupplyApproveModel${ Timber.e(it) return } - MultilineLambdaItParameter:YieldSupplyModel.kt$YieldSupplyModel${ Timber.e(it) uiState.update { YieldSupplyUM.Content( title = resourceReference( R.string.yield_module_token_details_earn_notification_earning_on_your_balance_title, ), subtitle = resourceReference( R.string.yield_module_token_details_earn_notification_earning_on_your_balance_subtitle, ), rewardsApy = TextReference.EMPTY, onClick = ::onActiveClick, showWarningIcon = showWarningIcon, showInfoIcon = showInfoIcon, apy = "", ) } } MultilineLambdaItParameter:YieldSupplyModel.kt$YieldSupplyModel${ Timber.e(it) uiState.update { YieldSupplyUM.Initial } } MultilineLambdaItParameter:YieldSupplyModel.kt$YieldSupplyModel${ Timber.w(it.toString()) return@launch } MultilineLambdaItParameter:YieldSupplyStartEarningModel.kt$YieldSupplyStartEarningModel${ Timber.w(it.toString()) showAlertError() }