diff --git a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt index 8adbb68381..4498836964 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/MarketsDomainModule.kt @@ -1,11 +1,13 @@ package com.tangem.tap.di.domain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains import com.tangem.domain.card.repository.DerivationsRepository import com.tangem.domain.markets.* import com.tangem.domain.markets.repositories.MarketsTokenRepository import com.tangem.domain.tokens.repository.CurrenciesRepository import com.tangem.domain.tokens.repository.NetworksRepository import com.tangem.domain.tokens.repository.QuotesRepository +import com.tangem.domain.wallets.legacy.UserWalletsListManager import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -64,6 +66,18 @@ object MarketsDomainModule { ) } + @Provides + @Singleton + fun provideFilterNetworksUseCase( + userWalletsListManager: UserWalletsListManager, + excludedBlockchains: ExcludedBlockchains, + ): FilterAvailableNetworksForWalletUseCase { + return FilterAvailableNetworksForWalletUseCase( + userWalletsListManager = userWalletsListManager, + excludedBlockchains = excludedBlockchains, + ) + } + @Provides @Singleton fun provideGetTokenExchangesUseCase(marketsTokenRepository: MarketsTokenRepository): GetTokenExchangesUseCase { diff --git a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt index 27017a9505..4966d2d7b0 100644 --- a/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt +++ b/common/src/main/kotlin/com/tangem/common/TangemBlogUrlBuilder.kt @@ -24,5 +24,9 @@ object TangemBlogUrlBuilder { data object SeedNotify : Post { override val path: String = "seed-notify" } + + data object SeedNotifySecond : Post { + override val path: String = "tangem-resolves-log-issue" + } } } \ No newline at end of file diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 6a6f96f728..a59d399816 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { /** Project - Domain */ implementation(projects.domain.appCurrency.models) + implementation(projects.domain.models) implementation(projects.domain.legacy) implementation(projects.domain.staking.models) implementation(projects.domain.tokens.models) diff --git a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt index 671e32f204..c2100bf9dd 100644 --- a/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt +++ b/common/ui/src/main/java/com/tangem/common/ui/tokens/TokenItemStateConverter.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format import com.tangem.core.ui.format.bigdecimal.percent import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.utils.StringsSigns.DASH_SIGN @@ -40,7 +41,7 @@ class TokenItemStateConverter( createSubtitleState(it, appCurrency) }, private val subtitle2StateProvider: (CryptoCurrencyStatus) -> TokenItemState.Subtitle2State? = { - createSubtitle2State(currencyStatus = it) + createSubtitle2State(status = it) }, private val fiatAmountStateProvider: (CryptoCurrencyStatus) -> TokenItemState.FiatAmountState? = { createFiatAmountState(status = it, appCurrency = appCurrency) @@ -147,7 +148,7 @@ class TokenItemStateConverter( return totalAmount.format { crypto(currency) } } - fun CryptoCurrencyStatus.getStakedBalance() = + private fun CryptoCurrencyStatus.getStakedBalance() = (value.yieldBalance as? YieldBalance.Data)?.getTotalWithRewardsStakingBalance().orZero() private fun createTitleState(currencyStatus: CryptoCurrencyStatus): TokenItemState.TitleState { @@ -190,15 +191,16 @@ class TokenItemStateConverter( } } - private fun createSubtitle2State(currencyStatus: CryptoCurrencyStatus): TokenItemState.Subtitle2State? { - return when (currencyStatus.value) { + private fun createSubtitle2State(status: CryptoCurrencyStatus): TokenItemState.Subtitle2State? { + return when (status.value) { is CryptoCurrencyStatus.Loaded, is CryptoCurrencyStatus.Custom, is CryptoCurrencyStatus.NoQuote, is CryptoCurrencyStatus.NoAccount, -> { TokenItemState.Subtitle2State.TextContent( - text = currencyStatus.getFormattedCryptoAmount(includeStaking = true), + text = status.getFormattedCryptoAmount(includeStaking = true), + isFlickering = status.value.isFlickering(), ) } is CryptoCurrencyStatus.Loading, @@ -221,6 +223,7 @@ class TokenItemStateConverter( -> { TokenItemState.FiatAmountState.Content( text = status.getFormattedFiatAmount(appCurrency = appCurrency, includeStaking = true), + isFlickering = status.value.isFlickering(), icons = buildList { if (!status.getStakedBalance().isZero()) { TokenItemState.FiatAmountState.Content.IconUM( @@ -228,6 +231,12 @@ class TokenItemStateConverter( useAccentColor = true, ).let(::add) } + if (status.value.getStatusSource() == StatusSource.ONLY_CACHE) { + TokenItemState.FiatAmountState.Content.IconUM( + iconRes = R.drawable.ic_error_sync_24, + useAccentColor = false, + ).let(::add) + } }.toImmutableList(), ) } @@ -248,6 +257,7 @@ class TokenItemStateConverter( price = fiatRate.getFormattedCryptoPrice(appCurrency), priceChangePercent = priceChange.format { percent() }, type = priceChange.getPriceChangeType(), + isFlickering = value.isFlickering(), ) } else { TokenItemState.SubtitleState.Unknown @@ -263,5 +273,15 @@ class TokenItemStateConverter( private fun BigDecimal.getPriceChangeType(): PriceChangeType { return PriceChangeConverter.fromBigDecimal(value = this) } + + private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = getStatusSource() == StatusSource.CACHE + + private fun CryptoCurrencyStatus.Value.getStatusSource(): StatusSource? { + return when (this) { + is CryptoCurrencyStatus.Loaded -> source + is CryptoCurrencyStatus.NoAccount -> source + else -> null + } + } } } \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 2e43145350..696989eaf7 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -52,6 +52,7 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.moshi.adapters) + implementation(deps.moshi.adapters.ext) implementation(deps.okHttp) implementation(deps.okHttp.prettyLogging) implementation(deps.retrofit) diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt index 44e90009b0..c109e5b91e 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/TangemTechApi.kt @@ -123,6 +123,17 @@ interface TangemTechApi { @Body body: SeedPhraseNotificationDTO, ): ApiResponse + @GET("seedphrase-notification/{wallet_id}/confirmed") + suspend fun getSeedPhraseSecondNotificationStatus( + @Path("wallet_id") walletId: String, + ): ApiResponse + + @PUT("seedphrase-notification/{wallet_id}/confirmed") + suspend fun updateSeedPhraseSecondNotificationStatus( + @Path("wallet_id") walletId: String, + @Body body: SeedPhraseNotificationDTO, + ): ApiResponse + @GET("hot_crypto") suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse diff --git a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SeedPhraseNotificationDTO.kt b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SeedPhraseNotificationDTO.kt index e54c1cab47..3184e87ead 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SeedPhraseNotificationDTO.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/api/tangemTech/models/SeedPhraseNotificationDTO.kt @@ -19,6 +19,12 @@ data class SeedPhraseNotificationDTO(val status: Status) { @Json(name = "confirmed") CONFIRMED, + @Json(name = "rejected") + REJECTED, + + @Json(name = "accepted") + ACCEPTED, + ; } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index c515d594a1..21c50703c5 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -9,6 +9,7 @@ import com.tangem.datasource.api.common.adapter.DateTimeAdapter import com.tangem.datasource.api.common.adapter.LocalDateAdapter import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters import com.tangem.datasource.local.config.providers.models.ProviderModel +import com.tangem.datasource.local.network.entity.NetworkStatusDM import com.tangem.domain.models.scan.serialization.* import com.tangem.domain.visa.model.VisaActivationRemoteState import com.tangem.domain.visa.model.VisaCardActivationStatus @@ -16,6 +17,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dev.onenowy.moshipolymorphicadapter.NamePolymorphicAdapterFactory import javax.inject.Singleton @Module @@ -38,6 +40,11 @@ class MoshiModule { .add(DateTimeAdapter()) .add(VisaActivationRemoteState.jsonAdapter) .add(VisaCardActivationStatus.jsonAdapter) + .add( + NamePolymorphicAdapterFactory.of(NetworkStatusDM::class.java) + .withSubtype(NetworkStatusDM.Verified::class.java, "amounts") + .withSubtype(NetworkStatusDM.NoAccount::class.java, "amount_to_create_account"), + ) .addLast(KotlinJsonAdapterFactory()) .addStakeKitEnumFallbackAdapters() .build() diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt index bbf8c38124..62010f5c24 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/NetworksStatusesStoreModule.kt @@ -7,7 +7,10 @@ import com.squareup.moshi.Moshi import com.tangem.datasource.local.datastore.RuntimeDataStore import com.tangem.datasource.local.network.DefaultNetworksStatusesStore import com.tangem.datasource.local.network.NetworksStatusesStore -import com.tangem.datasource.local.network.utils.NetworkStatusesSerializer +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.mapWithStringKeyTypes +import com.tangem.datasource.utils.setTypes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -30,7 +33,11 @@ internal object NetworksStatusesStoreModule { return DefaultNetworksStatusesStore( runtimeDataStore = RuntimeDataStore(), persistenceDataStore = DataStoreFactory.create( - serializer = NetworkStatusesSerializer(moshi), + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(valueTypes = setTypes()), + defaultValue = emptyMap(), + ), produceFile = { context.dataStoreFile(fileName = "networks_statuses") }, scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), ), diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt index 59b45e9e38..655f4a41b3 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/StakingStoreModule.kt @@ -4,9 +4,13 @@ import android.content.Context import androidx.datastore.core.DataStoreFactory import androidx.datastore.dataStoreFile import com.squareup.moshi.Moshi +import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO import com.tangem.datasource.local.datastore.RuntimeDataStore +import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.token.* -import com.tangem.datasource.local.token.utils.YieldBalancesSerializer +import com.tangem.datasource.utils.MoshiDataStoreSerializer +import com.tangem.datasource.utils.mapWithStringKeyTypes +import com.tangem.datasource.utils.setTypes import com.tangem.utils.coroutines.CoroutineDispatcherProvider import dagger.Module import dagger.Provides @@ -35,11 +39,16 @@ internal object StakingStoreModule { dispatchers: CoroutineDispatcherProvider, ): StakingBalanceStore { return DefaultStakingBalanceStore( - dataStore = DataStoreFactory.create( - serializer = YieldBalancesSerializer(moshi), + persistenceStore = DataStoreFactory.create( + serializer = MoshiDataStoreSerializer( + moshi = moshi, + types = mapWithStringKeyTypes(valueTypes = setTypes()), + defaultValue = emptyMap(), + ), produceFile = { context.dataStoreFile(fileName = "yield_balances") }, scope = CoroutineScope(context = dispatchers.io + SupervisorJob()), ), + runtimeStore = RuntimeSharedStore(), ) } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt index 437628831c..f3e3d06f65 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt @@ -2,25 +2,25 @@ package com.tangem.datasource.local.network import androidx.datastore.core.DataStore import com.tangem.datasource.local.datastore.RuntimeDataStore -import com.tangem.datasource.local.network.entity.NetworkStatusesDM -import com.tangem.datasource.local.network.utils.toDataModel -import com.tangem.datasource.local.network.utils.toDomainModel +import com.tangem.datasource.local.network.converter.NetworkStatusConverter +import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace -import com.tangem.utils.extensions.replaceBy +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.* -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.launch + +private typealias NetworkStatusesByWalletId = Map> internal class DefaultNetworksStatusesStore( private val runtimeDataStore: RuntimeDataStore>, - private val persistenceDataStore: DataStore, + private val persistenceDataStore: DataStore, ) : NetworksStatusesStore { - private val mutex = Mutex() - override fun get(key: UserWalletId): Flow> { return runtimeDataStore.get(provideStringKey(key)) } @@ -33,17 +33,26 @@ internal class DefaultNetworksStatusesStore( .firstOrNull { it.id == status.networkId } ?: return@mapNotNullTo null - status.toDomainModel(network) + NetworkStatusConverter(network = network, isCached = true).convert(value = status) } .orEmpty() if (cachedStatuses.isNotEmpty()) { send(cachedStatuses) + + /** + * Required for storing cache data. + * This will help to recognize networks that are still uploaded. + * + * @see mergeStatuses + */ + storeAll(key = key, values = cachedStatuses) } runtimeDataStore.get(provideStringKey(key)) .onEach { runtimeStatuses -> val mergedStatuses = mergeStatuses( + networks = networks, cachedStatuses = cachedStatuses, runtimeStatuses = runtimeStatuses, ) @@ -54,36 +63,20 @@ internal class DefaultNetworksStatusesStore( } override suspend fun getSyncOrNull(key: UserWalletId): Set? { - return runtimeDataStore.getSyncOrNull(provideStringKey(key)) + return runtimeDataStore.getSyncOrNull(key = provideStringKey(key)) } override suspend fun store(key: UserWalletId, value: NetworkStatus) { - mutex.withLock { - val newValues = getSyncOrNull(key) - ?.addOrReplace(value) { it.network == value.network } - ?: setOf(value) - - runtimeDataStore.store(provideStringKey(key), newValues) - storeNetworkStatusInPersistence(key, value) - } + storeAll(key = key, values = setOf(value)) } - override suspend fun storeAll(key: UserWalletId, values: Collection) { - mutex.withLock { - val currentValues = getSyncOrNull(key) ?: emptySet() - val updatedValues = currentValues.toMutableSet() + override suspend fun storeAll(key: UserWalletId, values: Set) { + coroutineScope { + val updatedValues = getSyncOrNull(key).orEmpty() + .addOrReplace(items = values) { prev, new -> prev.network == new.network } - values.forEach { newValue -> - val isReplaced = updatedValues.replaceBy(newValue) { - it.network == newValue.network - } - - if (!isReplaced) { - updatedValues.add(newValue) - } - } - - runtimeDataStore.store(provideStringKey(key), updatedValues) + launch { runtimeDataStore.store(key = provideStringKey(key), value = updatedValues) } + launch { storeNetworkStatusInPersistence(userWalletId = key, statuses = updatedValues) } } } @@ -93,29 +86,48 @@ internal class DefaultNetworksStatusesStore( * If a status with the same network is in both sets, the status from [runtimeStatuses] is used. */ private fun mergeStatuses( + networks: Set, cachedStatuses: Set, runtimeStatuses: Set, ): Set { - val runtimeMap = runtimeStatuses.associateBy { it.network } - val mergedCached = cachedStatuses.filter { it.network !in runtimeMap.keys } + return networks.mapNotNullTo(hashSetOf()) { network -> + val runtimeStatus = runtimeStatuses.firstOrNull { it.network == network } - return mergedCached.toSet() + runtimeStatuses + if (runtimeStatus == null || runtimeStatus.value is NetworkStatus.Unreachable) { + getCachedStatusIfPossible(cachedStatuses = cachedStatuses, network = network) + ?: runtimeStatus + } else { + runtimeStatus + } + } } - private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) { - val status = networkStatus.value - val network = networkStatus.network + private fun getCachedStatusIfPossible(cachedStatuses: Set, network: Network): NetworkStatus? { + val cached = cachedStatuses.firstOrNull { it.network == network } ?: return null - if (status !is NetworkStatus.Verified) return + val updatedCachedStatus = when (val status = cached.value) { + is NetworkStatus.NoAccount -> status.copy(source = StatusSource.ONLY_CACHE) + is NetworkStatus.Verified -> status.copy(source = StatusSource.ONLY_CACHE) + is NetworkStatus.Refreshing, + is NetworkStatus.Unreachable, + is NetworkStatus.MissedDerivation, + -> null + } + + return if (updatedCachedStatus != null) { + cached.copy(value = updatedCachedStatus) + } else { + null + } + } + + private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, statuses: Set) { + // Converter will return null if the network status is not supported + val newStatuses = NetworkStatusDataModelConverter.convertSet(input = statuses).filterNotNull().toSet() persistenceDataStore.updateData { storedStatuses -> - val userWalletStatuses = storedStatuses[userWalletId.stringValue] ?: emptySet() - val updatedStatuses = userWalletStatuses.addOrReplace(status.toDataModel(network)) { - it.networkId == network.id - } - storedStatuses.toMutableMap().apply { - this[userWalletId.stringValue] = updatedStatuses + this[userWalletId.stringValue] = newStatuses } } } diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt index 6b191e150d..81838f97aa 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/NetworksStatusesStore.kt @@ -15,5 +15,5 @@ interface NetworksStatusesStore { suspend fun store(key: UserWalletId, value: NetworkStatus) - suspend fun storeAll(key: UserWalletId, values: Collection) + suspend fun storeAll(key: UserWalletId, values: Set) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt new file mode 100644 index 0000000000..e0c0192c84 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt @@ -0,0 +1,60 @@ +package com.tangem.datasource.local.network.converter + +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.tokens.model.NetworkAddress +import com.tangem.utils.converter.TwoWayConverter +import timber.log.Timber + +/** + * Converter from [Set] to [NetworkAddress] and vice versa + * +[REDACTED_AUTHOR] + */ +internal class NetworkAddressConverter( + private val selectedAddress: String, +) : TwoWayConverter, NetworkAddress> { + + override fun convert(value: Set): NetworkAddress { + val defaultAddress = value + .firstOrNull { it.value == selectedAddress } + ?.let(::toNetworkAddress) + + requireNotNull(defaultAddress) { "Selected address must not be null" } + + return if (value.size != 1) { + NetworkAddress.Selectable( + defaultAddress = defaultAddress, + availableAddresses = value.mapTo(destination = hashSetOf(), transform = ::toNetworkAddress), + ) + } else { + NetworkAddress.Single(defaultAddress = defaultAddress) + } + } + + override fun convertBack(value: NetworkAddress): Set { + return value.availableAddresses + .map { address -> + NetworkStatusDM.Address( + value = address.value, + type = when (address.type) { + NetworkAddress.Address.Type.Primary -> NetworkStatusDM.Address.Type.Primary + NetworkAddress.Address.Type.Secondary -> NetworkStatusDM.Address.Type.Secondary + }, + ) + } + .toSet() + } + + private fun toNetworkAddress(address: NetworkStatusDM.Address): NetworkAddress.Address { + val type = when (address.type) { + NetworkStatusDM.Address.Type.Primary -> NetworkAddress.Address.Type.Primary + NetworkStatusDM.Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary + } + + if (address.value.isBlank()) { + Timber.w("Address value is blank") + } + + return NetworkAddress.Address(value = address.value, type = type) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt new file mode 100644 index 0000000000..cea7a8a4b7 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt @@ -0,0 +1,35 @@ +package com.tangem.datasource.local.network.converter + +import com.tangem.common.extensions.mapNotNullValues +import com.tangem.domain.tokens.model.CryptoCurrency +import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus +import com.tangem.utils.converter.TwoWayConverter +import java.math.BigDecimal + +private typealias AmountsDataModel = Map +private typealias AmountsDomainModel = Map + +/** + * Converter from [AmountsDataModel] to [AmountsDomainModel] and vice versa + * +[REDACTED_AUTHOR] + */ +internal object NetworkAmountsConverter : TwoWayConverter { + + override fun convert(value: AmountsDataModel): AmountsDomainModel { + return value + .mapKeys { CryptoCurrency.ID.fromValue(value = it.key) } + .mapValues { (_, amount) -> CryptoCurrencyAmountStatus.Loaded(value = amount) } + } + + override fun convertBack(value: AmountsDomainModel): AmountsDataModel { + return value + .mapKeys { (id, _) -> id.value } + .mapNotNullValues { (_, amount) -> + when (amount) { + is CryptoCurrencyAmountStatus.Loaded -> amount.value + is CryptoCurrencyAmountStatus.NotFound -> null + } + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt new file mode 100644 index 0000000000..f0a01555bf --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt @@ -0,0 +1,47 @@ +package com.tangem.datasource.local.network.converter + +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.models.StatusSource +import com.tangem.domain.tokens.model.Network +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.utils.converter.Converter + +/** + * Converter from [NetworkStatusDM] to [NetworkStatus] + * + * @property network network + * @property isCached flag that determines whether the status is a cache + * +[REDACTED_AUTHOR] + */ +internal class NetworkStatusConverter( + private val network: Network, + private val isCached: Boolean, +) : Converter { + + override fun convert(value: NetworkStatusDM): NetworkStatus { + val address = NetworkAddressConverter(selectedAddress = value.selectedAddress) + .convert(value = value.availableAddresses) + + val status = when (value) { + is NetworkStatusDM.Verified -> { + NetworkStatus.Verified( + address = address, + amounts = NetworkAmountsConverter.convert(value = value.amounts), + pendingTransactions = mapOf(), + source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, + ) + } + is NetworkStatusDM.NoAccount -> { + NetworkStatus.NoAccount( + address = address, + amountToCreateAccount = value.amountToCreateAccount, + errorMessage = value.errorMessage, + source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, + ) + } + } + + return NetworkStatus(network = network, value = status) + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt new file mode 100644 index 0000000000..cf42370c96 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt @@ -0,0 +1,38 @@ +package com.tangem.datasource.local.network.converter + +import com.tangem.datasource.local.network.entity.NetworkStatusDM +import com.tangem.domain.tokens.model.NetworkStatus +import com.tangem.utils.converter.Converter + +/** + * Converter from [NetworkStatus] to [NetworkStatusDM] + * +[REDACTED_AUTHOR] + */ +internal object NetworkStatusDataModelConverter : Converter { + + override fun convert(value: NetworkStatus): NetworkStatusDM? { + return when (val status = value.value) { + is NetworkStatus.Verified -> { + NetworkStatusDM.Verified( + networkId = value.network.id, + selectedAddress = status.address.defaultAddress.value, + availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value) + .convertBack(value = status.address), + amounts = NetworkAmountsConverter.convertBack(value = status.amounts), + ) + } + is NetworkStatus.NoAccount -> { + NetworkStatusDM.NoAccount( + networkId = value.network.id, + selectedAddress = status.address.defaultAddress.value, + availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value) + .convertBack(value = status.address), + amountToCreateAccount = status.amountToCreateAccount, + errorMessage = status.errorMessage, + ) + } + else -> null + } + } +} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt index 6c681b5cf8..b07ad107c1 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/entity/NetworkStatusDM.kt @@ -1,27 +1,48 @@ package com.tangem.datasource.local.network.entity +import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.tangem.domain.tokens.model.Network +import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel import java.math.BigDecimal -internal typealias NetworkStatusesDM = Map> +internal sealed interface NetworkStatusDM { -@JsonClass(generateAdapter = true) -internal data class NetworkStatusDM( - val networkId: Network.ID, - val selectedAddress: String, - val availableAddresses: Set
, - val amounts: Map, -) { + val networkId: Network.ID + val selectedAddress: String + val availableAddresses: Set
+ + @NameLabel("amounts") + data class Verified( + @Json(name = "network_id") override val networkId: Network.ID, + @Json(name = "selected_address") override val selectedAddress: String, + @Json(name = "available_addresses") override val availableAddresses: Set
, + @Json(name = "amounts") val amounts: Map, + ) : NetworkStatusDM + + @NameLabel("amount_to_create_account") + data class NoAccount( + @Json(name = "network_id") override val networkId: Network.ID, + @Json(name = "selected_address") override val selectedAddress: String, + @Json(name = "available_addresses") override val availableAddresses: Set
, + @Json(name = "amount_to_create_account") val amountToCreateAccount: BigDecimal, + @Json(name = "error_message") val errorMessage: String, + ) : NetworkStatusDM @JsonClass(generateAdapter = true) data class Address( - val value: String, - val type: Type, + @Json(name = "value") val value: String, + @Json(name = "type") val type: Type, ) { + @JsonClass(generateAdapter = false) enum class Type { - Primary, Secondary, + + @Json(name = "primary") + Primary, + + @Json(name = "secondary") + Secondary, } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusMapper.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusMapper.kt deleted file mode 100644 index cd0aa4071c..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusMapper.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.tangem.datasource.local.network.utils - -import com.tangem.common.extensions.mapNotNullValues -import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.domain.tokens.model.* -import timber.log.Timber -import java.math.BigDecimal - -internal fun NetworkStatus.Verified.toDataModel(network: Network): NetworkStatusDM { - return NetworkStatusDM( - networkId = network.id, - selectedAddress = address.defaultAddress.value, - availableAddresses = address.availableAddresses - .map { address -> - NetworkStatusDM.Address( - value = address.value, - type = when (address.type) { - NetworkAddress.Address.Type.Primary -> NetworkStatusDM.Address.Type.Primary - NetworkAddress.Address.Type.Secondary -> NetworkStatusDM.Address.Type.Secondary - }, - ) - } - .toSet(), - amounts = amounts - .mapKeys { (id, _) -> id.value } - .mapNotNullValues { (_, amount) -> - when (amount) { - is CryptoCurrencyAmountStatus.Loaded -> amount.value - is CryptoCurrencyAmountStatus.NotFound -> null - } - }, - ) -} - -internal fun NetworkStatusDM.toDomainModel(network: Network): NetworkStatus { - return NetworkStatus( - network = network, - value = NetworkStatus.Verified( - address = mapToDomainAddress(selectedAddress, availableAddresses), - amounts = mapToDomainAmounts(amounts), - pendingTransactions = mapOf(), - ), - ) -} - -private fun mapToDomainAmounts(amounts: Map): Map { - return amounts - .mapKeys { CryptoCurrency.ID.fromValue(it.key) } - .mapValues { (_, amount) -> CryptoCurrencyAmountStatus.Loaded(amount) } -} - -private fun mapToDomainAddress( - selectedAddress: String, - availableAddresses: Set, -): NetworkAddress { - val defaultAddress = availableAddresses - .firstOrNull { it.value == selectedAddress } - ?.let(::mapToDomainAddress) - - requireNotNull(defaultAddress) { "Selected address must not be null" } - - return if (availableAddresses.size != 1) { - NetworkAddress.Selectable(defaultAddress, availableAddresses.mapTo(hashSetOf(), ::mapToDomainAddress)) - } else { - NetworkAddress.Single(defaultAddress) - } -} - -private fun mapToDomainAddress(address: NetworkStatusDM.Address): NetworkAddress.Address { - val type = when (address.type) { - NetworkStatusDM.Address.Type.Primary -> NetworkAddress.Address.Type.Primary - NetworkStatusDM.Address.Type.Secondary -> NetworkAddress.Address.Type.Secondary - } - - if (address.value.isBlank()) { - Timber.w("Address value is blank") - } - - return NetworkAddress.Address(address.value, type) -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusesSerializer.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusesSerializer.kt deleted file mode 100644 index 487cd016b3..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusesSerializer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.datasource.local.network.utils - -import androidx.datastore.core.Serializer -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.tangem.datasource.local.network.entity.NetworkStatusDM -import com.tangem.datasource.local.network.entity.NetworkStatusesDM -import java.io.InputStream -import java.io.OutputStream - -internal class NetworkStatusesSerializer(moshi: Moshi) : Serializer { - - private val adapter by lazy { - val type = Types.newParameterizedType( - Map::class.java, - String::class.java, - Types.newParameterizedType(Set::class.java, NetworkStatusDM::class.java), - ) - - moshi.adapter(type) - } - - override val defaultValue: NetworkStatusesDM = emptyMap() - - override suspend fun readFrom(input: InputStream): NetworkStatusesDM { - return input.bufferedReader().use { reader -> - adapter.fromJson(reader.readText()) ?: defaultValue - } - } - - override suspend fun writeTo(t: NetworkStatusesDM, output: OutputStream) { - output.bufferedWriter().use { writer -> - writer.write(adapter.toJson(t)) - } - } -} \ 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 00e2ab95b4..434d4c4a92 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 @@ -119,6 +119,8 @@ object PreferencesKeys { val WAS_LOG_FILE_CLEARED by lazy { booleanPreferencesKey(name = "wasLogFileCleared") } + val SEED_FIRST_NOTIFICATION_SHOW_TIME by lazy { longPreferencesKey("seedFirstNotificationTime") } + fun getShouldShowStoriesKey(storyId: String) = booleanPreferencesKey("shouldShowStories_$storyId") // region Permission diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt index 150f67eb66..d7e5299c72 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/DefaultQuotesStore.kt @@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore import com.tangem.datasource.api.tangemTech.models.QuotesResponse import com.tangem.datasource.local.datastore.RuntimeSharedStore import com.tangem.datasource.local.quote.converter.QuoteConverter +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import kotlinx.coroutines.coroutineScope @@ -32,7 +33,12 @@ internal class DefaultQuotesStore( runtimeStore.get() .onEach { - val mergedQuotes = mergeQuotes(cachedQuotes = cachedQuotes, runtimeQuotes = it) + val mergedQuotes = mergeQuotes( + currenciesIds = currenciesIds, + cachedQuotes = cachedQuotes, + runtimeQuotes = it, + ) + send(element = mergedQuotes) } .launchIn(scope = this) @@ -51,6 +57,14 @@ internal class DefaultQuotesStore( } } + override suspend fun storeEmptyQuotes(currenciesIds: Set) { + runtimeStore.update(default = emptySet()) { saved -> + val new = currenciesIds.map { Quote.Empty(it) } + + (saved + new).distinctBy { it.rawCurrencyId }.toSet() + } + } + private suspend fun getCachedQuotes(currenciesIds: Set): Set { val ids = currenciesIds.map(CryptoCurrency.RawID::value).toSet() val cachedQuotes = persistenceStore.data.firstOrNull().orEmpty().filterKeys { it in ids } @@ -58,15 +72,27 @@ internal class DefaultQuotesStore( return QuoteConverter(isCached = true).convertSet(input = cachedQuotes.entries) } - private fun mergeQuotes(cachedQuotes: Set, runtimeQuotes: Set): Set { - return runtimeQuotes.map { runtimeQuote -> - if (runtimeQuote is Quote.Empty) { - cachedQuotes.firstOrNull { runtimeQuote.rawCurrencyId == it.rawCurrencyId } ?: runtimeQuote - } else { - runtimeQuote + private fun mergeQuotes( + currenciesIds: Set, + cachedQuotes: Set, + runtimeQuotes: Set, + ): Set { + return currenciesIds + .mapTo(hashSetOf()) { currencyId -> + val runtimeQuote = runtimeQuotes.firstOrNull { it.rawCurrencyId == currencyId } + + if (runtimeQuote == null || runtimeQuote is Quote.Empty) { + getCachedQuoteIfPossible(cachedStatuses = cachedQuotes, currencyId = currencyId) + } else { + runtimeQuote + } } - } - .toSet() + } + + private fun getCachedQuoteIfPossible(cachedStatuses: Set, currencyId: CryptoCurrency.RawID): Quote { + return cachedStatuses.firstOrNull { it.rawCurrencyId == currencyId } + ?.copy(source = StatusSource.ONLY_CACHE) + ?: Quote.Empty(currencyId) } private suspend fun storeInRuntimeStore(response: QuotesResponse) { diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt index feb0872d92..4d632c3558 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/QuotesStore.kt @@ -16,4 +16,7 @@ interface QuotesStore { /** Store [response] from remote */ suspend fun store(response: QuotesResponse) + + /** Store [Quote.Empty] for [currenciesIds] */ + suspend fun storeEmptyQuotes(currenciesIds: Set) } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt index 314d2ee920..120bea1730 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/quote/converter/QuoteConverter.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.quote.converter import com.tangem.datasource.api.tangemTech.models.QuotesResponse +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import com.tangem.utils.converter.Converter @@ -23,7 +24,7 @@ internal class QuoteConverter(private val isCached: Boolean) : rawCurrencyId = CryptoCurrency.RawID(currencyId), fiatRate = quote.price.orZero(), priceChange = quote.priceChange24h.orZero().movePointLeft(2), - isCached = isCached, + source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, ) } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index 28f5e8298f..50de014b59 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt @@ -2,52 +2,83 @@ package com.tangem.datasource.local.token import androidx.datastore.core.DataStore import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.datasource.local.token.entity.YieldBalanceWrappersDTO +import com.tangem.datasource.local.datastore.RuntimeSharedStore +import com.tangem.datasource.local.token.converter.YieldBalanceConverter +import com.tangem.domain.models.StatusSource +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.extensions.addOrReplace -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +internal typealias YieldBalanceWrappersDTO = Map> +internal typealias YieldBalanceListByWalletId = Map> + +/** + * Default implementation of [StakingBalanceStore] + * + * @property persistenceStore persistence store + * @property runtimeStore runtime store + */ internal class DefaultStakingBalanceStore( - private val dataStore: DataStore, + private val persistenceStore: DataStore, + private val runtimeStore: RuntimeSharedStore, ) : StakingBalanceStore { - override fun get(userWalletId: UserWalletId): Flow> { - return dataStore.data.map { it[userWalletId.stringValue].orEmpty() } - } - - override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? { - return dataStore.data.firstOrNull() - ?.get(userWalletId.stringValue) - } - - override suspend fun store(userWalletId: UserWalletId, items: Set) { - dataStore.updateData { current -> - current.toMutableMap().apply { - this[userWalletId.stringValue] = items + override fun get(userWalletId: UserWalletId): Flow> = channelFlow { + val cachedBalances = persistenceStore.data + .map { + val wrappers = it[userWalletId.stringValue].orEmpty() + YieldBalanceConverter(isCached = true).convertSet(input = wrappers) } + .firstOrNull() + .orEmpty() + + if (cachedBalances.isNotEmpty()) { + send(cachedBalances) + } + + runtimeStore.get() + .map { it[userWalletId].orEmpty() } + .onEach { + val mergedBalances = mergeYieldBalances(cachedBalances = cachedBalances, runtimeBalances = it) + + send(mergedBalances) + } + .launchIn(scope = this) + } + + override fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow { + return get(userWalletId).map { balances -> + balances.getBalance(address = address, integrationId = integrationId) } } - override fun get( - userWalletId: UserWalletId, - address: String, - integrationId: String, - ): Flow { - return get(userWalletId) - .map { balances -> - balances.firstOrNull { it.integrationId == integrationId && it.addresses.address == address } - } + override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? { + return runtimeStore.getSyncOrNull()?.getValue(userWalletId) } override suspend fun getSyncOrNull( userWalletId: UserWalletId, address: String, integrationId: String, - ): YieldBalanceWrapperDTO? { - return getSyncOrNull(userWalletId) - ?.firstOrNull { it.integrationId == integrationId && it.addresses.address == address } + ): YieldBalance? { + val balances = getSyncOrNull(userWalletId) ?: return null + + return balances.getBalance(address, integrationId) + } + + override suspend fun store(userWalletId: UserWalletId, items: Set) { + coroutineScope { + launch { + storeInRuntimeStore( + userWalletId = userWalletId, + items = YieldBalanceConverter(isCached = false).convertSet(input = items), + ) + } + launch { storeInPersistenceStore(userWalletId = userWalletId, items = items) } + } } override suspend fun store( @@ -56,10 +87,94 @@ internal class DefaultStakingBalanceStore( address: String, item: YieldBalanceWrapperDTO, ) { - val balances = getSyncOrNull(userWalletId) - ?.addOrReplace(item) { it.integrationId == integrationId && it.addresses.address == address } - ?: setOf(item) + coroutineScope { + launch { + storeInRuntimeStore(userWalletId, integrationId, address, item) + storeInPersistenceStore(userWalletId, integrationId, address, item) + } + } + } - store(userWalletId, balances) + private suspend fun storeInRuntimeStore( + userWalletId: UserWalletId, + integrationId: String, + address: String, + item: YieldBalanceWrapperDTO, + ) { + val newBalance = YieldBalanceConverter(isCached = false).convert(value = item) + + val balances = getSyncOrNull(userWalletId) + ?.addOrReplace(newBalance) { it.integrationId == integrationId && it.address == address } + ?: setOf(newBalance) + + storeInRuntimeStore(userWalletId = userWalletId, items = balances) + } + + private suspend fun storeInRuntimeStore(userWalletId: UserWalletId, items: Set) { + runtimeStore.update(default = emptyMap()) { + it.toMutableMap().apply { + this[userWalletId] = items + } + } + } + + private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, items: Set) { + persistenceStore.updateData { current -> + current.toMutableMap().apply { + this[userWalletId.stringValue] = items + } + } + } + + private suspend fun storeInPersistenceStore( + userWalletId: UserWalletId, + integrationId: String, + address: String, + item: YieldBalanceWrapperDTO, + ) { + persistenceStore.updateData { current -> + current.toMutableMap().apply { + this[userWalletId.stringValue] = current[userWalletId.stringValue] + ?.addOrReplace(item) { it.integrationId == integrationId && it.addresses.address == address } + ?: setOf(item) + } + } + } + + private fun mergeYieldBalances( + cachedBalances: Set, + runtimeBalances: Set, + ): Set { + return runtimeBalances + .map { runtime -> + runtime.takeIf { runtime !is YieldBalance.Error } + ?: getCachedBalanceIfPossible(cachedBalances, runtime) + } + .toSet() + } + + private fun getCachedBalanceIfPossible(cachedBalances: Set, runtime: YieldBalance): YieldBalance { + val cached = cachedBalances.getBalance(address = runtime.address, integrationId = runtime.integrationId) + ?: return runtime + + val updatedCached = when (cached) { + is YieldBalance.Data -> cached.copy(source = StatusSource.ONLY_CACHE) + is YieldBalance.Empty -> cached.copy(source = StatusSource.ONLY_CACHE) + is YieldBalance.Error -> null + } + + return updatedCached ?: runtime + } + + private fun Set.getBalance(address: String?, integrationId: String?): YieldBalance? { + return firstOrNull { yieldBalance -> + val data = yieldBalance as? YieldBalance.Data + val balance = data?.balance + + val isCorrectAddress = address != null && address == data?.address + val isCorrectIntegration = integrationId != null && balance?.integrationId == integrationId + + isCorrectIntegration && isCorrectAddress + } } } \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt index 8e10d5f395..0638d1ac9f 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/StakingBalanceStore.kt @@ -1,24 +1,29 @@ package com.tangem.datasource.local.token import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.stakekit.YieldBalance +import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow +/** Staking balance store */ interface StakingBalanceStore { - fun get(userWalletId: UserWalletId): Flow> + /** Get flow of [YieldBalanceList] by [userWalletId] */ + fun get(userWalletId: UserWalletId): Flow> - suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? + /** Get flow of [YieldBalance] by [userWalletId], [address] and [integrationId] */ + fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow + /** Get [YieldBalanceList] synchronously or null by [userWalletId] */ + suspend fun getSyncOrNull(userWalletId: UserWalletId): Set? + + /** Get [YieldBalance] synchronously or null by [userWalletId], [address] and [integrationId] */ + suspend fun getSyncOrNull(userWalletId: UserWalletId, address: String, integrationId: String): YieldBalance? + + /** Store [items] by [userWalletId] */ suspend fun store(userWalletId: UserWalletId, items: Set) - fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow - - suspend fun getSyncOrNull( - userWalletId: UserWalletId, - address: String, - integrationId: String, - ): YieldBalanceWrapperDTO? - + /** Store [item] by [userWalletId], [integrationId] and [address] */ suspend fun store(userWalletId: UserWalletId, integrationId: String, address: String, item: YieldBalanceWrapperDTO) } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/BalanceTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt similarity index 86% rename from data/staking/src/main/java/com/tangem/data/staking/converters/BalanceTypeConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt index a7260e3f31..a1e9edcb49 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/BalanceTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/BalanceTypeConverter.kt @@ -1,10 +1,10 @@ -package com.tangem.data.staking.converters +package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO.BalanceTypeDTO import com.tangem.domain.staking.model.stakekit.BalanceType import com.tangem.utils.converter.Converter -internal class BalanceTypeConverter : Converter { +internal object BalanceTypeConverter : Converter { override fun convert(value: BalanceTypeDTO): BalanceType { return when (value) { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/PendingActionConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt similarity index 83% rename from data/staking/src/main/java/com/tangem/data/staking/converters/action/PendingActionConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt index c9ddee7f77..716d5fc960 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/PendingActionConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/PendingActionConverter.kt @@ -1,16 +1,14 @@ -package com.tangem.data.staking.converters.action +package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.BalanceDTO import com.tangem.domain.staking.model.stakekit.PendingAction import com.tangem.utils.converter.Converter -internal class PendingActionConverter : Converter { - - private val stakingActionTypeConverter by lazy(LazyThreadSafetyMode.NONE) { StakingActionTypeConverter() } +internal object PendingActionConverter : Converter { override fun convert(value: BalanceDTO.PendingAction): PendingAction { return PendingAction( - type = stakingActionTypeConverter.convert(value.type), + type = StakingActionTypeConverter.convert(value.type), passthrough = value.passthrough, args = with(value.args) { PendingAction.PendingActionArgs( diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt similarity index 91% rename from data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt index 95829958e7..d95777148f 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/StakingActionTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingActionTypeConverter.kt @@ -1,11 +1,11 @@ -package com.tangem.data.staking.converters.action +package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.action.StakingActionTypeDTO import com.tangem.domain.staking.model.stakekit.action.StakingActionType import com.tangem.utils.converter.Converter @Suppress("CyclomaticComplexMethod") -class StakingActionTypeConverter : Converter { +object StakingActionTypeConverter : Converter { override fun convert(value: StakingActionTypeDTO): StakingActionType { return when (value) { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt similarity index 98% rename from data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt index 82ebb3dbbb..e69821a778 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/StakingNetworkTypeConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/StakingNetworkTypeConverter.kt @@ -1,11 +1,11 @@ -package com.tangem.data.staking.converters +package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.NetworkTypeDTO import com.tangem.domain.staking.model.stakekit.NetworkType import com.tangem.utils.converter.TwoWayConverter @Suppress("CyclomaticComplexMethod", "LongMethod") -class StakingNetworkTypeConverter : TwoWayConverter { +object StakingNetworkTypeConverter : TwoWayConverter { override fun convert(value: NetworkTypeDTO): NetworkType { return when (value) { diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt similarity index 74% rename from data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt index 2f67326a0c..4a363aa324 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/TokenConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/TokenConverter.kt @@ -1,17 +1,15 @@ -package com.tangem.data.staking.converters +package com.tangem.datasource.local.token.converter import com.tangem.datasource.api.stakekit.models.response.model.TokenDTO import com.tangem.domain.staking.model.stakekit.Token import com.tangem.utils.converter.TwoWayConverter -class TokenConverter( - private val stakingNetworkTypeConverter: StakingNetworkTypeConverter, -) : TwoWayConverter { +object TokenConverter : TwoWayConverter { override fun convert(value: TokenDTO): Token { return Token( name = value.name, - network = stakingNetworkTypeConverter.convert(value.network), + network = StakingNetworkTypeConverter.convert(value.network), symbol = value.symbol, decimals = value.decimals, address = value.address, @@ -24,7 +22,7 @@ class TokenConverter( override fun convertBack(value: Token): TokenDTO { return TokenDTO( name = value.name, - network = stakingNetworkTypeConverter.convertBack(value.network), + network = StakingNetworkTypeConverter.convertBack(value.network), symbol = value.symbol, decimals = value.decimals, address = value.address, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt similarity index 64% rename from data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt rename to core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index c0807165b7..9df25f3294 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -1,36 +1,37 @@ -package com.tangem.data.staking.converters +package com.tangem.datasource.local.token.converter -import com.tangem.data.staking.converters.action.PendingActionConverter import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.BalanceItem import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceItem import com.tangem.utils.converter.Converter -internal class YieldBalanceConverter : Converter { +internal class YieldBalanceConverter(private val isCached: Boolean) : Converter { - private val pendingActionConverter by lazy(LazyThreadSafetyMode.NONE) { PendingActionConverter() } - private val networkTypeConverter by lazy(LazyThreadSafetyMode.NONE) { StakingNetworkTypeConverter() } - private val tokenConverter by lazy(LazyThreadSafetyMode.NONE) { TokenConverter(networkTypeConverter) } - private val balanceTypeConverter by lazy(LazyThreadSafetyMode.NONE) { BalanceTypeConverter() } override fun convert(value: YieldBalanceWrapperDTO): YieldBalance { return if (value.balances.isEmpty()) { - YieldBalance.Empty + YieldBalance.Empty( + integrationId = value.integrationId, + address = value.addresses.address, + source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, + ) } else { YieldBalance.Data( + integrationId = value.integrationId, address = value.addresses.address, balance = YieldBalanceItem( items = value.balances.map { item -> BalanceItem( groupId = item.groupId, - token = tokenConverter.convert(item.tokenDTO), - type = balanceTypeConverter.convert(item.type), + token = TokenConverter.convert(item.tokenDTO), + type = BalanceTypeConverter.convert(item.type), amount = item.amount, rawCurrencyId = item.tokenDTO.coinGeckoId, // tron-specific. operates validatorAddresses instead of validatorAddress validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0), date = item.date?.toDateTime(), - pendingActions = pendingActionConverter + pendingActions = PendingActionConverter .convertList(item.pendingActions) .sortedBy { it.passthrough }, isPending = false, @@ -39,6 +40,7 @@ internal class YieldBalanceConverter : Converter> \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/utils/YieldBalancesSerializer.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/utils/YieldBalancesSerializer.kt deleted file mode 100644 index 421fd14414..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/utils/YieldBalancesSerializer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.tangem.datasource.local.token.utils - -import androidx.datastore.core.Serializer -import com.squareup.moshi.Moshi -import com.squareup.moshi.Types -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO -import com.tangem.datasource.local.token.entity.YieldBalanceWrappersDTO -import java.io.InputStream -import java.io.OutputStream - -internal class YieldBalancesSerializer(moshi: Moshi) : Serializer { - - private val adapter by lazy { - val type = Types.newParameterizedType( - Map::class.java, - String::class.java, - Types.newParameterizedType(Set::class.java, YieldBalanceWrapperDTO::class.java), - ) - - moshi.adapter(type) - } - - override val defaultValue: YieldBalanceWrappersDTO = emptyMap() - - override suspend fun readFrom(input: InputStream): YieldBalanceWrappersDTO { - return input.bufferedReader().use { reader -> - adapter.fromJson(reader.readText()) ?: defaultValue - } - } - - override suspend fun writeTo(t: YieldBalanceWrappersDTO, output: OutputStream) { - output.bufferedWriter().use { writer -> - writer.write(adapter.toJson(t)) - } - } -} \ No newline at end of file diff --git a/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiTypesExt.kt b/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiTypesExt.kt index 2330648ff4..cc20669688 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiTypesExt.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/utils/MoshiTypesExt.kt @@ -3,6 +3,10 @@ package com.tangem.datasource.utils import com.squareup.moshi.Types import java.lang.reflect.ParameterizedType +fun mapWithStringKeyTypes(valueTypes: ParameterizedType): ParameterizedType { + return Types.newParameterizedType(Map::class.java, String::class.java, valueTypes) +} + inline fun mapWithStringKeyTypes(): ParameterizedType { return Types.newParameterizedType(Map::class.java, String::class.java, T::class.java) } diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml index 60fe380d48..fd85f80b66 100644 --- a/core/res/src/main/res/values-fr/strings.xml +++ b/core/res/src/main/res/values-fr/strings.xml @@ -881,13 +881,13 @@ Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange. Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille. Nouveau fournisseur d\'échange disponible ! - Ayez confiance en bénéficiant d\'un support constant, garantissant que vos transactions se déroulent sans problème à tout moment + Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes Assistance 24 heures sur 24 - Les fournisseurs d\'échange de confiance vous permettent d\'échanger des actifs sans effort, en gardant tout en sécurité dans votre portefeuille + Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement Échangez Avec Nous Une sécurité de haut niveau et des fournisseurs vérifiés garantissant que vos actifs sont protégés à chaque échange Plus Sûr Que Jamais - Maximisez votre valeur avec des tarifs provenant d\'un large réseau de fournisseurs de confiance, en choisissant toujours le meilleur + Maximisez votre valeur avec des tarifs d\'un large réseau de fournisseurs de confiance Meilleurs Tarifs Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics Plus Simple Que Jamais diff --git a/core/res/src/main/res/values-ja/strings.xml b/core/res/src/main/res/values-ja/strings.xml index d05722217e..931cb0fd78 100644 --- a/core/res/src/main/res/values-ja/strings.xml +++ b/core/res/src/main/res/values-ja/strings.xml @@ -871,16 +871,16 @@ ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。 より多くのトークンをより良いレートで、ウォレット内にて直接交換します。 新しいスワッププロバイダーが利用可能になりました! - 継続的なサポートで、いつでもスムーズに取引を行うことができます - 24時間体制のサポート - 信頼できる交換プロバイダーは、ウォレット内の資産を安全に保管しながら、簡単に交換できるようにします。 - 当社の交換機能をご利用ください - トップレベルのセキュリティと認証済みプロバイダーが、スワップ時にユーザーの資産を確実に保護します。 - 最高に安全 + 24時間体制のサポートであらゆる問題に対応します。 + いつもここに + 複数の信頼できるプロバイダーが一箇所に集結。ウォレット内で様々な暗号資産を簡単に交換できます。 + ぜひスワップしてください + 失敗も死角もありません。取引は常に保護されます。 + 難攻不落の防御 幅広いネットワークの中から、常に最適なプロバイダーと料金レートを選択します - ベストレート + 破格のレート 手間がかからず直感的に操作でき、数回タップするだけでトークンを交換できます。 - 最高に簡単 + とにかく便利 この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。 金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです この金額には、サービスプロバイダーの手数料が含まれています。 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index dd9d95e11a..b463199a6f 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -674,6 +674,8 @@ Scan the card or ring to change its settings. The changes will impact only the card or ring you\'ve scanned and will not affect other devices tied to your wallet. Get your Tangem ready! Security Alert + No, I did not + Yes, guide me You don’t have enough funds in your balance to sell cryptocurrency. Please deposit the desired asset to proceed. Insufficient Balance Selling cryptocurrency is unavailable in your region at the moment. We’re actively working to bring this option to you soon—stay tuned! @@ -1089,6 +1091,8 @@ Enjoying Tangem? You must associate your token before receiving tokens Network rent fee required + Action required + Did you contact support via the app or email within 7 days of creating a wallet? If you did or are unsure, follow and complete the instructions. Thank you! All set! No further actions required. You will now be redirected to the official Tangem website. Please read and follow instructions there. Have you ever contacted the Tangem support team directly through this application? diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt index 13d405a0c7..761ade8992 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/internal/TokenPrice.kt @@ -20,6 +20,7 @@ import com.tangem.core.ui.R import com.tangem.core.ui.components.RectangleShimmer import com.tangem.core.ui.components.SpacerW4 import com.tangem.core.ui.components.SpacerW6 +import com.tangem.core.ui.components.flicker import com.tangem.core.ui.components.marketprice.PriceChangeType import com.tangem.core.ui.extensions.resolveReference import com.tangem.core.ui.extensions.stringReference @@ -37,6 +38,7 @@ internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier) price = state.price, type = state.type, priceChangePercent = state.priceChangePercent, + isFlickering = state.isFlickering, ) } is TokenPriceState.TextContent -> { @@ -58,11 +60,12 @@ internal fun TokenPrice(state: TokenPriceState?, modifier: Modifier = Modifier) @Composable private fun PriceBlock( price: String, + isFlickering: Boolean, modifier: Modifier = Modifier, type: PriceChangeType? = null, priceChangePercent: String? = null, ) { - Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { + Row(modifier = modifier.flicker(isFlickering), verticalAlignment = Alignment.CenterVertically) { PriceText(text = price, modifier = Modifier.weight(weight = 1f, fill = false)) SpacerW6() @@ -149,16 +152,19 @@ private class TokenPriceChangeStateProvider : CollectionPreviewParameterProvider price = "1.234", priceChangePercent = "2.5%", type = PriceChangeType.UP, + isFlickering = false, ), TokenPriceState.CryptoPriceContent( price = "1.234", priceChangePercent = "2.5%", type = PriceChangeType.DOWN, + isFlickering = false, ), TokenPriceState.CryptoPriceContent( price = "1.234", priceChangePercent = "2.5%", type = PriceChangeType.NEUTRAL, + isFlickering = false, ), TokenPriceState.TextContent(value = stringReference(value = "Subtitle"), isAvailable = true), TokenPriceState.Unknown, diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt index 0a92c4ec25..8bc21a0f65 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/token/state/TokenItemState.kt @@ -181,6 +181,7 @@ sealed class TokenItemState { val price: String, val priceChangePercent: String, val type: PriceChangeType, + val isFlickering: Boolean = false, ) : SubtitleState() data class TextContent(val value: TextReference, val isAvailable: Boolean = true) : SubtitleState() diff --git a/core/utils/src/main/java/com/tangem/utils/TimeConstants.kt b/core/utils/src/main/java/com/tangem/utils/TimeConstants.kt new file mode 100644 index 0000000000..a2bf41adfe --- /dev/null +++ b/core/utils/src/main/java/com/tangem/utils/TimeConstants.kt @@ -0,0 +1,4 @@ +package com.tangem.utils + +const val H24_MILLIS = 24L * 60 * 60 * 1000 +const val WEEK_MILLIS = 7L * H24_MILLIS \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt index 86bdfe2e6e..cad9f1fada 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/List.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/List.kt @@ -64,4 +64,12 @@ inline fun MutableList.addOrReplace(item: T, predicate: (T) -> Boolean) { if (!isReplaced) { add(item) } +} + +fun List.filterIf(condition: Boolean, predicate: (T) -> Boolean): List { + return if (condition) { + this.filter(predicate) + } else { + this + } } \ No newline at end of file diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt index f5c0cf1eb7..f15a1a5f30 100644 --- a/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt +++ b/core/utils/src/main/java/com/tangem/utils/extensions/Set.kt @@ -36,4 +36,16 @@ inline fun Set.addOrReplace(item: T, predicate: (T) -> Boolean): Set { } return mutableList +} + +inline fun Set.addOrReplace(items: Set, predicate: (T, T) -> Boolean): Set { + val updatedValues = this.toMutableSet() + + items.forEach { newValue -> + val isReplaced = updatedValues.replaceBy(item = newValue, predicate = { predicate(it, newValue) }) + + if (!isReplaced) updatedValues.add(newValue) + } + + return updatedValues } \ No newline at end of file diff --git a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt index 4d16a5b159..3f53fb1c9c 100644 --- a/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt +++ b/data/onramp/src/main/java/com/tangem/data/onramp/converters/HotCryptoCurrencyConverter.kt @@ -6,6 +6,7 @@ import com.tangem.blockchainsdk.utils.fromNetworkId import com.tangem.data.common.currency.CryptoCurrencyFactory import com.tangem.data.common.currency.getNetwork import com.tangem.datasource.api.tangemTech.models.HotCryptoResponse +import com.tangem.domain.models.StatusSource import com.tangem.domain.models.scan.ScanResponse import com.tangem.domain.onramp.model.HotCryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrency @@ -94,7 +95,7 @@ internal class HotCryptoCurrencyConverter( rawCurrencyId = rawCurrencyId, fiatRate = fiatRate, priceChange = priceChange.movePointLeft(2), - isCached = false, // It doesn't matter + source = StatusSource.ACTUAL, // It doesn't matter ) } else { Quote.Empty(rawCurrencyId) diff --git a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt index 4694b9f4d6..4e4c6e7833 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/DefaultStakingRepository.kt @@ -15,10 +15,10 @@ import com.tangem.common.extensions.hexToBytes import com.tangem.common.extensions.toCompressedPublicKey import com.tangem.data.common.api.safeApiCall import com.tangem.data.common.cache.CacheRegistry -import com.tangem.data.staking.converters.* +import com.tangem.data.staking.converters.YieldBalanceListConverter +import com.tangem.data.staking.converters.YieldConverter import com.tangem.data.staking.converters.action.ActionStatusConverter import com.tangem.data.staking.converters.action.EnterActionResponseConverter -import com.tangem.data.staking.converters.action.StakingActionTypeConverter import com.tangem.data.staking.converters.transaction.GasEstimateConverter import com.tangem.data.staking.converters.transaction.StakingTransactionConverter import com.tangem.data.staking.converters.transaction.StakingTransactionStatusConverter @@ -32,6 +32,8 @@ import com.tangem.datasource.api.stakekit.models.response.model.action.StakingAc import com.tangem.datasource.api.stakekit.models.response.model.transaction.tron.TronStakeKitTransaction import com.tangem.datasource.local.token.StakingBalanceStore import com.tangem.datasource.local.token.StakingYieldsStore +import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter +import com.tangem.datasource.local.token.converter.TokenConverter import com.tangem.domain.staking.model.StakingApproval import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo @@ -77,36 +79,19 @@ internal class DefaultStakingRepository( moshi: Moshi, ) : StakingRepository { - private val stakingNetworkTypeConverter = StakingNetworkTypeConverter() - private val networkTypeConverter = StakingNetworkTypeConverter() private val transactionStatusConverter = StakingTransactionStatusConverter() private val transactionTypeConverter = StakingTransactionTypeConverter() private val actionStatusConverter = ActionStatusConverter() - private val stakingActionTypeConverter = StakingActionTypeConverter() - private val tokenConverter = TokenConverter( - stakingNetworkTypeConverter = stakingNetworkTypeConverter, - ) - private val yieldConverter = YieldConverter( - tokenConverter = tokenConverter, - ) - private val gasEstimateConverter = GasEstimateConverter( - tokenConverter = tokenConverter, - ) + private val transactionConverter = StakingTransactionConverter( - networkTypeConverter = networkTypeConverter, transactionStatusConverter = transactionStatusConverter, transactionTypeConverter = transactionTypeConverter, - gasEstimateConverter = gasEstimateConverter, ) private val enterActionResponseConverter = EnterActionResponseConverter( actionStatusConverter = actionStatusConverter, - stakingActionTypeConverter = stakingActionTypeConverter, transactionConverter = transactionConverter, ) - private val yieldBalanceConverter = YieldBalanceConverter() - private val yieldBalanceListConverter = YieldBalanceListConverter(yieldBalanceConverter) - private val tronStakeKitTransactionAdapter by lazy { moshi.adapter(TronStakeKitTransaction::class.java) } private val networkTypeAdapter by lazy { moshi.adapter(NetworkTypeDTO::class.java) } private val stakingActionStatusAdapter by lazy { moshi.adapter(StakingActionStatusDTO::class.java) } @@ -157,7 +142,7 @@ internal class DefaultStakingRepository( return withContext(dispatchers.io) { val address = walletManagersFacade.getDefaultAddress(userWalletId, cryptoCurrency.network).orEmpty() - val networkTypeDto = networkTypeConverter.convertBack(networkType) + val networkTypeDto = StakingNetworkTypeConverter.convertBack(networkType) val networkTypeString = networkTypeDto.extractJsonName() val actionStatusDTO = actionStatusConverter.convertBack(stakingActionStatus) @@ -301,7 +286,7 @@ internal class DefaultStakingRepository( ) } - gasEstimateConverter.convert(gasEstimateDTO.getOrThrow()) + GasEstimateConverter.convert(gasEstimateDTO.getOrThrow()) } } @@ -382,7 +367,7 @@ internal class DefaultStakingRepository( .distinctUntilChanged() .collectLatest { if (it != null) { - send(yieldBalanceConverter.convert(it)) + send(it) } else { error("No yield balance available for currency ${cryptoCurrency.id.value}") } @@ -408,10 +393,8 @@ internal class DefaultStakingRepository( val integrationId = integrationIdMap[getIntegrationKey(cryptoCurrency.id)] ?: error("Could not get integrationId") - val result = stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId) - ?: return@withContext YieldBalance.Error - - yieldBalanceConverter.convert(result) + stakingBalanceStore.getSyncOrNull(userWalletId, address, integrationId) + ?: YieldBalance.Error(integrationId, address) } override suspend fun fetchMultiYieldBalance( @@ -434,7 +417,7 @@ internal class DefaultStakingRepository( return@invokeOnExpire } - val yields = yieldConverter.convertListIgnoreErrors( + val yields = YieldConverter.convertListIgnoreErrors( input = yieldDTOs, onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, ) @@ -485,7 +468,7 @@ internal class DefaultStakingRepository( cryptoCurrencies: List, ): Flow { return stakingBalanceStore.get(userWalletId) - .map(yieldBalanceListConverter::convert) + .map(YieldBalanceListConverter::convert) .flowOn(dispatchers.io) } @@ -495,7 +478,7 @@ internal class DefaultStakingRepository( ): Flow = channelFlow { stakingBalanceStore.get(userWalletId) .onEach { - val balances = yieldBalanceListConverter.convert(it) + val balances = YieldBalanceListConverter.convert(it) send(balances) } .launchIn(scope = this + dispatchers.io) @@ -510,15 +493,19 @@ internal class DefaultStakingRepository( cryptoCurrencies: List, ): YieldBalanceList = withContext(dispatchers.io) { fetchMultiYieldBalance(userWalletId, cryptoCurrencies) - val result = stakingBalanceStore.getSyncOrNull(userWalletId) ?: return@withContext YieldBalanceList.Error - yieldBalanceListConverter.convert(result) + + stakingBalanceStore.getSyncOrNull(userWalletId)?.let(YieldBalanceListConverter::convert) + ?: YieldBalanceList.Error } override suspend fun isAnyTokenStaked(userWalletId: UserWalletId): Boolean { return withContext(dispatchers.io) { stakingBalanceStore.getSyncOrNull(userWalletId) ?.let { - it.isNotEmpty() && it.any { yieldBalance -> yieldBalance.balances.isNotEmpty() } + it.isNotEmpty() && + it.any { yieldBalance -> + (yieldBalance as? YieldBalance.Data)?.balance?.items?.isNotEmpty() == true + } } ?: false } @@ -537,7 +524,7 @@ internal class DefaultStakingRepository( ), args = ActionRequestBodyArgs( amount = params.amount.toPlainString(), - inputToken = tokenConverter.convertBack(params.token), + inputToken = TokenConverter.convertBack(params.token), validatorAddress = params.validatorAddress, validatorAddresses = listOf(params.validatorAddress), // check on other networks tronResource = getTronResource(network), @@ -609,7 +596,7 @@ internal class DefaultStakingRepository( } private suspend fun getEnabledYieldsSync(): List { - return yieldConverter.convertListIgnoreErrors( + return YieldConverter.convertListIgnoreErrors( input = stakingYieldsStore.getSync(), onError = { Timber.e("Error converting one of the items in enabled yields: $it") }, ) diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt index 0f3d08a7c1..63bc4d1655 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldBalanceListConverter.kt @@ -1,20 +1,16 @@ package com.tangem.data.staking.converters -import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO +import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.staking.model.stakekit.YieldBalanceList import com.tangem.utils.converter.Converter -internal class YieldBalanceListConverter( - private val yieldBalanceConverter: YieldBalanceConverter, -) : Converter, YieldBalanceList> { +internal object YieldBalanceListConverter : Converter, YieldBalanceList> { - override fun convert(value: Set): YieldBalanceList { + override fun convert(value: Set): YieldBalanceList { return if (value.isEmpty()) { YieldBalanceList.Empty } else { - YieldBalanceList.Data( - balances = value.map(yieldBalanceConverter::convert), - ) + YieldBalanceList.Data(balances = value.toList()) } } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt index cc61ab8eae..9d8c056e83 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/YieldConverter.kt @@ -4,6 +4,7 @@ import com.tangem.datasource.api.stakekit.models.response.model.AddressArgumentD import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.MetadataDTO.RewardScheduleDTO import com.tangem.datasource.api.stakekit.models.response.model.YieldDTO.ValidatorDTO.ValidatorStatusDTO +import com.tangem.datasource.local.token.converter.TokenConverter import com.tangem.domain.staking.model.stakekit.AddressArgument import com.tangem.domain.staking.model.stakekit.Yield import com.tangem.domain.staking.model.stakekit.Yield.Metadata.RewardSchedule @@ -11,15 +12,20 @@ import com.tangem.domain.staking.model.stakekit.Yield.Validator.ValidatorStatus import com.tangem.utils.converter.Converter import kotlinx.collections.immutable.toImmutableList -class YieldConverter( - private val tokenConverter: TokenConverter, -) : Converter { +internal object YieldConverter : Converter { + + private val PARTNERS = listOf( + "cosmosvaloper1wrx0x9m9ykdhw9sg04v7uljme53wuj03aa5d4f", + "H2tJNyMHnRF6ahCQLQ1sSycM4FGchymuzyYzUqKEuydk", + ) + + private val PARTNERS_NAMES = listOf("Meria") override fun convert(value: YieldDTO): Yield { return Yield( id = value.id.asMandatory("id"), - token = tokenConverter.convert(value.token.asMandatory("token")), - tokens = value.tokens.asMandatory("tokens").map { tokenConverter.convert(it) }, + token = TokenConverter.convert(value.token.asMandatory("token")), + tokens = value.tokens.asMandatory("tokens").map(TokenConverter::convert), args = convertArgs(value.args.asMandatory("args")), status = convertStatus(value.status.asMandatory("status")), apy = value.apy.asMandatory("apy"), @@ -85,9 +91,9 @@ class YieldConverter( logoUri = metadataDTO.logoUri.asMandatory("logoUri"), description = metadataDTO.description.asMandatory("description"), documentation = metadataDTO.documentation, - gasFeeToken = tokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")), - token = tokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")), - tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map { tokenConverter.convert(it) }, + gasFeeToken = TokenConverter.convert(metadataDTO.gasFeeTokenDTO.asMandatory("gasFeeTokenDTO")), + token = TokenConverter.convert(metadataDTO.tokenDTO.asMandatory("tokenDTO")), + tokens = metadataDTO.tokensDTO.asMandatory("tokensDTO").map(TokenConverter::convert), type = metadataDTO.type.asMandatory("type"), rewardSchedule = convertRewardSchedule(metadataDTO.rewardSchedule.asMandatory("rewardSchedule")), cooldownPeriod = metadataDTO.cooldownPeriod?.let { convertPeriod(it) }, @@ -183,14 +189,4 @@ class YieldConverter( private fun isStrategicPartner(validatorAddress: String?, validatorName: String): Boolean { return PARTNERS.any { it == validatorAddress } || PARTNERS_NAMES.any { it.equals(validatorName, true) } } - - private companion object { - val PARTNERS = listOf( - "cosmosvaloper1wrx0x9m9ykdhw9sg04v7uljme53wuj03aa5d4f", - "H2tJNyMHnRF6ahCQLQ1sSycM4FGchymuzyYzUqKEuydk", - ) - val PARTNERS_NAMES = listOf( - "Meria", - ) - } } \ No newline at end of file diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt index b46fd75f85..162b984c15 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/action/EnterActionResponseConverter.kt @@ -2,12 +2,12 @@ package com.tangem.data.staking.converters.action import com.tangem.data.staking.converters.transaction.StakingTransactionConverter import com.tangem.datasource.api.stakekit.models.response.ActionDTO +import com.tangem.datasource.local.token.converter.StakingActionTypeConverter import com.tangem.domain.staking.model.stakekit.action.StakingAction import com.tangem.utils.converter.Converter class EnterActionResponseConverter( private val actionStatusConverter: ActionStatusConverter, - private val stakingActionTypeConverter: StakingActionTypeConverter, private val transactionConverter: StakingTransactionConverter, ) : Converter { @@ -16,7 +16,7 @@ class EnterActionResponseConverter( id = value.id, integrationId = value.integrationId, status = actionStatusConverter.convert(value.status), - type = stakingActionTypeConverter.convert(value.type), + type = StakingActionTypeConverter.convert(value.type), currentStepIndex = value.currentStepIndex, amount = value.amount, validatorAddress = value.validatorAddress, diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt index 6220823faf..a9fb76fc0a 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/GasEstimateConverter.kt @@ -1,18 +1,16 @@ package com.tangem.data.staking.converters.transaction -import com.tangem.data.staking.converters.TokenConverter import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingGasEstimateDTO +import com.tangem.datasource.local.token.converter.TokenConverter import com.tangem.domain.staking.model.stakekit.transaction.StakingGasEstimate import com.tangem.utils.converter.Converter -class GasEstimateConverter( - private val tokenConverter: TokenConverter, -) : Converter { +internal object GasEstimateConverter : Converter { override fun convert(value: StakingGasEstimateDTO): StakingGasEstimate { return StakingGasEstimate( amount = value.amount, - token = tokenConverter.convert(value.token), + token = TokenConverter.convert(value.token), gasLimit = value.gasLimit, ) } diff --git a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt index ef86c8ef8b..bbb6b0c175 100644 --- a/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt +++ b/data/staking/src/main/java/com/tangem/data/staking/converters/transaction/StakingTransactionConverter.kt @@ -1,21 +1,19 @@ package com.tangem.data.staking.converters.transaction -import com.tangem.data.staking.converters.StakingNetworkTypeConverter import com.tangem.datasource.api.stakekit.models.response.model.transaction.StakingTransactionDTO +import com.tangem.datasource.local.token.converter.StakingNetworkTypeConverter import com.tangem.domain.staking.model.stakekit.transaction.StakingTransaction import com.tangem.utils.converter.Converter class StakingTransactionConverter( - private val networkTypeConverter: StakingNetworkTypeConverter, private val transactionStatusConverter: StakingTransactionStatusConverter, private val transactionTypeConverter: StakingTransactionTypeConverter, - private val gasEstimateConverter: GasEstimateConverter, ) : Converter { override fun convert(value: StakingTransactionDTO): StakingTransaction { return StakingTransaction( id = value.id, - network = networkTypeConverter.convert(value.network), + network = StakingNetworkTypeConverter.convert(value.network), status = transactionStatusConverter.convert(value.status), type = transactionTypeConverter.convert(value.type), hash = value.hash, @@ -23,7 +21,7 @@ class StakingTransactionConverter( unsignedTransaction = value.unsignedTransaction, stepIndex = value.stepIndex, error = value.error, - gasEstimate = value.gasEstimate?.let { gasEstimateConverter.convert(it) }, + gasEstimate = value.gasEstimate?.let(GasEstimateConverter::convert), stakeId = value.stakeId, explorerUrl = value.explorerUrl, ledgerHwAppId = value.ledgerHwAppId, 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 b5aa2cd7fc..d6d0caaf7d 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 @@ -22,6 +22,8 @@ import com.tangem.datasource.local.preferences.utils.getObject import com.tangem.datasource.local.preferences.utils.getObjectSyncOrNull import com.tangem.datasource.local.preferences.utils.storeObject import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.common.extensions.canHandleBlockchain +import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.error.DataError import com.tangem.domain.demo.DemoConfig import com.tangem.domain.tokens.model.CryptoCurrency @@ -33,6 +35,7 @@ import com.tangem.domain.walletmanager.WalletManagersFacade import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.models.UserWalletId import com.tangem.utils.coroutines.CoroutineDispatcherProvider +import com.tangem.utils.extensions.filterIf import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import timber.log.Timber @@ -47,7 +50,7 @@ internal class DefaultCurrenciesRepository( private val appPreferencesStore: AppPreferencesStore, private val expressServiceLoader: ExpressServiceLoader, private val dispatchers: CoroutineDispatcherProvider, - excludedBlockchains: ExcludedBlockchains, + private val excludedBlockchains: ExcludedBlockchains, ) : CurrenciesRepository { private val demoConfig = DemoConfig() @@ -486,12 +489,14 @@ internal class DefaultCurrenciesRepository( @OptIn(ExperimentalCoroutinesApi::class) override fun getAllWalletsCryptoCurrencies( currencyRawId: CryptoCurrency.RawID, + needFilterByAvailable: Boolean, ): Flow>> { return userWalletsStore.userWallets.flatMapLatest { userWallets -> userWallets.forEach { fetchTokensIfCacheExpired(userWallet = it, refresh = false) } val userWalletsWithCurrencies = userWallets .filterNot(UserWallet::isLocked) + .filterIf(needFilterByAvailable) { it.filterWalletByAvailableBlockchain(currencyRawId) } .map { userWallet -> if (userWallet.isMultiCurrency) { getSavedUserTokensResponse(userWallet.walletId).map { storedTokens -> @@ -525,6 +530,15 @@ internal class DefaultCurrenciesRepository( } } + private fun UserWallet.filterWalletByAvailableBlockchain(currencyRawId: CryptoCurrency.RawID): Boolean { + val blockchain = Blockchain.fromNetworkId(currencyRawId.value) ?: return true + return this.scanResponse.card.canHandleBlockchain( + blockchain = blockchain, + cardTypesResolver = this.cardTypesResolver, + excludedBlockchains = excludedBlockchains, + ) + } + override fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean { val blockchain = Blockchain.fromNetworkId(network.backendId) return blockchain?.isNetworkFeeZero() ?: false diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt index 7d219967a7..0760786a7d 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/repository/DefaultNetworksRepository.kt @@ -120,8 +120,17 @@ internal class DefaultNetworksRepository( refresh: Boolean, ) = coroutineScope { if (refresh) { - val statusesToRefresh = networks.map { NetworkStatus(it, NetworkStatus.Refreshing) } - networksStatusesStore.storeAll(userWalletId, statusesToRefresh) + val statusesToRefresh = networksStatusesStore.getSyncOrNull(userWalletId)?.mapNotNull { + if (it.network in networks) { + it.copy(value = NetworkStatus.Refreshing) + } else { + null + } + } + + if (statusesToRefresh != null) { + networksStatusesStore.storeAll(key = userWalletId, values = statusesToRefresh.toSet()) + } } val currencies = getCurrencies(userWalletId, networks) 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 c9b3f2ce64..8c7ce6b8bf 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 @@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext +import timber.log.Timber internal class DefaultQuotesRepository( private val tangemTechApi: TangemTechApi, @@ -115,24 +116,26 @@ internal class DefaultQuotesRepository( val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies( rawCurrenciesIds.map { it.value }.toSet(), ) - val response = safeApiCallWithTimeout( + + safeApiCallWithTimeout( call = { val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",") - tangemTechApi.getQuotes(appCurrencyId, coinIds).bind() + val response = tangemTechApi.getQuotes(appCurrencyId, coinIds).bind() + + val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( + response = response, + filteredIds = replacementIdsResult.idsFiltered, + ) + + quotesStore.store(updatedResponse) }, onError = { error -> - cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) }) + Timber.e(error) - throw error + cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) }) + quotesStore.storeEmptyQuotes(currenciesIds = rawCurrenciesIds) }, ) - - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( - response, - replacementIdsResult.idsFiltered, - ) - - quotesStore.store(updatedResponse) } private suspend fun filterExpiredCurrenciesIds( diff --git a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt index 0aa2528c5c..3ccfb526a8 100644 --- a/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt +++ b/data/tokens/src/main/kotlin/com/tangem/data/tokens/utils/NetworkStatusFactory.kt @@ -1,5 +1,6 @@ package com.tangem.data.tokens.utils +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.* import com.tangem.domain.txhistory.models.TxHistoryItem import com.tangem.domain.walletmanager.model.Address @@ -26,6 +27,7 @@ internal class NetworkStatusFactory { address = getNetworkAddress(result.selectedAddress, result.addresses), amountToCreateAccount = result.amountToCreateAccount, errorMessage = result.errorMessage, + source = StatusSource.ACTUAL, ) is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( address = getNetworkAddress(result.selectedAddress, result.addresses), @@ -34,6 +36,7 @@ internal class NetworkStatusFactory { transactions = result.currentTransactions, currencies = currencies, ), + source = StatusSource.ACTUAL, ) }, ) diff --git a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt index c6e3354caa..0d0c0f38ee 100644 --- a/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt +++ b/data/wallets/src/main/java/com/tangem/data/wallets/DefaultWalletsRepository.kt @@ -9,12 +9,15 @@ import com.tangem.datasource.api.tangemTech.models.SeedPhraseNotificationDTO.Sta import com.tangem.datasource.local.datastore.RuntimeStateStore import com.tangem.datasource.local.preferences.AppPreferencesStore import com.tangem.datasource.local.preferences.PreferencesKeys +import com.tangem.datasource.local.preferences.PreferencesKeys.SEED_FIRST_NOTIFICATION_SHOW_TIME import com.tangem.datasource.local.preferences.utils.get import com.tangem.datasource.local.preferences.utils.getSyncOrDefault import com.tangem.datasource.local.preferences.utils.store import com.tangem.datasource.local.userwallet.UserWalletsStore +import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository +import com.tangem.utils.WEEK_MILLIS import com.tangem.utils.coroutines.CoroutineDispatcherProvider import com.tangem.utils.coroutines.runCatching import kotlinx.coroutines.flow.Flow @@ -23,11 +26,13 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +typealias SeedPhraseNotificationsStatuses = Map + internal class DefaultWalletsRepository( private val appPreferencesStore: AppPreferencesStore, private val tangemTechApi: TangemTechApi, private val userWalletsStore: UserWalletsStore, - private val seedPhraseNotificationVisibilityStore: RuntimeStateStore>, + private val seedPhraseNotificationVisibilityStore: RuntimeStateStore, private val dispatchers: CoroutineDispatcherProvider, ) : WalletsRepository { @@ -57,11 +62,16 @@ internal class DefaultWalletsRepository( } } - override fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow { + override fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow { return channelFlow { launch { seedPhraseNotificationVisibilityStore.get() - .map { it.getOrDefault(key = userWalletId, defaultValue = false) } + .map { + it.getOrDefault( + key = userWalletId, + defaultValue = SeedPhraseNotificationsStatus.NOT_NEEDED, + ) + } .collectLatest(::send) } @@ -73,18 +83,53 @@ internal class DefaultWalletsRepository( val userWallet = userWalletsStore.getSyncOrNull(key = userWalletId) val status = if (userWallet?.isImported == false) { - false + Status.NOT_NEEDED } else { runCatching(dispatchers.io) { tangemTechApi.getSeedPhraseNotificationStatus(walletId = userWalletId.stringValue).getOrThrow() - } - .fold( - onSuccess = { it.status == Status.NOTIFIED }, - onFailure = { it is HttpException && it.code == HttpException.Code.NOT_FOUND }, - ) + }.fold( + onSuccess = { it.status }, + onFailure = { + if (it is HttpException && it.code == HttpException.Code.NOT_FOUND) { + Status.NOTIFIED + } else { + Status.NOT_NEEDED + } + }, + ) } - updateNotificationVisibility(id = userWalletId, value = status) + when { + status == Status.NOTIFIED -> updateNotificationVisibility( + id = userWalletId, + value = SeedPhraseNotificationsStatus.SHOW_FIRST, + ) + status == Status.CONFIRMED && checkNeedFetchSecondNotification() -> + fetchSeedPhraseSecondNotificationStatus(userWalletId) + else -> updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) + } + } + + private suspend fun fetchSeedPhraseSecondNotificationStatus(userWalletId: UserWalletId) { + val status = runCatching(dispatchers.io) { + tangemTechApi.getSeedPhraseSecondNotificationStatus(walletId = userWalletId.stringValue).getOrThrow() + }.fold( + onSuccess = { it.status }, + onFailure = { Status.NOT_NEEDED }, + ) + + val showStatus = when (status) { + Status.CONFIRMED -> SeedPhraseNotificationsStatus.SHOW_SECOND + else -> SeedPhraseNotificationsStatus.NOT_NEEDED + } + + updateNotificationVisibility(id = userWalletId, value = showStatus) + } + + private suspend fun checkNeedFetchSecondNotification(): Boolean { + val firstNotificationTime = + appPreferencesStore.getSyncOrDefault(SEED_FIRST_NOTIFICATION_SHOW_TIME, default = 0L) + return System.currentTimeMillis() - firstNotificationTime > WEEK_MILLIS } override suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId) { @@ -102,9 +147,10 @@ internal class DefaultWalletsRepository( walletId = userWalletId.stringValue, body = SeedPhraseNotificationDTO(status = Status.CONFIRMED), ).getOrThrow() + appPreferencesStore.store(key = SEED_FIRST_NOTIFICATION_SHOW_TIME, value = System.currentTimeMillis()) } - updateNotificationVisibility(id = userWalletId, value = false) + updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) } override suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId) { @@ -113,9 +159,10 @@ internal class DefaultWalletsRepository( walletId = userWalletId.stringValue, body = SeedPhraseNotificationDTO(status = Status.DECLINED), ).getOrThrow() + appPreferencesStore.store(key = SEED_FIRST_NOTIFICATION_SHOW_TIME, value = System.currentTimeMillis()) } - updateNotificationVisibility(id = userWalletId, value = false) + updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) } override suspend fun markWallet2WasCreated(userWalletId: UserWalletId) { @@ -126,7 +173,29 @@ internal class DefaultWalletsRepository( } } - private suspend fun updateNotificationVisibility(id: UserWalletId, value: Boolean) { + override suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) { + runCatching(dispatchers.io) { + tangemTechApi.updateSeedPhraseSecondNotificationStatus( + walletId = userWalletId.stringValue, + body = SeedPhraseNotificationDTO(status = Status.REJECTED), + ).getOrThrow() + } + + updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) + } + + override suspend fun acceptSeedPhraseSecondNotification(userWalletId: UserWalletId) { + runCatching(dispatchers.io) { + tangemTechApi.updateSeedPhraseSecondNotificationStatus( + walletId = userWalletId.stringValue, + body = SeedPhraseNotificationDTO(status = Status.ACCEPTED), + ).getOrThrow() + } + + updateNotificationVisibility(id = userWalletId, value = SeedPhraseNotificationsStatus.NOT_NEEDED) + } + + private suspend fun updateNotificationVisibility(id: UserWalletId, value: SeedPhraseNotificationsStatus) { return seedPhraseNotificationVisibilityStore.update { it.toMutableMap().apply { this[id] = value diff --git a/domain/markets/build.gradle.kts b/domain/markets/build.gradle.kts index 430f975fa5..b3d59216c4 100644 --- a/domain/markets/build.gradle.kts +++ b/domain/markets/build.gradle.kts @@ -17,12 +17,21 @@ dependencies { api(projects.domain.core) api(projects.domain.markets.models) api(projects.domain.wallets.models) + api(projects.domain.models) + api(projects.domain.legacy) + api(projects.domain.wallets) implementation(projects.domain.tokens.models) implementation(projects.domain.tokens) api(projects.core.pagination) + /* Libs */ + api(projects.libs.blockchainSdk) + + /* SDK */ + implementation(tangemDeps.blockchain) + /* Utils */ implementation(deps.kotlin.serialization) implementation(projects.core.utils) diff --git a/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt new file mode 100644 index 0000000000..1b645a2d53 --- /dev/null +++ b/domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt @@ -0,0 +1,38 @@ +package com.tangem.domain.markets + +import com.tangem.blockchain.common.Blockchain +import com.tangem.blockchainsdk.utils.ExcludedBlockchains +import com.tangem.blockchainsdk.utils.fromNetworkId +import com.tangem.domain.common.extensions.supportedBlockchains +import com.tangem.domain.common.util.cardTypesResolver +import com.tangem.domain.wallets.legacy.UserWalletsListManager +import com.tangem.domain.wallets.models.UserWalletId + +class FilterAvailableNetworksForWalletUseCase( + private val userWalletsListManager: UserWalletsListManager, + private val excludedBlockchains: ExcludedBlockchains, +) { + + /** + * Filters [networks] list according supported blockchains for this card + * If [userWalletId] not found then returns the same list + */ + operator fun invoke( + userWalletId: UserWalletId, + networks: Set, + ): Set { + val userWallet = userWalletsListManager.userWalletsSync.firstOrNull { + it.walletId == userWalletId + } ?: return networks.toSet() + + val supportedBlockchains = userWallet.scanResponse.card.supportedBlockchains( + cardTypesResolver = userWallet.scanResponse.cardTypesResolver, + excludedBlockchains = excludedBlockchains, + ) + + return networks.filter { + val blockchain = Blockchain.fromNetworkId(it.networkId) + supportedBlockchains.contains(blockchain) + }.toSet() + } +} \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt new file mode 100644 index 0000000000..cba099bbec --- /dev/null +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt @@ -0,0 +1,24 @@ +package com.tangem.domain.models + +/** + * Source of the status of any loaded data + * +[REDACTED_AUTHOR] + */ +enum class StatusSource { + + /** + * Status is loaded from the cache. + * In most cases, it's a temporary value, then the source should become either [ACTUAL] or [ONLY_CACHE] + */ + CACHE, + + /** + * Status is updated by a data source that is of actual value. + * In most cases, this is a remote data source. But it can also be a value that we have updated programmatically. + */ + ACTUAL, + + /** Status is loaded from the cache and can't be updated with actual value */ + ONLY_CACHE, +} \ No newline at end of file diff --git a/domain/staking/models/build.gradle.kts b/domain/staking/models/build.gradle.kts index 68f2c9b6df..ca12daff07 100644 --- a/domain/staking/models/build.gradle.kts +++ b/domain/staking/models/build.gradle.kts @@ -9,6 +9,7 @@ plugins { dependencies { implementation(projects.domain.core) + implementation(projects.domain.models) implementation(deps.kotlin.serialization) implementation(deps.jodatime) diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt index ef9946cdb7..7456b68af6 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalance.kt @@ -1,14 +1,20 @@ package com.tangem.domain.staking.model.stakekit +import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.action.StakingActionType import org.joda.time.DateTime import java.math.BigDecimal sealed class YieldBalance { + abstract val integrationId: String? + abstract val address: String? + data class Data( + override val integrationId: String?, + override val address: String, val balance: YieldBalanceItem, - val address: String, + val source: StatusSource, ) : YieldBalance() { fun getTotalWithRewardsStakingBalance(): BigDecimal { return balance.items.sumOf { it.amount } @@ -34,9 +40,13 @@ sealed class YieldBalance { } } - data object Empty : YieldBalance() + data class Empty( + override val integrationId: String?, + override val address: String, + val source: StatusSource, + ) : YieldBalance() - data object Error : YieldBalance() + data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() } data class YieldBalanceItem( diff --git a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt index c1f4ec5e38..23aebbbdb4 100644 --- a/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt +++ b/domain/staking/models/src/main/kotlin/com/tangem/domain/staking/model/stakekit/YieldBalanceList.kt @@ -2,9 +2,7 @@ package com.tangem.domain.staking.model.stakekit sealed class YieldBalanceList { - data class Data( - val balances: List, - ) : YieldBalanceList() { + data class Data(val balances: List) : YieldBalanceList() { fun getBalance(address: String?, integrationId: String?): YieldBalance { return balances.firstOrNull { yieldBalance -> @@ -15,7 +13,7 @@ sealed class YieldBalanceList { val isCorrectIntegration = integrationId != null && balance?.integrationId == integrationId isCorrectIntegration && isCorrectAddress - } ?: YieldBalance.Error + } ?: YieldBalance.Error(integrationId = integrationId, address = address) } } diff --git a/domain/tokens/models/build.gradle.kts b/domain/tokens/models/build.gradle.kts index 92950e7beb..9a926341a8 100644 --- a/domain/tokens/models/build.gradle.kts +++ b/domain/tokens/models/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { implementation(projects.core.analytics.models) /** Project - Domain */ + implementation(projects.domain.models) implementation(projects.domain.txhistory.models) implementation(projects.domain.staking.models) diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt index d6db9ae7e0..d2df3fd74b 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/CryptoCurrencyStatus.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal @@ -83,6 +84,7 @@ data class CryptoCurrencyStatus( * Represents a state where there is no account associated with the cryptocurrency * * @property amountToCreateAccount base reserve amount for account creation + * @property source source of data */ data class NoAccount( val amountToCreateAccount: BigDecimal, @@ -90,6 +92,7 @@ data class CryptoCurrencyStatus( override val priceChange: BigDecimal?, override val fiatRate: BigDecimal?, override val networkAddress: NetworkAddress, + val source: StatusSource, ) : Value(isError = false) { override val amount: BigDecimal = BigDecimal.ZERO @@ -105,6 +108,7 @@ data class CryptoCurrencyStatus( * @property hasCurrentNetworkTransactions Indicates if there are any transactions in progress related to the * cryptocurrency network. * @property pendingTransactions The current cryptocurrency transactions. + * @property source source of data */ data class Loaded( override val amount: BigDecimal, @@ -115,6 +119,7 @@ data class CryptoCurrencyStatus( override val hasCurrentNetworkTransactions: Boolean, override val pendingTransactions: Set, override val networkAddress: NetworkAddress, + val source: StatusSource, ) : Value(isError = false) /** diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt index d33b011230..1f9bd66e11 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/NetworkStatus.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.models.StatusSource import com.tangem.domain.txhistory.models.TxHistoryItem import java.math.BigDecimal @@ -19,24 +20,33 @@ data class NetworkStatus( * * This sealed class includes different states like unreachable, missed derivation, verified, and no account. */ - sealed class Value + sealed class Value { + + abstract val source: StatusSource + } /** * Represents the state where the network is refreshing. */ - data object Refreshing : Value() + data object Refreshing : Value() { + override val source: StatusSource = StatusSource.ACTUAL + } /** * Represents the state where the network is unreachable. * * @property address Network addresses. */ - data class Unreachable(val address: NetworkAddress?) : Value() + data class Unreachable(val address: NetworkAddress?) : Value() { + override val source: StatusSource = StatusSource.ACTUAL + } /** * Represents the state where a derivation has been missed. */ - data object MissedDerivation : Value() + data object MissedDerivation : Value() { + override val source: StatusSource = StatusSource.ACTUAL + } /** * Represents the verified state of the network, including the amounts associated with different cryptocurrencies @@ -45,12 +55,14 @@ data class NetworkStatus( * @property address Network addresses. * @property amounts A map containing the amounts associated with different cryptocurrencies within the network. * @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies + * @property source source of data * within the network. */ data class Verified( val address: NetworkAddress, val amounts: Map, val pendingTransactions: Map>, + override val source: StatusSource, ) : Value() /** @@ -59,10 +71,12 @@ data class NetworkStatus( * @property address Network addresses. * @property amountToCreateAccount The amount required to create an account within the network. * @property errorMessage error message + * @property source source of data */ data class NoAccount( val address: NetworkAddress, val amountToCreateAccount: BigDecimal, val errorMessage: String, + override val source: StatusSource, ) : Value() } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt index d4d7fb06c7..ebfb2e7525 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/Quote.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.models.StatusSource import java.math.BigDecimal sealed interface Quote { @@ -19,12 +20,12 @@ sealed interface Quote { * @property rawCurrencyId The unique identifier of the cryptocurrency for which the financial information is provided. * @property fiatRate The current fiat exchange rate for the cryptocurrency. * @property priceChange The price change for the cryptocurrency. - * @property isCached flag that determines whether the quote is a cache + * @property source source of data */ data class Value( override val rawCurrencyId: CryptoCurrency.RawID, val fiatRate: BigDecimal, val priceChange: BigDecimal, - val isCached: Boolean, + val source: StatusSource, ) : Quote } \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/TotalFiatBalance.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/TotalFiatBalance.kt index 98deb39ace..5cb26044a2 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/TotalFiatBalance.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/model/TotalFiatBalance.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.models.StatusSource import java.math.BigDecimal /** @@ -27,5 +28,6 @@ sealed class TotalFiatBalance { data class Loaded( val amount: BigDecimal, val isAllAmountsSummarized: Boolean, + val source: StatusSource, ) : TotalFiatBalance() } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt index 0dfb01856e..5a77f4953e 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetAllWalletsCryptoCurrencyStatusesUseCase.kt @@ -42,8 +42,9 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase( @OptIn(ExperimentalCoroutinesApi::class) operator fun invoke( currencyRawId: CryptoCurrency.RawID, + needFilterByAvailable: Boolean = false, ): Flow>>> { - return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId) + return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId, needFilterByAvailable) .flatMapLatest { userWalletsWithCurrencies: Map> -> val walletStatusFlows = userWalletsWithCurrencies.map { (userWallet, cryptoCurrencies) -> val operations = CurrenciesStatusesOperations( @@ -64,6 +65,7 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase( } combine(walletStatusFlows) { it.toMap() } + .onEmpty { emit(emptyMap()) } } .flowOn(dispatchers.io) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt index 2bd9431825..3814c64c01 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/model/TokenList.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.model +import com.tangem.domain.models.StatusSource import java.math.BigDecimal /** @@ -47,6 +48,7 @@ sealed class TokenList { override val totalFiatBalance: TotalFiatBalance = TotalFiatBalance.Loaded( amount = BigDecimal.ZERO, isAllAmountsSummarized = true, + source = StatusSource.ACTUAL, ) } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt index 2c73402bff..7e2dc0db71 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/CurrencyStatusOperations.kt @@ -1,5 +1,6 @@ package com.tangem.domain.tokens.operations +import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.* import java.math.BigDecimal @@ -49,17 +50,27 @@ internal class CurrencyStatusOperations( ) } - private fun createNoAccountStatus(status: NetworkStatus.NoAccount): CryptoCurrencyStatus.NoAccount = - CryptoCurrencyStatus.NoAccount( + private fun createNoAccountStatus(status: NetworkStatus.NoAccount): CryptoCurrencyStatus.NoAccount { + return CryptoCurrencyStatus.NoAccount( amountToCreateAccount = status.amountToCreateAccount, fiatAmount = if (quote == null) null else BigDecimal.ZERO, priceChange = quote?.priceChange, fiatRate = quote?.fiatRate, networkAddress = status.address, + source = getResultStatusSource( + sources = listOf( + status.source, + (quote as? Quote.Value)?.source ?: StatusSource.ACTUAL, + ), + ), ) + } - private fun createStatus(status: NetworkStatus.Verified, yieldBalance: YieldBalance?): CryptoCurrencyStatus.Value { - val amount = when (val amount = status.amounts[currency.id]) { + private fun createStatus( + networkStatusValue: NetworkStatus.Verified, + yieldBalance: YieldBalance?, + ): CryptoCurrencyStatus.Value { + val amount = when (val amount = networkStatusValue.amounts[currency.id]) { null -> { return CryptoCurrencyStatus.Loading } @@ -69,10 +80,10 @@ internal class CurrencyStatusOperations( is CryptoCurrencyAmountStatus.Loaded -> amount.value } - val hasCurrentNetworkTransactions = status.pendingTransactions.isNotEmpty() - val currentTransactions = status.pendingTransactions.getOrElse(currency.id, ::emptySet) + val hasCurrentNetworkTransactions = networkStatusValue.pendingTransactions.isNotEmpty() + val currentTransactions = networkStatusValue.pendingTransactions.getOrElse(currency.id, ::emptySet) val yieldBalanceData = yieldBalance as? YieldBalance.Data - val isCurrentAddressStaking = yieldBalanceData?.address == status.address.defaultAddress.value + val isCurrentAddressStaking = yieldBalanceData?.address == networkStatusValue.address.defaultAddress.value val filteredTokenBalances = yieldBalanceData?.balance?.items?.filter { it.token.coinGeckoId == currency.id.rawCurrencyId?.value } @@ -94,14 +105,14 @@ internal class CurrencyStatusOperations( priceChange = quote?.priceChange, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, - networkAddress = status.address, + networkAddress = networkStatusValue.address, yieldBalance = currentYieldBalance, ) quote is Quote.Empty || ignoreQuote -> CryptoCurrencyStatus.NoQuote( amount = amount, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, - networkAddress = status.address, + networkAddress = networkStatusValue.address, yieldBalance = currentYieldBalance, ) quote is Quote.Value -> CryptoCurrencyStatus.Loaded( @@ -111,8 +122,15 @@ internal class CurrencyStatusOperations( priceChange = quote.priceChange, hasCurrentNetworkTransactions = hasCurrentNetworkTransactions, pendingTransactions = currentTransactions, - networkAddress = status.address, + networkAddress = networkStatusValue.address, yieldBalance = currentYieldBalance, + source = getResultStatusSource( + sources = listOf( + networkStatusValue.source, + currentYieldBalance?.source ?: StatusSource.ACTUAL, + quote.source, + ), + ), ) else -> CryptoCurrencyStatus.Loading } @@ -127,4 +145,18 @@ internal class CurrencyStatusOperations( private fun calculateFiatAmount(amount: BigDecimal, fiatRate: BigDecimal): BigDecimal { return amount * fiatRate } + + /* + * ACTUAL, ACTUAL, ACTUAL -> ACTUAL + * ACTUAL, ACTUAL, CACHE -> CACHE + * ACTUAL, ACTUAL, ONLY_CACHE -> ONLY_CACHE + * ACTUAL, CACHE, ONLY_CACHE -> ONLY_CACHE + */ + private fun getResultStatusSource(sources: List): StatusSource { + return when { + sources.any { it == StatusSource.ONLY_CACHE } -> StatusSource.ONLY_CACHE + sources.any { it == StatusSource.CACHE } -> StatusSource.CACHE + else -> StatusSource.ACTUAL + } + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt index 41f42b4ccd..d8cea3866c 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/operations/TokenListFiatBalanceOperations.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.operations import arrow.core.NonEmptyList +import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.stakekit.YieldBalance import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TotalFiatBalance @@ -34,14 +35,14 @@ internal class TokenListFiatBalanceOperations( is CryptoCurrencyStatus.NoAmount, -> { if (BlockchainUtils.isIncludeToBalanceOnError(token.currency.network.id.value)) { - fiatBalance = recalculateNoAccountBalance(fiatBalance) + fiatBalance = recalculateNoAccountBalance(status, fiatBalance) } else { fiatBalance = TotalFiatBalance.Failed break } } is CryptoCurrencyStatus.NoAccount -> { - fiatBalance = recalculateNoAccountBalance(fiatBalance) + fiatBalance = recalculateNoAccountBalance(status, fiatBalance) } is CryptoCurrencyStatus.Loaded -> { fiatBalance = recalculateBalance(status, fiatBalance) @@ -55,11 +56,15 @@ internal class TokenListFiatBalanceOperations( return fiatBalance } - private fun recalculateNoAccountBalance(currentBalance: TotalFiatBalance): TotalFiatBalance { + private fun recalculateNoAccountBalance( + status: CryptoCurrencyStatus.Value, + currentBalance: TotalFiatBalance, + ): TotalFiatBalance { return (currentBalance as? TotalFiatBalance.Loaded)?.copy(isAllAmountsSummarized = false) ?: TotalFiatBalance.Loaded( amount = BigDecimal.ZERO, isAllAmountsSummarized = false, + source = (status as? CryptoCurrencyStatus.NoAccount)?.source ?: StatusSource.ACTUAL, ) } @@ -77,6 +82,7 @@ internal class TokenListFiatBalanceOperations( ) ?: TotalFiatBalance.Loaded( amount = status.fiatAmount + fiatStakingBalance, isAllAmountsSummarized = true, + source = status.source, ) } } @@ -95,6 +101,7 @@ internal class TokenListFiatBalanceOperations( ) ?: TotalFiatBalance.Loaded( amount = status.fiatAmount.orZero() + fiatYieldBalance, isAllAmountsSummarized = isTokenAmountCanBeSummarized, + source = StatusSource.ACTUAL, ) } } diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt index 04fa86295f..d48d0a63f4 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/repository/CurrenciesRepository.kt @@ -248,7 +248,10 @@ interface CurrenciesRepository { ): CryptoCurrency.Token /** Get crypto currencies by [currencyRawId] from all user wallets */ - fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow>> + fun getAllWalletsCryptoCurrencies( + currencyRawId: CryptoCurrency.RawID, + needFilterByAvailable: Boolean, + ): Flow>> fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 903bc4f2ec..39e4ffa39c 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt @@ -2,13 +2,13 @@ package com.tangem.domain.tokens.mock import arrow.core.NonEmptySet import arrow.core.nonEmptySetOf +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus import com.tangem.domain.tokens.model.Network import com.tangem.domain.tokens.model.NetworkAddress import com.tangem.domain.tokens.model.NetworkStatus import java.math.BigDecimal -@Suppress("MemberVisibilityCanBePrivate") internal object MockNetworks { val amountToCreateAccount: BigDecimal = BigDecimal.TEN @@ -52,17 +52,24 @@ internal object MockNetworks { transactionExtrasType = Network.TransactionExtrasType.NONE, ) - val networkStatus1 = NetworkStatus( + val verifiedNetworksStatuses: NonEmptySet + get() = nonEmptySetOf( + verifiedNetworkStatus1, + verifiedNetworkStatus2, + verifiedNetworkStatus3, + ) + + private val networkStatus1 = NetworkStatus( network = network1, value = NetworkStatus.Unreachable(address = null), ) - val networkStatus2 = NetworkStatus( + private val networkStatus2 = NetworkStatus( network = network2, value = NetworkStatus.MissedDerivation, ) - val networkStatus3 = NetworkStatus( + private val networkStatus3 = NetworkStatus( network = network3, value = NetworkStatus.NoAccount( amountToCreateAccount = amountToCreateAccount, @@ -70,12 +77,11 @@ internal object MockNetworks { defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), errorMessage = "", + source = StatusSource.ACTUAL, ), ) - val errorNetworksStatuses = nonEmptySetOf(networkStatus1, networkStatus2, networkStatus3) - - val verifiedNetworkStatus1: NetworkStatus + private val verifiedNetworkStatus1: NetworkStatus get() = networkStatus1.copy( value = NetworkStatus.Verified( amounts = mapOf( @@ -87,10 +93,11 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + source = StatusSource.ACTUAL, ), ) - val verifiedNetworkStatus2: NetworkStatus + private val verifiedNetworkStatus2: NetworkStatus get() = networkStatus2.copy( value = NetworkStatus.Verified( amounts = mapOf( @@ -102,10 +109,11 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + source = StatusSource.ACTUAL, ), ) - val verifiedNetworkStatus3: NetworkStatus + private val verifiedNetworkStatus3: NetworkStatus get() = networkStatus3.copy( value = NetworkStatus.Verified( amounts = mapOf( @@ -118,13 +126,7 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + source = StatusSource.ACTUAL, ), ) - - val verifiedNetworksStatuses: NonEmptySet - get() = nonEmptySetOf( - verifiedNetworkStatus1, - verifiedNetworkStatus2, - verifiedNetworkStatus3, - ) } \ No newline at end of file diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt index a3b1ee5456..885859db22 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockQuotes.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptySetOf +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.Quote import java.math.BigDecimal @@ -12,70 +13,70 @@ internal object MockQuotes { rawCurrencyId = MockTokens.token1.id.rawCurrencyId!!, fiatRate = BigDecimal("1.23"), priceChange = BigDecimal("0.01"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote2 = Quote.Value( rawCurrencyId = MockTokens.token2.id.rawCurrencyId!!, fiatRate = BigDecimal("2.34"), priceChange = BigDecimal("-0.02"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote3 = Quote.Value( rawCurrencyId = MockTokens.token3.id.rawCurrencyId!!, fiatRate = BigDecimal("3.45"), priceChange = BigDecimal("0.03"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote4 = Quote.Value( rawCurrencyId = MockTokens.token4.id.rawCurrencyId!!, fiatRate = BigDecimal("4.56"), priceChange = BigDecimal("-0.04"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote5 = Quote.Value( rawCurrencyId = MockTokens.token5.id.rawCurrencyId!!, fiatRate = BigDecimal("5.67"), priceChange = BigDecimal("0.05"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote6 = Quote.Value( rawCurrencyId = MockTokens.token6.id.rawCurrencyId!!, fiatRate = BigDecimal("6.78"), priceChange = BigDecimal("-0.06"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote7 = Quote.Value( rawCurrencyId = MockTokens.token7.id.rawCurrencyId!!, fiatRate = BigDecimal("7.89"), priceChange = BigDecimal("0.07"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote8 = Quote.Value( rawCurrencyId = MockTokens.token8.id.rawCurrencyId!!, fiatRate = BigDecimal("8.90"), priceChange = BigDecimal("-0.08"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote9 = Quote.Value( rawCurrencyId = MockTokens.token9.id.rawCurrencyId!!, fiatRate = BigDecimal("9.01"), priceChange = BigDecimal("0.09"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote10 = Quote.Value( rawCurrencyId = MockTokens.token10.id.rawCurrencyId!!, fiatRate = BigDecimal("10.12"), priceChange = BigDecimal("-0.10"), - isCached = false, + source = StatusSource.ACTUAL, ) val quote11 = Quote.Empty(CryptoCurrency.RawID("null")) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt index 3f15752baf..a9a32bc442 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokenLists.kt @@ -2,6 +2,7 @@ package com.tangem.domain.tokens.mock import arrow.core.NonEmptyList import arrow.core.toNonEmptyListOrNull +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.mock.MockNetworksGroups.failedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.loadedNetworksGroups import com.tangem.domain.tokens.mock.MockNetworksGroups.sortedNetworksGroups @@ -79,6 +80,7 @@ internal object MockTokenLists { totalFiatBalance = TotalFiatBalance.Loaded( amount = tokens.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, isAllAmountsSummarized = true, + source = StatusSource.ACTUAL, ), ) } @@ -95,6 +97,7 @@ internal object MockTokenLists { .flatMap { it.currencies as NonEmptyList } .sumOf { it.value.fiatAmount ?: BigDecimal.ZERO }, isAllAmountsSummarized = true, + source = StatusSource.ACTUAL, ), ) } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt index a9d704b56f..c5e2c10c31 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockTokensStates.kt @@ -1,6 +1,7 @@ package com.tangem.domain.tokens.mock import arrow.core.nonEmptyListOf +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.* import java.math.BigDecimal @@ -74,6 +75,7 @@ internal object MockTokensStates { networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + source = StatusSource.ACTUAL, ), ) @@ -87,6 +89,7 @@ internal object MockTokensStates { networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + source = StatusSource.ACTUAL, ), ) @@ -100,6 +103,7 @@ internal object MockTokensStates { networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + source = StatusSource.ACTUAL, ), ) @@ -113,6 +117,7 @@ internal object MockTokensStates { networkAddress = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + source = StatusSource.ACTUAL, ), ) @@ -157,6 +162,7 @@ internal object MockTokensStates { hasCurrentNetworkTransactions = false, networkAddress = requireNotNull(networkStatus.value as? NetworkStatus.Verified).address, yieldBalance = null, + source = StatusSource.ACTUAL, ) } status.copy(value = value) diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt index a94add56b4..233a8a3796 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockCurrenciesRepository.kt @@ -155,6 +155,7 @@ internal class MockCurrenciesRepository( override fun getAllWalletsCryptoCurrencies( currencyRawId: CryptoCurrency.RawID, + needFilterByAvailable: Boolean, ): Flow>> { return emptyFlow() } diff --git a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt index d88dda9050..dbb5002018 100644 --- a/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt +++ b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/repository/MockStakingRepository.kt @@ -138,13 +138,13 @@ class MockStakingRepository : StakingRepository { userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, ): Flow = channelFlow { - send(YieldBalance.Error) + send(YieldBalance.Error(integrationId = null, address = null)) } override suspend fun getSingleYieldBalanceSync( userWalletId: UserWalletId, cryptoCurrency: CryptoCurrency, - ): YieldBalance = YieldBalance.Error + ): YieldBalance = YieldBalance.Error(integrationId = null, address = null) override suspend fun fetchMultiYieldBalance( userWalletId: UserWalletId, @@ -158,7 +158,11 @@ class MockStakingRepository : StakingRepository { userWalletId: UserWalletId, cryptoCurrencies: List, ): Flow { - return flowOf(YieldBalanceList.Data(listOf(YieldBalance.Error))) + return flowOf( + YieldBalanceList.Data( + balances = listOf(YieldBalance.Error(integrationId = null, address = null)), + ), + ) } override fun getMultiYieldBalanceUpdatesLegacy( @@ -170,7 +174,7 @@ class MockStakingRepository : StakingRepository { userWalletId: UserWalletId, cryptoCurrencies: List, ): YieldBalanceList = YieldBalanceList.Data( - balances = listOf(YieldBalance.Error), + balances = listOf(YieldBalance.Error(integrationId = null, address = null)), ) override suspend fun createAction( diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SeedPhraseNotificationsStatus.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SeedPhraseNotificationsStatus.kt new file mode 100644 index 0000000000..2c9180e6a6 --- /dev/null +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/models/SeedPhraseNotificationsStatus.kt @@ -0,0 +1,8 @@ +package com.tangem.domain.wallets.models + +enum class SeedPhraseNotificationsStatus { + + SHOW_FIRST, + SHOW_SECOND, + NOT_NEEDED, +} \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt index 18be29acdd..f226543d60 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/repository/WalletsRepository.kt @@ -1,5 +1,6 @@ package com.tangem.domain.wallets.repository +import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletId import kotlinx.coroutines.flow.Flow @@ -15,7 +16,7 @@ interface WalletsRepository { suspend fun setHasWalletsWithRing(userWalletId: UserWalletId) - fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow + fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow suspend fun notifiedSeedPhraseNotification(userWalletId: UserWalletId) @@ -23,5 +24,9 @@ interface WalletsRepository { suspend fun declineSeedPhraseNotification(userWalletId: UserWalletId) + suspend fun rejectSeedPhraseSecondNotification(userWalletId: UserWalletId) + + suspend fun acceptSeedPhraseSecondNotification(userWalletId: UserWalletId) + suspend fun markWallet2WasCreated(userWalletId: UserWalletId) } \ No newline at end of file diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SeedPhraseNotificationUseCase.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SeedPhraseNotificationUseCase.kt index e5e1217c7c..6488fb915a 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SeedPhraseNotificationUseCase.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/usecase/SeedPhraseNotificationUseCase.kt @@ -1,5 +1,6 @@ package com.tangem.domain.wallets.usecase +import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWalletId import com.tangem.domain.wallets.repository.WalletsRepository import kotlinx.coroutines.flow.Flow @@ -8,7 +9,7 @@ class SeedPhraseNotificationUseCase( private val walletsRepository: WalletsRepository, ) { - operator fun invoke(userWalletId: UserWalletId): Flow { + operator fun invoke(userWalletId: UserWalletId): Flow { return walletsRepository.seedPhraseNotificationStatus(userWalletId) } @@ -23,4 +24,12 @@ class SeedPhraseNotificationUseCase( suspend fun decline(userWalletId: UserWalletId) { walletsRepository.declineSeedPhraseNotification(userWalletId) } + + suspend fun acceptSecond(userWalletId: UserWalletId) { + walletsRepository.acceptSeedPhraseSecondNotification(userWalletId) + } + + suspend fun rejectSecond(userWalletId: UserWalletId) { + walletsRepository.rejectSeedPhraseSecondNotification(userWalletId) + } } \ No newline at end of file diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt index 46ba611519..f36474bbee 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/details/impl/model/formatter/MarketsDateTimeFormatters.kt @@ -8,15 +8,14 @@ import com.tangem.core.ui.utils.DateTimeFormatters import com.tangem.core.ui.utils.formatAsDateTime import com.tangem.domain.markets.PriceChangeInterval import com.tangem.features.markets.impl.R +import com.tangem.utils.H24_MILLIS +import com.tangem.utils.WEEK_MILLIS import org.joda.time.DateTime import org.joda.time.DateTimeZone import java.math.BigDecimal internal object MarketsDateTimeFormatters { - private const val H24_MILLIS = 24L * 60 * 60 * 1000 - private const val WEEK_MILLIS = 7L * H24_MILLIS - private val dateTimeMMMFormatter by lazy { DateTimeFormatters.getBestFormatterBySkeleton("dd MMM Hm") } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt index f4e8c0aad9..d48bdba0c6 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/loader/PortfolioDataLoader.kt @@ -65,7 +65,7 @@ internal class PortfolioDataLoader @Inject constructor( private fun getAllWalletsCryptoCurrenciesData( currencyRawId: CryptoCurrency.RawID, ): Flow>> { - return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId) + return getAllWalletsCryptoCurrencyStatusesUseCase(currencyRawId, true) .distinctUntilChanged() .map { walletsWithMaybeStatuses -> walletsWithMaybeStatuses.mapValues { entry -> @@ -94,20 +94,21 @@ internal class PortfolioDataLoader @Inject constructor( } } } + }.onEmpty { + emit( + walletsWithStatuses.mapValues { (wallet, statuses) -> + statuses.map { + PortfolioData.CryptoCurrencyData( + userWallet = wallet, + status = it, + actions = emptyList(), + ) + } + }, + ) } - .onEmpty { - emit( - walletsWithStatuses.mapValues { (wallet, statuses) -> - statuses.map { - PortfolioData.CryptoCurrencyData( - userWallet = wallet, - status = it, - actions = emptyList(), - ) - } - }, - ) - } + }.onEmpty { + emit(emptyMap()) } .distinctUntilChanged() } diff --git a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt index 68b432f633..b28d495ac2 100644 --- a/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt +++ b/features/markets/impl/src/main/kotlin/com/tangem/features/markets/portfolio/impl/model/MarketsPortfolioModel.kt @@ -18,6 +18,7 @@ import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.card.HasMissedDerivationsUseCase import com.tangem.domain.managetokens.CheckCurrencyUnsupportedUseCase import com.tangem.domain.managetokens.model.CurrencyUnsupportedState +import com.tangem.domain.markets.FilterAvailableNetworksForWalletUseCase import com.tangem.domain.markets.SaveMarketTokensUseCase import com.tangem.domain.markets.TokenMarketInfo import com.tangem.domain.wallets.models.UserWalletId @@ -49,6 +50,7 @@ internal class MarketsPortfolioModel @Inject constructor( private val portfolioDataLoader: PortfolioDataLoader, private val hasMissedDerivationsUseCase: HasMissedDerivationsUseCase, private val saveMarketTokensUseCase: SaveMarketTokensUseCase, + private val filterAvailableNetworks: FilterAvailableNetworksForWalletUseCase, private val addToPortfolioManager: AddToPortfolioManager, private val analyticsEventHandler: AnalyticsEventHandler, ) : Model() { @@ -175,10 +177,16 @@ internal class MarketsPortfolioModel @Inject constructor( flow2 = selectedMultiWalletIdFlow, flow3 = addToPortfolioManager.getAddToPortfolioData(), transform = { portfolioBSVisibilityModel, selectedWalletId, addToPortfolioData -> + val filteredNetworks = selectedWalletId?.let { + filterAvailableNetworks(selectedWalletId, addToPortfolioData.availableNetworks ?: emptySet()) + } ?: emptySet() + PortfolioUIData( portfolioBSVisibilityModel = portfolioBSVisibilityModel, selectedWalletId = selectedWalletId, - addToPortfolioData = addToPortfolioData, + addToPortfolioData = addToPortfolioData.copy( + availableNetworks = filteredNetworks, + ), hasMissedDerivations = hasMissedDerivations(selectedWalletId, addToPortfolioData), ) }, diff --git a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt index 75bb0ebc77..c8b17e62c5 100644 --- a/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt +++ b/features/tokendetails/impl/src/main/kotlin/com/tangem/feature/tokendetails/presentation/tokendetails/state/factory/TokenDetailsLoadedBalanceConverter.kt @@ -12,6 +12,7 @@ import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.extensions.wrappedList import com.tangem.core.ui.format.bigdecimal.* import com.tangem.domain.appcurrency.model.AppCurrency +import com.tangem.domain.models.StatusSource import com.tangem.domain.staking.model.StakingAvailability import com.tangem.domain.staking.model.StakingEntryInfo import com.tangem.domain.staking.model.stakekit.RewardBlockType @@ -121,7 +122,7 @@ internal class TokenDetailsLoadedBalanceConverter( onBalanceSelect = clickIntents::onBalanceSelect, selectedBalanceType = currentState.selectedBalanceType, isBalanceSelectorEnabled = isBalanceSelectorEnabled, - isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA] + isBalanceFlickering = status.value.isFlickering(), ) is CryptoCurrencyStatus.Loading -> TokenDetailsBalanceBlockState.Loading( actionButtons = currentState.actionButtons, @@ -344,4 +345,14 @@ internal class TokenDetailsLoadedBalanceConverter( RewardBlockType.RewardUnavailable -> TextReference.EMPTY } } + + private fun CryptoCurrencyStatus.Value.isFlickering(): Boolean = getStatusSource() == StatusSource.CACHE + + private fun CryptoCurrencyStatus.Value.getStatusSource(): StatusSource? { + return when (this) { + is CryptoCurrencyStatus.Loaded -> source + is CryptoCurrencyStatus.NoAccount -> source + else -> null + } + } } \ No newline at end of file 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 aae1a9292b..c39efabf74 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 @@ -129,8 +129,14 @@ sealed class WalletScreenAnalyticsEvent { data object NoticeSeedPhraseSupport : MainScreen(event = "Notice - Seed Phrase Support") + data object NoticeSeedPhraseSupportSecond : MainScreen(event = "Notice - Seed Phrase Support2") + data object NoticeSeedPhraseSupportButtonNo : MainScreen(event = "Button - Support No") data object NoticeSeedPhraseSupportButtonYes : MainScreen(event = "Button - Support Yes") + + data object NoticeSeedPhraseSupportButtonUsed : MainScreen(event = "Button - Support Used") + + data object NoticeSeedPhraseSupportButtonDeclined : MainScreen(event = "Button - Support Declined") } } \ No newline at end of file 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 6313ea4653..cc8e0c9c5e 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 @@ -51,15 +51,17 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.NoteMigration -> MainScreen.NotePromo is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner( source = AnalyticsParam.ScreensSources.Main, - programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action + programName = ProgramName.Empty, // Use it on new promo action ) is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender] is WalletNotification.Informational.NoAccount, is WalletNotification.Warning.LowSignatures, is WalletNotification.Warning.SomeNetworksUnreachable, is WalletNotification.Warning.NetworksUnreachable, + is WalletNotification.UsedOutdatedData, -> null is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport + is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond } } } \ No newline at end of file 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 d863a3dea9..7756255af2 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 @@ -4,11 +4,13 @@ import com.tangem.domain.common.CardTypesResolver import com.tangem.domain.common.util.cardTypesResolver import com.tangem.domain.core.lce.Lce import com.tangem.domain.demo.IsDemoCardUseCase +import com.tangem.domain.models.StatusSource import com.tangem.domain.settings.IsReadyToShowRateAppUseCase import com.tangem.domain.tokens.error.TokenListError import com.tangem.domain.tokens.model.CryptoCurrency import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.tokens.model.TokenList +import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase @@ -44,6 +46,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( flow4 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId), ) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus -> buildList { + addUsedOutdatedDataNotification(maybeTokenList) + addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents) addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents) @@ -61,23 +65,28 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } } + private fun MutableList.addUsedOutdatedDataNotification( + maybeTokenList: Lce, + ) { + val tokenList = maybeTokenList.getOrNull(isPartialContentAccepted = false)?.flattenCurrencies().orEmpty() + + val hasOnlyCachedData = tokenList.any { + when (val value = it.value) { + is CryptoCurrencyStatus.Loaded -> value.source == StatusSource.ONLY_CACHE + is CryptoCurrencyStatus.NoAccount -> value.source == StatusSource.ONLY_CACHE + else -> false + } + } + + addIf(element = WalletNotification.UsedOutdatedData, condition = hasOnlyCachedData) + } + private fun MutableList.addCriticalNotifications( userWallet: UserWallet, - seedPhraseIssueStatus: Boolean, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, clickIntents: WalletClickIntents, ) { - addIf( - element = WalletNotification.Critical.SeedPhraseNotification( - onDeclineClick = clickIntents::onSeedPhraseNotificationDecline, - onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm, - ), - condition = with(userWallet) { - val isDemo = isDemoCardUseCase(cardId = userWallet.cardId) - val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported - - !isDemo && isWalletWithSeedPhrase && seedPhraseIssueStatus - }, - ) + addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents) val cardTypesResolver = userWallet.scanResponse.cardTypesResolver addIf( @@ -103,6 +112,39 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( } } + private fun MutableList.addSeedNotificationIfNeeded( + userWallet: UserWallet, + seedPhraseIssueStatus: SeedPhraseNotificationsStatus, + clickIntents: WalletClickIntents, + ) { + val isNotificationAvailable = with(userWallet) { + val isDemo = isDemoCardUseCase(cardId = userWallet.cardId) + val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported + + !isDemo && isWalletWithSeedPhrase + } + + when (seedPhraseIssueStatus) { + SeedPhraseNotificationsStatus.SHOW_FIRST -> addIf( + element = WalletNotification.Critical.SeedPhraseNotification( + onDeclineClick = clickIntents::onSeedPhraseNotificationDecline, + onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.SHOW_SECOND -> addIf( + element = WalletNotification.Critical.SeedPhraseSecondNotification( + onDeclineClick = clickIntents::onSeedPhraseSecondNotificationReject, + onConfirmClick = clickIntents::onSeedPhraseSecondNotificationAccept, + ), + condition = isNotificationAvailable, + ) + SeedPhraseNotificationsStatus.NOT_NEEDED -> { + // do nothing + } + } + } + private fun MutableList.addInformationalNotifications( cardTypesResolver: CardTypesResolver, maybeTokenList: Lce, diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt index 0228296bdd..f24b36a03b 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/domain/Wallet2CobrandImage.kt @@ -302,4 +302,10 @@ internal enum class Wallet2CobrandImage( cards3ResId = R.drawable.ill_locked_money_card3_120_106, batchIds = setOf("AF63"), ), + + Ghoad( + cards2ResId = R.drawable.ill_ghoad_card2_120_106, + cards3ResId = R.drawable.ill_ghoad_card3_120_106, + batchIds = setOf("AF89"), + ), } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index dfcfda9298..6cb194fd18 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt @@ -51,7 +51,10 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ) - data class SeedPhraseNotification(val onDeclineClick: () -> Unit, val onConfirmClick: () -> Unit) : Critical( + data class SeedPhraseNotification( + val onDeclineClick: () -> Unit, + val onConfirmClick: () -> Unit, + ) : Critical( title = resourceReference(R.string.warning_seedphrase_issue_title), subtitle = resourceReference(R.string.warning_seedphrase_issue_message), buttonsState = NotificationConfig.ButtonsState.SecondaryPairButtonsConfig( @@ -61,6 +64,20 @@ sealed class WalletNotification(val config: NotificationConfig) { onRightClick = onConfirmClick, ), ) + + data class SeedPhraseSecondNotification( + val onDeclineClick: () -> Unit, + val onConfirmClick: () -> Unit, + ) : Critical( + title = resourceReference(R.string.warning_seedphrase_action_required_title), + subtitle = resourceReference(R.string.warning_seedphrase_contacted_support), + buttonsState = NotificationConfig.ButtonsState.SecondaryPairButtonsConfig( + leftText = resourceReference(R.string.seed_warning_no), + onLeftClick = onDeclineClick, + rightText = resourceReference(R.string.seed_warning_yes), + onRightClick = onConfirmClick, + ), + ) } sealed class Warning( @@ -217,4 +234,11 @@ sealed class WalletNotification(val config: NotificationConfig) { ), ), ) + + data object UsedOutdatedData : WalletNotification( + config = NotificationConfig( + subtitle = resourceReference(R.string.warning_some_token_balances_not_updated), + iconResId = R.drawable.ic_error_sync_24, + ), + ) } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt index 7eed57588c..885e370725 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletState.kt @@ -3,9 +3,9 @@ package com.tangem.feature.wallet.presentation.wallet.state.model import androidx.compose.runtime.Immutable import com.tangem.common.ui.expressStatus.state.ExpressTransactionStateUM import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig +import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.core.ui.components.marketprice.MarketPriceBlockState import com.tangem.core.ui.components.transactions.state.TxHistoryState -import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedTxHistoryStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.LockedWalletStateHolder import com.tangem.feature.wallet.presentation.wallet.state.model.holder.TxHistoryStateHolder diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index a4d821f725..cd0675560d 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt @@ -4,8 +4,8 @@ import arrow.core.Either import arrow.core.getOrElse import com.tangem.core.ui.extensions.stringReference import com.tangem.core.ui.format.bigdecimal.crypto +import com.tangem.core.ui.format.bigdecimal.fiat import com.tangem.core.ui.format.bigdecimal.format -import com.tangem.core.ui.utils.BigDecimalFormatter import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.visa.model.VisaCurrency import com.tangem.domain.wallets.models.UserWallet @@ -76,17 +76,20 @@ internal class SetBalancesAndLimitsTransformer( }, cardCount = userWallet.getCardsCount(), isZeroBalance = visaCurrency.balances.available.isZero(), - isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA] + isBalanceFlickering = false, ) } } private fun createAdditionalInfo(visaCurrency: VisaCurrency): WalletAdditionalInfo { - val fiatAmount = BigDecimalFormatter.formatFiatAmount( - fiatAmount = visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) }, - fiatCurrencyCode = visaCurrency.fiatCurrency.code, - fiatCurrencySymbol = visaCurrency.fiatCurrency.symbol, - ) + val fiatAmount = visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) } + .format { + fiat( + fiatCurrencyCode = visaCurrency.fiatCurrency.code, + fiatCurrencySymbol = visaCurrency.fiatCurrency.symbol, + ) + } + val infoContent = stringReference( value = buildString { append(fiatAmount) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt index a3522cddca..009caf2327 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetTokenListErrorTransformer.kt @@ -1,6 +1,7 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.getCardsCount import com.tangem.domain.tokens.error.TokenListError @@ -61,14 +62,12 @@ internal class SetTokenListErrorTransformer( additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet), imageResId = imageResId, dropDownItems = dropDownItems, - balance = BigDecimalFormatter.formatFiatAmount( - fiatAmount = BigDecimal.ZERO, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + balance = BigDecimal.ZERO.format { + fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) + }, cardCount = selectedWallet.getCardsCount(), isZeroBalance = true, - isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA] + isBalanceFlickering = false, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt index 65a68880bb..252f77384f 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/MultiWalletCardStateConverter.kt @@ -1,8 +1,10 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.getCardsCount +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.TotalFiatBalance import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory @@ -51,14 +53,12 @@ internal class MultiWalletCardStateConverter( additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet), imageResId = imageResId, dropDownItems = dropDownItems, - balance = BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatBalance.amount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ), + balance = fiatBalance.amount.format { + fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) + }, isZeroBalance = fiatBalance.amount.isZero(), cardCount = selectedWallet.getCardsCount(), - isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA] + isBalanceFlickering = fiatBalance.source == StatusSource.CACHE, ) } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt index f763c222f8..be612bad8e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/converter/SingleWalletCardStateConverter.kt @@ -1,12 +1,15 @@ package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter -import com.tangem.core.ui.utils.BigDecimalFormatter +import com.tangem.core.ui.format.bigdecimal.fiat +import com.tangem.core.ui.format.bigdecimal.format import com.tangem.domain.appcurrency.model.AppCurrency import com.tangem.domain.common.util.getCardsCount +import com.tangem.domain.models.StatusSource import com.tangem.domain.tokens.model.CryptoCurrencyStatus import com.tangem.domain.wallets.models.UserWallet import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState +import com.tangem.utils.StringsSigns.DASH_SIGN import com.tangem.utils.converter.Converter import com.tangem.utils.extensions.isZero @@ -62,17 +65,15 @@ internal class SingleWalletCardStateConverter( balance = formatFiatAmount(status = status, appCurrency = appCurrency), cardCount = selectedWallet.getCardsCount(), isZeroBalance = status.fiatAmount?.isZero(), - isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA] + isBalanceFlickering = (status as? CryptoCurrencyStatus.Loaded)?.source == StatusSource.CACHE, ) } private fun formatFiatAmount(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String { - val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN + val fiatAmount = status.fiatAmount ?: return DASH_SIGN - return BigDecimalFormatter.formatFiatAmount( - fiatAmount = fiatAmount, - fiatCurrencyCode = appCurrency.code, - fiatCurrencySymbol = appCurrency.symbol, - ) + return fiatAmount.format { + fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol) + } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index dd24826ded..6c62f4d63e 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt @@ -266,21 +266,21 @@ private fun TitleText(text: String, modifier: Modifier = Modifier) { @Composable private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) { AnimatedContent( - targetState = state, + targetState = (state as? WalletCardState.Content)?.balance?.orMaskWithStars(isBalanceHidden).orEmpty(), label = "Update the balance", modifier = modifier, transitionSpec = { fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith fadeOut(animationSpec = tween(durationMillis = 90)) }, - ) { walletCardState -> - when (walletCardState) { + ) { balance -> + when (state) { is WalletCardState.Content -> { ResizableText( modifier = Modifier .defaultMinSize(minHeight = TangemTheme.dimens.size32) - .flicker(isFlickering = walletCardState.isBalanceFlickering), - text = walletCardState.balance.orMaskWithStars(isBalanceHidden), + .flicker(isFlickering = state.isBalanceFlickering), + text = balance, fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize), color = TangemTheme.colors.text.primary1, overflow = TextOverflow.Ellipsis, 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 3c12b92a98..211ce57e2d 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 @@ -43,6 +43,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList TangemTheme.colors.icon.accent is WalletNotification.RateApp -> TangemTheme.colors.icon.attention is WalletNotification.UnlockWallets -> TangemTheme.colors.icon.primary1 + is WalletNotification.UsedOutdatedData -> TangemTheme.colors.text.attention else -> null }, ) diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt index 732ee16502..115c9535ab 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/viewmodels/intents/WalletWarningsClickIntents.kt @@ -72,6 +72,10 @@ internal interface WalletWarningsClickIntents { fun onSeedPhraseNotificationConfirm() fun onSeedPhraseNotificationDecline() + + fun onSeedPhraseSecondNotificationAccept() + + fun onSeedPhraseSecondNotificationReject() } @Suppress("LongParameterList") @@ -312,6 +316,39 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor( ) } + override fun onSeedPhraseSecondNotificationAccept() { + val userWallet = getSelectedUserWallet() ?: return + + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonUsed) + + walletEventSender.send( + event = WalletEvent.ShowAlert( + state = WalletAlertState.SimpleOkAlert( + message = resourceReference(R.string.warning_seedphrase_issue_answer_yes), + onOkClick = { + viewModelScope.launch { + seedPhraseNotificationUseCase.acceptSecond(userWalletId = userWallet.walletId) + + urlOpener.openUrl( + url = TangemBlogUrlBuilder.build(post = TangemBlogUrlBuilder.Post.SeedNotifySecond), + ) + } + }, + ), + ), + ) + } + + override fun onSeedPhraseSecondNotificationReject() { + val userWallet = getSelectedUserWallet() ?: return + + analyticsEventHandler.send(MainScreen.NoticeSeedPhraseSupportButtonDeclined) + + viewModelScope.launch { + seedPhraseNotificationUseCase.rejectSecond(userWalletId = userWallet.walletId) + } + } + private fun getSelectedUserWallet(): UserWallet? { val userWalletId = stateHolder.getSelectedWalletId() return getUserWalletUseCase(userWalletId).getOrElse { diff --git a/features/wallet/impl/src/main/res/drawable/ill_ghoad_card2_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_ghoad_card2_120_106.webp new file mode 100644 index 0000000000..e42c3f5a98 Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_ghoad_card2_120_106.webp differ diff --git a/features/wallet/impl/src/main/res/drawable/ill_ghoad_card3_120_106.webp b/features/wallet/impl/src/main/res/drawable/ill_ghoad_card3_120_106.webp new file mode 100644 index 0000000000..b72ed95fad Binary files /dev/null and b/features/wallet/impl/src/main/res/drawable/ill_ghoad_card3_120_106.webp differ diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 7f200fada5..1ee33a57c2 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -59,6 +59,7 @@ kotsonGsonExt = "2.5.0" lottie = "3.4.0" lottie-compose = "6.6.0" moshi = "1.15.1" +moshiAdaptersExt = "0.1.5" okhttp = "4.9.3" rekotlin = "1.0.4" retrofit = "2.11.0" @@ -224,6 +225,7 @@ material = { module = "com.google.android.material:material", version.ref = "goo moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" } moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } +moshi-adapters-ext = { module = "dev.onenowy.moshipolymorphicadapter:moshi-polymorphic-adapter", version.ref = "moshiAdaptersExt" } moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } okHttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okHttp-prettyLogging = { module = "com.github.ihsanbal:LoggingInterceptor", version.ref = "okHttp-prettyLogging" }