From 22cbd1b4224baaa852cb01417228bf1600318e3b Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Feb 2025 14:50:26 +0400 Subject: [PATCH 01/13] Updated on 2026-08-14 --- .../di/NetworksStatusesStoreModule.kt | 11 ++++-- .../network/DefaultNetworksStatusesStore.kt | 8 ++--- .../local/network/entity/NetworkStatusDM.kt | 23 +++++++----- .../network/utils/NetworkStatusMapper.kt | 3 +- .../utils/NetworkStatusesSerializer.kt | 36 ------------------- .../tangem/datasource/utils/MoshiTypesExt.kt | 4 +++ .../repository/DefaultNetworksRepository.kt | 13 +++++-- .../data/tokens/utils/NetworkStatusFactory.kt | 1 + .../domain/tokens/model/NetworkStatus.kt | 2 ++ .../tangem/domain/tokens/mock/MockNetworks.kt | 32 ++++++++--------- 10 files changed, 64 insertions(+), 69 deletions(-) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusesSerializer.kt 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/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt index 437628831c..c0ff388721 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,7 +2,7 @@ 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.entity.NetworkStatusesByWalletId import com.tangem.datasource.local.network.utils.toDataModel import com.tangem.datasource.local.network.utils.toDomainModel import com.tangem.domain.tokens.model.Network @@ -16,7 +16,7 @@ import kotlinx.coroutines.sync.withLock internal class DefaultNetworksStatusesStore( private val runtimeDataStore: RuntimeDataStore>, - private val persistenceDataStore: DataStore, + private val persistenceDataStore: DataStore, ) : NetworksStatusesStore { private val mutex = Mutex() @@ -33,7 +33,7 @@ internal class DefaultNetworksStatusesStore( .firstOrNull { it.id == status.networkId } ?: return@mapNotNullTo null - status.toDomainModel(network) + status.toDomainModel(network = network, isCached = true) } .orEmpty() @@ -110,7 +110,7 @@ internal class DefaultNetworksStatusesStore( persistenceDataStore.updateData { storedStatuses -> val userWalletStatuses = storedStatuses[userWalletId.stringValue] ?: emptySet() - val updatedStatuses = userWalletStatuses.addOrReplace(status.toDataModel(network)) { + val updatedStatuses = userWalletStatuses.addOrReplace(item = status.toDataModel(network)) { it.networkId == network.id } 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..43c947e55c 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,34 @@ 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 java.math.BigDecimal -internal typealias NetworkStatusesDM = Map> +internal typealias NetworkStatusesByWalletId = Map> @JsonClass(generateAdapter = true) internal data class NetworkStatusDM( - val networkId: Network.ID, - val selectedAddress: String, - val availableAddresses: Set
, - val amounts: Map, + @Json(name = "network_id") val networkId: Network.ID, + @Json(name = "selected_address") val selectedAddress: String, + @Json(name = "available_addresses") val availableAddresses: Set
, + @Json(name = "amounts") val amounts: Map, ) { @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 index cd0aa4071c..9b8de4a71e 100644 --- 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 @@ -32,7 +32,7 @@ internal fun NetworkStatus.Verified.toDataModel(network: Network): NetworkStatus ) } -internal fun NetworkStatusDM.toDomainModel(network: Network): NetworkStatus { +internal fun NetworkStatusDM.toDomainModel(network: Network, isCached: Boolean): NetworkStatus { return NetworkStatus( network = network, value = NetworkStatus.Verified( @@ -40,6 +40,7 @@ internal fun NetworkStatusDM.toDomainModel(network: Network): NetworkStatus { amounts = mapToDomainAmounts(amounts), pendingTransactions = mapOf(), ), + isCached = isCached, ) } 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/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/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..716ae1da27 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) + } } val currencies = getCurrencies(userWalletId, networks) 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..f3e1ba0f34 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 @@ -36,6 +36,7 @@ internal class NetworkStatusFactory { ), ) }, + isCached = 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..cf693aa286 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 @@ -8,10 +8,12 @@ import java.math.BigDecimal * * @property network The network for which the status is provided. * @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network. + * @property isCached flag that determines whether the status is a cache */ data class NetworkStatus( val network: Network, val value: Value, + val isCached: Boolean, ) { /** 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..e1f5a64040 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 @@ -8,7 +8,6 @@ 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 +51,26 @@ 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), + isCached = false, ) - val networkStatus2 = NetworkStatus( + private val networkStatus2 = NetworkStatus( network = network2, value = NetworkStatus.MissedDerivation, + isCached = false, ) - val networkStatus3 = NetworkStatus( + private val networkStatus3 = NetworkStatus( network = network3, value = NetworkStatus.NoAccount( amountToCreateAccount = amountToCreateAccount, @@ -71,11 +79,10 @@ internal object MockNetworks { ), errorMessage = "", ), + isCached = false, ) - val errorNetworksStatuses = nonEmptySetOf(networkStatus1, networkStatus2, networkStatus3) - - val verifiedNetworkStatus1: NetworkStatus + private val verifiedNetworkStatus1: NetworkStatus get() = networkStatus1.copy( value = NetworkStatus.Verified( amounts = mapOf( @@ -90,7 +97,7 @@ internal object MockNetworks { ), ) - val verifiedNetworkStatus2: NetworkStatus + private val verifiedNetworkStatus2: NetworkStatus get() = networkStatus2.copy( value = NetworkStatus.Verified( amounts = mapOf( @@ -105,7 +112,7 @@ internal object MockNetworks { ), ) - val verifiedNetworkStatus3: NetworkStatus + private val verifiedNetworkStatus3: NetworkStatus get() = networkStatus3.copy( value = NetworkStatus.Verified( amounts = mapOf( @@ -120,11 +127,4 @@ internal object MockNetworks { ), ), ) - - val verifiedNetworksStatuses: NonEmptySet - get() = nonEmptySetOf( - verifiedNetworkStatus1, - verifiedNetworkStatus2, - verifiedNetworkStatus3, - ) } \ No newline at end of file From 127d851b674daa6ea3b26060721d00f6746e6db4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Feb 2025 15:38:16 +0300 Subject: [PATCH 02/13] Updated on 2026-08-14 --- gradle/dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 535afc2fe4..e899c95796 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,9 +89,9 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "develop-952" +tangemBlockchainSdk = "release-app_5.21-953" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "develop-434" +tangemCardSdk = "release-app_5.21-435" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ From 64d78ba3fbc7c155434d88890d838a50ba7cfe64 Mon Sep 17 00:00:00 2001 From: Tangem Date: Mon, 17 Feb 2025 19:28:18 +0700 Subject: [PATCH 03/13] Updated on 2026-08-14 --- gradle/dependencies.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index e899c95796..19433ae682 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -89,7 +89,7 @@ markdownComposeView = "0.5.4" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_5.21-953" +tangemBlockchainSdk = "release-app_5.21-955" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds tangemCardSdk = "release-app_5.21-435" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ From 5bdb9513aa248c2b50216d7f45f785bc192c105a Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Feb 2025 12:55:55 +0300 Subject: [PATCH 04/13] Updated on 2026-08-14 --- .../com/tangem/common/TangemBlogUrlBuilder.kt | 4 + .../api/tangemTech/TangemTechApi.kt | 11 +++ .../models/SeedPhraseNotificationDTO.kt | 6 ++ .../local/preferences/PreferencesKeys.kt | 2 + core/res/src/main/res/values-fr/strings.xml | 6 +- core/res/src/main/res/values-ja/strings.xml | 16 ++-- core/res/src/main/res/values/strings.xml | 4 + .../java/com/tangem/utils/TimeConstants.kt | 4 + .../data/wallets/DefaultWalletsRepository.kt | 95 ++++++++++++++++--- .../models/SeedPhraseNotificationsStatus.kt | 8 ++ .../wallets/repository/WalletsRepository.kt | 7 +- .../usecase/SeedPhraseNotificationUseCase.kt | 11 ++- .../formatter/MarketsDateTimeFormatters.kt | 5 +- .../analytics/WalletScreenAnalyticsEvent.kt | 6 ++ .../utils/WalletWarningsAnalyticsSender.kt | 3 +- .../domain/GetMultiWalletWarningsFactory.kt | 49 +++++++--- .../wallet/state/model/WalletNotification.kt | 19 +++- .../intents/WalletWarningsClickIntents.kt | 37 ++++++++ 18 files changed, 249 insertions(+), 44 deletions(-) create mode 100644 core/utils/src/main/java/com/tangem/utils/TimeConstants.kt create mode 100644 domain/wallets/src/main/java/com/tangem/domain/wallets/models/SeedPhraseNotificationsStatus.kt 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/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/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/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/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/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/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/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..20073ee9ee 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,7 +51,7 @@ 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, @@ -60,6 +60,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( is WalletNotification.Warning.NetworksUnreachable, -> 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..877fb3ad46 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 @@ -9,6 +9,7 @@ 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 @@ -63,21 +64,10 @@ internal class GetMultiWalletWarningsFactory @Inject constructor( 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 +93,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/state/model/WalletNotification.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/model/WalletNotification.kt index dfcfda9298..632dff3210 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( 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 { From 8924c1a93b3886487c78c27ece17181120dabda6 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Feb 2025 20:34:37 +0400 Subject: [PATCH 05/13] Updated on 2026-08-14 --- .../datasource/di/StakingStoreModule.kt | 15 +- .../local/token/DefaultStakingBalanceStore.kt | 173 ++++++++++++++---- .../local/token/StakingBalanceStore.kt | 25 ++- .../token/converter}/BalanceTypeConverter.kt | 4 +- .../converter}/PendingActionConverter.kt | 8 +- .../converter}/StakingActionTypeConverter.kt | 4 +- .../converter}/StakingNetworkTypeConverter.kt | 4 +- .../local/token/converter}/TokenConverter.kt | 10 +- .../token/converter}/YieldBalanceConverter.kt | 23 +-- .../token/entity/YieldBalanceWrappersDTO.kt | 5 - .../token/utils/YieldBalancesSerializer.kt | 36 ---- .../data/staking/DefaultStakingRepository.kt | 57 +++--- .../converters/YieldBalanceListConverter.kt | 12 +- .../data/staking/converters/YieldConverter.kt | 32 ++-- .../action/EnterActionResponseConverter.kt | 4 +- .../transaction/GasEstimateConverter.kt | 8 +- .../StakingTransactionConverter.kt | 8 +- .../staking/model/stakekit/YieldBalance.kt | 15 +- .../model/stakekit/YieldBalanceList.kt | 6 +- .../repository/MockStakingRepository.kt | 12 +- 20 files changed, 261 insertions(+), 200 deletions(-) rename {data/staking/src/main/java/com/tangem/data/staking/converters => core/datasource/src/main/java/com/tangem/datasource/local/token/converter}/BalanceTypeConverter.kt (86%) rename {data/staking/src/main/java/com/tangem/data/staking/converters/action => core/datasource/src/main/java/com/tangem/datasource/local/token/converter}/PendingActionConverter.kt (83%) rename {data/staking/src/main/java/com/tangem/data/staking/converters/action => core/datasource/src/main/java/com/tangem/datasource/local/token/converter}/StakingActionTypeConverter.kt (91%) rename {data/staking/src/main/java/com/tangem/data/staking/converters => core/datasource/src/main/java/com/tangem/datasource/local/token/converter}/StakingNetworkTypeConverter.kt (98%) rename {data/staking/src/main/java/com/tangem/data/staking/converters => core/datasource/src/main/java/com/tangem/datasource/local/token/converter}/TokenConverter.kt (74%) rename {data/staking/src/main/java/com/tangem/data/staking/converters => core/datasource/src/main/java/com/tangem/datasource/local/token/converter}/YieldBalanceConverter.kt (64%) delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/entity/YieldBalanceWrappersDTO.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/token/utils/YieldBalancesSerializer.kt 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/token/DefaultStakingBalanceStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/DefaultStakingBalanceStore.kt index 28f5e8298f..f5798a708a 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,82 @@ 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.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 +86,85 @@ 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 -> + if (runtime is YieldBalance.Error) { + cachedBalances.getBalance(address = runtime.address, integrationId = runtime.integrationId) + ?: runtime + } else { + runtime + } + } + .toSet() + } + + 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..288a1f1323 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,36 @@ -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.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, + isCached = isCached, + ) } 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 +39,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/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/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..c36f0519fa 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 @@ -6,9 +6,14 @@ 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 isCached: Boolean, ) : YieldBalance() { fun getTotalWithRewardsStakingBalance(): BigDecimal { return balance.items.sumOf { it.amount } @@ -34,9 +39,13 @@ sealed class YieldBalance { } } - data object Empty : YieldBalance() + data class Empty( + override val integrationId: String?, + override val address: String, + val isCached: Boolean, + ) : 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/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( From 6df07307c02f6752cf998e8c949c3caf095b9378 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Feb 2025 19:18:14 +0200 Subject: [PATCH 06/13] Updated on 2026-08-14 --- app/build.gradle.kts | 6 +++--- common/ui-charts/build.gradle.kts | 6 +++--- common/ui/build.gradle.kts | 4 ++-- core/datasource/build.gradle.kts | 4 ++-- data/card/build.gradle.kts | 6 +++--- data/common/build.gradle.kts | 4 ++-- data/feedback/build.gradle.kts | 4 ++-- data/manage-tokens/build.gradle.kts | 4 ++-- data/markets/build.gradle.kts | 2 +- data/onramp/build.gradle.kts | 4 ++-- data/qr-scanning/build.gradle.kts | 2 +- data/staking/build.gradle.kts | 4 ++-- data/tokens/build.gradle.kts | 4 ++-- data/transaction/build.gradle.kts | 4 ++-- data/txhistory/build.gradle.kts | 2 +- data/visa/build.gradle.kts | 4 ++-- data/wallets/build.gradle.kts | 2 +- domain/card/build.gradle.kts | 4 ++-- domain/demo/build.gradle.kts | 4 ++-- domain/legacy/build.gradle.kts | 6 +++--- domain/models/build.gradle.kts | 2 +- domain/staking/build.gradle.kts | 2 +- domain/tokens/build.gradle.kts | 2 +- domain/transaction/build.gradle.kts | 6 +++--- domain/wallets/build.gradle.kts | 4 ++-- domain/wallets/models/build.gradle.kts | 2 +- features/details/impl/build.gradle.kts | 4 ++-- features/onboarding-v2/impl/build.gradle.kts | 4 ++-- features/onboarding/build.gradle.kts | 6 +++--- features/qr-scanning/impl/build.gradle.kts | 2 +- features/referral/data/build.gradle.kts | 2 +- features/referral/domain/build.gradle.kts | 2 +- features/send/impl/build.gradle.kts | 4 ++-- features/staking/impl/build.gradle.kts | 4 ++-- features/swap/data/build.gradle.kts | 2 +- features/swap/domain/build.gradle.kts | 4 ++-- features/tester/impl/build.gradle.kts | 2 +- features/tokendetails/impl/build.gradle.kts | 4 ++-- features/wallet/impl/build.gradle.kts | 4 ++-- gradle/dependencies.toml | 19 ------------------- gradle/tangem_dependencies.toml | 18 ++++++++++++++++++ libs/blockchain-sdk/build.gradle.kts | 4 ++-- libs/crypto/build.gradle.kts | 2 +- libs/tangem-sdk-api/build.gradle.kts | 4 ++-- plugins/configuration/settings.gradle.kts | 3 +++ settings.gradle.kts | 4 ++++ 46 files changed, 101 insertions(+), 95 deletions(-) create mode 100644 gradle/tangem_dependencies.toml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cce042451f..f3177619a4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -222,11 +222,11 @@ dependencies { exclude(group = "com.google.protobuf", module = "protobuf-javalite") } /** Tangem libraries */ - implementation(deps.tangem.blockchain) { + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } - implementation(deps.tangem.card.core) - implementation(deps.tangem.card.android) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { exclude(module = "joda-time") } diff --git a/common/ui-charts/build.gradle.kts b/common/ui-charts/build.gradle.kts index 58bf27c3a5..0520d97bb3 100644 --- a/common/ui-charts/build.gradle.kts +++ b/common/ui-charts/build.gradle.kts @@ -13,9 +13,9 @@ dependencies { implementation(projects.core.ui) /** Compose */ - implementation(deps.tangem.vico.core) - implementation(deps.tangem.vico.compose) - implementation(deps.tangem.vico.compose.m3) + implementation(tangemDeps.vico.core) + implementation(tangemDeps.vico.compose) + implementation(tangemDeps.vico.compose.m3) implementation(deps.lifecycle.compose) implementation(deps.compose.foundation) diff --git a/common/ui/build.gradle.kts b/common/ui/build.gradle.kts index 709c410a9f..6a6f96f728 100644 --- a/common/ui/build.gradle.kts +++ b/common/ui/build.gradle.kts @@ -38,8 +38,8 @@ dependencies { implementation(projects.domain.onramp.models) implementation(projects.domain.promo.models) - implementation(deps.tangem.card.core) - implementation(deps.tangem.blockchain) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } } \ No newline at end of file diff --git a/core/datasource/build.gradle.kts b/core/datasource/build.gradle.kts index 3c417c187c..2e43145350 100644 --- a/core/datasource/build.gradle.kts +++ b/core/datasource/build.gradle.kts @@ -34,8 +34,8 @@ dependencies { implementation(projects.domain.models) /** Tangem libraries */ - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) /** DI */ implementation(deps.hilt.android) diff --git a/data/card/build.gradle.kts b/data/card/build.gradle.kts index eb80b3d8cf..6d546bc04d 100644 --- a/data/card/build.gradle.kts +++ b/data/card/build.gradle.kts @@ -13,15 +13,15 @@ android { dependencies { implementation(deps.androidx.datastore) - implementation(deps.tangem.blockchain) { + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } implementation(deps.hilt.android) kapt(deps.hilt.kapt) - implementation(deps.tangem.card.android) - implementation(deps.tangem.card.core) + implementation(tangemDeps.card.android) + implementation(tangemDeps.card.core) implementation(projects.core.datasource) implementation(projects.core.utils) diff --git a/data/common/build.gradle.kts b/data/common/build.gradle.kts index 5a5b6c1d85..96dc4861ca 100644 --- a/data/common/build.gradle.kts +++ b/data/common/build.gradle.kts @@ -19,8 +19,8 @@ dependencies { implementation(projects.domain.tokens.models) /* Libs - SDK */ - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) implementation(projects.libs.blockchainSdk) /* DI */ diff --git a/data/feedback/build.gradle.kts b/data/feedback/build.gradle.kts index 5f75809008..42c689a596 100644 --- a/data/feedback/build.gradle.kts +++ b/data/feedback/build.gradle.kts @@ -21,8 +21,8 @@ dependencies { // endregion // region Tangem libraries - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) // endregion // Other libraries diff --git a/data/manage-tokens/build.gradle.kts b/data/manage-tokens/build.gradle.kts index a8373e38ff..d303cffc3f 100644 --- a/data/manage-tokens/build.gradle.kts +++ b/data/manage-tokens/build.gradle.kts @@ -33,8 +33,8 @@ dependencies { implementation(projects.libs.blockchainSdk) /** Tangem SDKs */ - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) /** AndroidX */ implementation(deps.androidx.datastore) diff --git a/data/markets/build.gradle.kts b/data/markets/build.gradle.kts index 9e8f4d07a7..9f3c3fc995 100644 --- a/data/markets/build.gradle.kts +++ b/data/markets/build.gradle.kts @@ -41,7 +41,7 @@ dependencies { implementation(deps.moshi) implementation(deps.moshi.kotlin) implementation(deps.timber) - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) // endregion diff --git a/data/onramp/build.gradle.kts b/data/onramp/build.gradle.kts index dd8b614228..fc4b8695b8 100644 --- a/data/onramp/build.gradle.kts +++ b/data/onramp/build.gradle.kts @@ -52,8 +52,8 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) - implementation(deps.tangem.card.core) - implementation(deps.tangem.blockchain) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } diff --git a/data/qr-scanning/build.gradle.kts b/data/qr-scanning/build.gradle.kts index 1c801589b7..a422fdbd4a 100644 --- a/data/qr-scanning/build.gradle.kts +++ b/data/qr-scanning/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { implementation(projects.core.ui) /** SdK */ - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) /** DI */ implementation(deps.hilt.android) diff --git a/data/staking/build.gradle.kts b/data/staking/build.gradle.kts index 511b6f79b0..ee2849d585 100644 --- a/data/staking/build.gradle.kts +++ b/data/staking/build.gradle.kts @@ -54,8 +54,8 @@ dependencies { implementation(projects.libs.blockchainSdk) implementation(projects.libs.crypto) - implementation(deps.tangem.card.core) - implementation(deps.tangem.blockchain) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } diff --git a/data/tokens/build.gradle.kts b/data/tokens/build.gradle.kts index b268688860..003cc83849 100644 --- a/data/tokens/build.gradle.kts +++ b/data/tokens/build.gradle.kts @@ -33,8 +33,8 @@ dependencies { implementation(projects.libs.blockchainSdk) /** Tangem SDKs */ - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) /** AndroidX */ implementation(deps.androidx.datastore) diff --git a/data/transaction/build.gradle.kts b/data/transaction/build.gradle.kts index 709f743912..0426e68bbc 100644 --- a/data/transaction/build.gradle.kts +++ b/data/transaction/build.gradle.kts @@ -13,8 +13,8 @@ android { dependencies { /** Tangem SDKs */ - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) /** Core */ implementation(projects.core.datasource) diff --git a/data/txhistory/build.gradle.kts b/data/txhistory/build.gradle.kts index 3a8e769a69..23bb1bd73d 100644 --- a/data/txhistory/build.gradle.kts +++ b/data/txhistory/build.gradle.kts @@ -25,7 +25,7 @@ dependencies { implementation(deps.androidx.paging.runtime) implementation(deps.timber) implementation(deps.jodatime) - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) implementation(deps.hilt.core) kapt(deps.hilt.kapt) diff --git a/data/visa/build.gradle.kts b/data/visa/build.gradle.kts index 5a74bcefe0..b2f273b514 100644 --- a/data/visa/build.gradle.kts +++ b/data/visa/build.gradle.kts @@ -43,8 +43,8 @@ dependencies { kaptForObfuscatingVariants(deps.retrofit.response.type.keeper) /** Libs - Tangem */ - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) /** DI */ implementation(deps.hilt.core) diff --git a/data/wallets/build.gradle.kts b/data/wallets/build.gradle.kts index 2f37436631..46c3ad34e3 100644 --- a/data/wallets/build.gradle.kts +++ b/data/wallets/build.gradle.kts @@ -13,7 +13,7 @@ android { dependencies { /** Tangem libraries */ - implementation(deps.tangem.blockchain) // android-library + implementation(tangemDeps.blockchain) // android-library /** Core */ implementation(projects.core.datasource) diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts index d317bafbf5..a5aa3d005f 100644 --- a/domain/card/build.gradle.kts +++ b/domain/card/build.gradle.kts @@ -20,8 +20,8 @@ dependencies { implementation(projects.domain.tokens.models) implementation(projects.domain.wallets.models) - implementation(deps.tangem.card.core) - implementation(deps.tangem.blockchain) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } diff --git a/domain/demo/build.gradle.kts b/domain/demo/build.gradle.kts index a9e6de5249..83c77bfc66 100644 --- a/domain/demo/build.gradle.kts +++ b/domain/demo/build.gradle.kts @@ -9,6 +9,6 @@ android { } dependencies { - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/domain/legacy/build.gradle.kts b/domain/legacy/build.gradle.kts index f87e9728c8..8a3a831cbe 100644 --- a/domain/legacy/build.gradle.kts +++ b/domain/legacy/build.gradle.kts @@ -25,11 +25,11 @@ dependencies { implementation(projects.domain.txhistory.models) implementation(projects.domain.wallets.models) /** Tangem libraries */ - implementation(deps.tangem.blockchain) { + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } - implementation(deps.tangem.card.core) - implementation(deps.tangem.card.android) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { exclude(module = "joda-time") } diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts index fec72be756..aeafc373d1 100644 --- a/domain/models/build.gradle.kts +++ b/domain/models/build.gradle.kts @@ -10,7 +10,7 @@ plugins { dependencies { api(projects.domain.visa.models) - implementation(deps.tangem.card.core) + implementation(tangemDeps.card.core) implementation(deps.moshi.kotlin) implementation(deps.moshi.adapters) implementation(deps.kotlin.serialization) diff --git a/domain/staking/build.gradle.kts b/domain/staking/build.gradle.kts index a0caf393f8..917cd0ad5f 100644 --- a/domain/staking/build.gradle.kts +++ b/domain/staking/build.gradle.kts @@ -23,7 +23,7 @@ dependencies { implementation(projects.features.staking.api) - implementation(deps.tangem.blockchain) { + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } } \ No newline at end of file diff --git a/domain/tokens/build.gradle.kts b/domain/tokens/build.gradle.kts index 3e8fc1ea6a..9382289ad6 100644 --- a/domain/tokens/build.gradle.kts +++ b/domain/tokens/build.gradle.kts @@ -50,7 +50,7 @@ dependencies { /** Tests */ testImplementation(deps.test.junit) testImplementation(deps.test.coroutine) - testImplementation(deps.tangem.blockchain) { + testImplementation(tangemDeps.blockchain) { exclude(module = "joda-time") } } \ No newline at end of file diff --git a/domain/transaction/build.gradle.kts b/domain/transaction/build.gradle.kts index b54aeca082..f798f98dc8 100644 --- a/domain/transaction/build.gradle.kts +++ b/domain/transaction/build.gradle.kts @@ -16,11 +16,11 @@ dependencies { implementation(projects.core.ui) /** Tangem SDKs */ - implementation(deps.tangem.card.core) - implementation(deps.tangem.card.android) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { exclude(module = "joda-time") } - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) implementation(projects.domain.models) implementation(projects.domain.legacy) diff --git a/domain/wallets/build.gradle.kts b/domain/wallets/build.gradle.kts index 9972fdc9c7..616cbdcc21 100644 --- a/domain/wallets/build.gradle.kts +++ b/domain/wallets/build.gradle.kts @@ -26,7 +26,7 @@ dependencies { // endregion // region Tangem libraries - implementation(deps.tangem.blockchain) // android-library - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) // android-library + implementation(tangemDeps.card.core) // endregion } \ No newline at end of file diff --git a/domain/wallets/models/build.gradle.kts b/domain/wallets/models/build.gradle.kts index 9c259ee6b6..03afdb3da9 100644 --- a/domain/wallets/models/build.gradle.kts +++ b/domain/wallets/models/build.gradle.kts @@ -7,7 +7,7 @@ plugins { dependencies { // region Tangem libraries - implementation(deps.tangem.card.core) + implementation(tangemDeps.card.core) // endregion // region Domain modules diff --git a/features/details/impl/build.gradle.kts b/features/details/impl/build.gradle.kts index b3981ae397..2ffdab4b0e 100644 --- a/features/details/impl/build.gradle.kts +++ b/features/details/impl/build.gradle.kts @@ -45,9 +45,9 @@ dependencies { /* SDK */ // TODO: For TangemError model, should be removed after card domain scanning refactoring - implementation(deps.tangem.card.core) + implementation(tangemDeps.card.core) // For image resolving - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) /* AndroidX */ implementation(deps.androidx.activity.compose) diff --git a/features/onboarding-v2/impl/build.gradle.kts b/features/onboarding-v2/impl/build.gradle.kts index 3f1c7150fc..636d71237b 100644 --- a/features/onboarding-v2/impl/build.gradle.kts +++ b/features/onboarding-v2/impl/build.gradle.kts @@ -45,8 +45,8 @@ dependencies { /** Tangem libraries */ implementation(projects.libs.tangemSdkApi) - implementation(deps.tangem.card.core) - implementation(deps.tangem.card.android) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { exclude(module = "joda-time") } diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts index fb353a3220..6d048e8c4d 100644 --- a/features/onboarding/build.gradle.kts +++ b/features/onboarding/build.gradle.kts @@ -25,8 +25,8 @@ dependencies { implementation(project(":data:common")) /** Tangem libraries */ - implementation(deps.tangem.card.core) - implementation(deps.tangem.card.android) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { exclude(module = "joda-time") } @@ -50,7 +50,7 @@ dependencies { implementation(deps.compose.coil) implementation(deps.compose.shimmer) - implementation(deps.tangem.card.core) + implementation(tangemDeps.card.core) /** Other libraries */ implementation(deps.compose.shimmer) diff --git a/features/qr-scanning/impl/build.gradle.kts b/features/qr-scanning/impl/build.gradle.kts index 87bb1cc68e..f8bb506a61 100644 --- a/features/qr-scanning/impl/build.gradle.kts +++ b/features/qr-scanning/impl/build.gradle.kts @@ -60,5 +60,5 @@ dependencies { /** Other dependencies */ implementation(deps.arrow.core) implementation(deps.timber) - implementation(deps.tangem.card.core) + implementation(tangemDeps.card.core) } \ No newline at end of file diff --git a/features/referral/data/build.gradle.kts b/features/referral/data/build.gradle.kts index 954bac9c2e..bbe186db0f 100644 --- a/features/referral/data/build.gradle.kts +++ b/features/referral/data/build.gradle.kts @@ -38,5 +38,5 @@ dependencies { kapt(deps.hilt.kapt) /** Tangem deps */ - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) } \ No newline at end of file diff --git a/features/referral/domain/build.gradle.kts b/features/referral/domain/build.gradle.kts index 4206513db7..a77d439bbb 100644 --- a/features/referral/domain/build.gradle.kts +++ b/features/referral/domain/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(deps.arrow.core) implementation(deps.jodatime) implementation(deps.timber) - implementation(deps.tangem.card.core) + implementation(tangemDeps.card.core) /** DI */ implementation(deps.hilt.android) diff --git a/features/send/impl/build.gradle.kts b/features/send/impl/build.gradle.kts index 176864f05a..6ab25de93a 100644 --- a/features/send/impl/build.gradle.kts +++ b/features/send/impl/build.gradle.kts @@ -39,8 +39,8 @@ dependencies { implementation(deps.compose.constraintLayout) /** Tangem SDKs */ - implementation(deps.tangem.card.core) - implementation(deps.tangem.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) /** Core modules */ implementation(projects.core.configToggles) diff --git a/features/staking/impl/build.gradle.kts b/features/staking/impl/build.gradle.kts index be3dbdd1e2..bbfa41e6e2 100644 --- a/features/staking/impl/build.gradle.kts +++ b/features/staking/impl/build.gradle.kts @@ -36,8 +36,8 @@ dependencies { implementation(deps.compose.constraintLayout) /** Tangem SDKs */ - implementation(deps.tangem.card.core) - implementation(deps.tangem.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) /** Core modules */ implementation(projects.core.configToggles) diff --git a/features/swap/data/build.gradle.kts b/features/swap/data/build.gradle.kts index 805cd27a59..f9af5bb02d 100644 --- a/features/swap/data/build.gradle.kts +++ b/features/swap/data/build.gradle.kts @@ -44,7 +44,7 @@ dependencies { implementation(projects.data.common) /** Tangem SDKs */ - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) /** Others */ implementation(deps.timber) diff --git a/features/swap/domain/build.gradle.kts b/features/swap/domain/build.gradle.kts index ed5fe567e7..46a1c51668 100644 --- a/features/swap/domain/build.gradle.kts +++ b/features/swap/domain/build.gradle.kts @@ -51,8 +51,8 @@ dependencies { implementation(deps.kotlin.coroutines) implementation(deps.arrow.core) implementation(deps.timber) - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) implementation(deps.moshi) kaptForObfuscatingVariants(deps.moshi.kotlin.codegen) } \ No newline at end of file diff --git a/features/tester/impl/build.gradle.kts b/features/tester/impl/build.gradle.kts index 5c994aba93..5f26b9b233 100644 --- a/features/tester/impl/build.gradle.kts +++ b/features/tester/impl/build.gradle.kts @@ -50,7 +50,7 @@ dependencies { implementation(projects.features.tester.api) /* SDK */ - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) /** Other modules */ implementation(projects.common.routing) diff --git a/features/tokendetails/impl/build.gradle.kts b/features/tokendetails/impl/build.gradle.kts index 4a38dc8f9b..1457affebd 100644 --- a/features/tokendetails/impl/build.gradle.kts +++ b/features/tokendetails/impl/build.gradle.kts @@ -35,8 +35,8 @@ dependencies { implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) implementation(deps.reKotlin) - implementation(deps.tangem.blockchain) - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) + implementation(tangemDeps.card.core) implementation(deps.timber) implementation(deps.lifecycle.compose) implementation(deps.kotlin.serialization) diff --git a/features/wallet/impl/build.gradle.kts b/features/wallet/impl/build.gradle.kts index 3d27a12451..99809111b3 100644 --- a/features/wallet/impl/build.gradle.kts +++ b/features/wallet/impl/build.gradle.kts @@ -39,8 +39,8 @@ dependencies { implementation(deps.jodatime) implementation(deps.kotlin.immutable.collections) implementation(deps.reKotlin) - implementation(deps.tangem.card.core) - implementation(deps.tangem.blockchain) + implementation(tangemDeps.card.core) + implementation(tangemDeps.blockchain) implementation(deps.timber) implementation(deps.firebase.perf) { exclude(group = "com.google.firebase", module = "protolite-well-known-types") diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 19433ae682..060b0ba823 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -88,15 +88,6 @@ markdown = "0.7.2" markdownComposeView = "0.5.4" # endregion Other libraries -# region Tangem -tangemBlockchainSdk = "release-app_5.21-955" -#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.21-435" -#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ -tangemVico = "2.0.0-alpha.25-tangem-developments8" -#tangemVico = "0.0.1" # Keep it! - used for local builds ^ -# endregion Tangem - # region Tools detektComposeRules = "1.3.0" detekt = "1.22.0" @@ -188,16 +179,6 @@ firebase-messaging = { module = "com.google.firebase:firebase-messaging-ktx" } firebase-perf = { module = "com.google.firebase:firebase-perf" } # endregion Firebase -# region Tangem -tangem-blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } -tangem-card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } -tangem-card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } - -tangem-vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } -tangem-vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } -tangem-vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } -# endregion Tangem - # region Detekt detekt-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "detektComposeRules" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml new file mode 100644 index 0000000000..d49f7f2f1d --- /dev/null +++ b/gradle/tangem_dependencies.toml @@ -0,0 +1,18 @@ +[versions] +tangemBlockchainSdk = "release-app_5.21-955" +#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds +tangemCardSdk = "release-app_5.21-435" +#tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ +tangemVico = "2.0.0-alpha.25-tangem-developments8" +#tangemVico = "0.0.1" # Keep it! - used for local builds ^ + + + +[libraries] +blockchain = { module = "com.tangem:blockchain", version.ref = "tangemBlockchainSdk" } +card-android = { module = "com.tangem.tangem-sdk-kotlin:android", version.ref = "tangemCardSdk" } +card-core = { module = "com.tangem.tangem-sdk-kotlin:core", version.ref = "tangemCardSdk" } + +vico-compose = { group = "com.tangem.vico", name = "compose", version.ref = "tangemVico" } +vico-compose-m3 = { group = "com.tangem.vico", name = "compose-m3", version.ref = "tangemVico" } +vico-core = { group = "com.tangem.vico", name = "core", version.ref = "tangemVico" } \ No newline at end of file diff --git a/libs/blockchain-sdk/build.gradle.kts b/libs/blockchain-sdk/build.gradle.kts index 12234d749f..31e9b0408e 100644 --- a/libs/blockchain-sdk/build.gradle.kts +++ b/libs/blockchain-sdk/build.gradle.kts @@ -45,8 +45,8 @@ dependencies { // endregion // region Tangem libraries - implementation(deps.tangem.blockchain) { exclude(module = "joda-time") } - implementation(deps.tangem.card.core) + implementation(tangemDeps.blockchain) { exclude(module = "joda-time") } + implementation(tangemDeps.card.core) // endregion testImplementation(deps.test.coroutine) diff --git a/libs/crypto/build.gradle.kts b/libs/crypto/build.gradle.kts index 1e443985c3..0278a0501c 100644 --- a/libs/crypto/build.gradle.kts +++ b/libs/crypto/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(deps.kotlin.coroutines) /** SDK */ - implementation(deps.tangem.blockchain) + implementation(tangemDeps.blockchain) /** Core */ implementation(projects.core.utils) diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts index 514702adfa..577365a357 100644 --- a/libs/tangem-sdk-api/build.gradle.kts +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -22,8 +22,8 @@ dependencies { implementation(projects.core.res) /** Tangem libraries */ - implementation(deps.tangem.card.core) - implementation(deps.tangem.card.android) { + implementation(tangemDeps.card.core) + implementation(tangemDeps.card.android) { exclude(module = "joda-time") } diff --git a/plugins/configuration/settings.gradle.kts b/plugins/configuration/settings.gradle.kts index 0ed5a2e04b..13c8987dd9 100644 --- a/plugins/configuration/settings.gradle.kts +++ b/plugins/configuration/settings.gradle.kts @@ -3,5 +3,8 @@ dependencyResolutionManagement { create("deps") { from(files("../../gradle/dependencies.toml")) } + create("tangemDeps") { + from(files("../../gradle/tangem_dependencies.toml")) + } } } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 7d45b0e97e..1ad6153ba0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -113,7 +113,11 @@ dependencyResolutionManagement { create("deps") { from(files("gradle/dependencies.toml")) } + create("tangemDeps") { + from(files("gradle/tangem_dependencies.toml")) + } } + } enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") From 42f0e3f0fea1e5515204bbbc0797eb51675228bc Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Feb 2025 13:56:56 +0500 Subject: [PATCH 07/13] Updated on 2026-08-14 --- .../wallet/domain/Wallet2CobrandImage.kt | 6 ++++++ .../res/drawable/ill_ghoad_card2_120_106.webp | Bin 0 -> 42880 bytes .../res/drawable/ill_ghoad_card3_120_106.webp | Bin 0 -> 58916 bytes 3 files changed, 6 insertions(+) create mode 100644 features/wallet/impl/src/main/res/drawable/ill_ghoad_card2_120_106.webp create mode 100644 features/wallet/impl/src/main/res/drawable/ill_ghoad_card3_120_106.webp 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/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 0000000000000000000000000000000000000000..e42c3f5a98cfddd85e75021744fc8977997d21b5 GIT binary patch literal 42880 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b72ed95fadbac5c8c50fe9e7dde1362afd175208 GIT binary patch literal 58916 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�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 IZ}ETt0G2cVD*ylh literal 0 HcmV?d00001 From 0bf2decb830834794ff3c4bbda8b20aa7e04ee53 Mon Sep 17 00:00:00 2001 From: Tangem Date: Tue, 18 Feb 2025 20:43:38 +0400 Subject: [PATCH 08/13] Updated on 2026-08-14 --- core/datasource/build.gradle.kts | 1 + .../com/tangem/datasource/di/MoshiModule.kt | 7 ++ .../network/DefaultNetworksStatusesStore.kt | 20 ++--- .../converter/NetworkAddressConverter.kt | 60 ++++++++++++++ .../converter/NetworkAmountsConverter.kt | 35 ++++++++ .../converter/NetworkStatusConverter.kt | 46 +++++++++++ .../NetworkStatusDataModelConverter.kt | 38 +++++++++ .../local/network/entity/NetworkStatusDM.kt | 30 +++++-- .../network/utils/NetworkStatusMapper.kt | 81 ------------------- .../data/tokens/utils/NetworkStatusFactory.kt | 3 +- .../domain/tokens/model/NetworkStatus.kt | 23 ++++-- .../tangem/domain/tokens/mock/MockNetworks.kt | 7 +- gradle/dependencies.toml | 2 + 13 files changed, 244 insertions(+), 109 deletions(-) create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAddressConverter.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkAmountsConverter.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusDataModelConverter.kt delete mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusMapper.kt 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/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/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt index c0ff388721..b1380981a3 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,9 +2,9 @@ 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.NetworkStatusesByWalletId -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.tokens.model.Network import com.tangem.domain.tokens.model.NetworkStatus import com.tangem.domain.wallets.models.UserWalletId @@ -14,6 +14,8 @@ import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +private typealias NetworkStatusesByWalletId = Map> + internal class DefaultNetworksStatusesStore( private val runtimeDataStore: RuntimeDataStore>, private val persistenceDataStore: DataStore, @@ -33,7 +35,7 @@ internal class DefaultNetworksStatusesStore( .firstOrNull { it.id == status.networkId } ?: return@mapNotNullTo null - status.toDomainModel(network = network, isCached = true) + NetworkStatusConverter(network = network, isCached = true).convert(value = status) } .orEmpty() @@ -103,16 +105,14 @@ internal class DefaultNetworksStatusesStore( } private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) { - val status = networkStatus.value val network = networkStatus.network - if (status !is NetworkStatus.Verified) return - persistenceDataStore.updateData { storedStatuses -> val userWalletStatuses = storedStatuses[userWalletId.stringValue] ?: emptySet() - val updatedStatuses = userWalletStatuses.addOrReplace(item = status.toDataModel(network)) { - it.networkId == network.id - } + val updatedStatuses = userWalletStatuses.addOrReplace( + item = NetworkStatusDataModelConverter.convert(value = networkStatus), + predicate = { it.networkId == network.id }, + ) storedStatuses.toMutableMap().apply { this[userWalletId.stringValue] = updatedStatuses 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..5f01c06bec --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/network/converter/NetworkStatusConverter.kt @@ -0,0 +1,46 @@ +package com.tangem.datasource.local.network.converter + +import com.tangem.datasource.local.network.entity.NetworkStatusDM +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(), + isCached = isCached, + ) + } + is NetworkStatusDM.NoAccount -> { + NetworkStatus.NoAccount( + address = address, + amountToCreateAccount = value.amountToCreateAccount, + errorMessage = value.errorMessage, + isCached = isCached, + ) + } + } + + 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..1a52b27c9e --- /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 -> error("Unsupported network status: $value") + } + } +} \ 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 43c947e55c..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 @@ -3,17 +3,31 @@ 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 NetworkStatusesByWalletId = Map> +internal sealed interface NetworkStatusDM { -@JsonClass(generateAdapter = true) -internal data class NetworkStatusDM( - @Json(name = "network_id") val networkId: Network.ID, - @Json(name = "selected_address") val selectedAddress: String, - @Json(name = "available_addresses") val availableAddresses: Set
, - @Json(name = "amounts") 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( 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 9b8de4a71e..0000000000 --- a/core/datasource/src/main/java/com/tangem/datasource/local/network/utils/NetworkStatusMapper.kt +++ /dev/null @@ -1,81 +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, isCached: Boolean): NetworkStatus { - return NetworkStatus( - network = network, - value = NetworkStatus.Verified( - address = mapToDomainAddress(selectedAddress, availableAddresses), - amounts = mapToDomainAmounts(amounts), - pendingTransactions = mapOf(), - ), - isCached = isCached, - ) -} - -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/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 f3e1ba0f34..af8e2c7fe4 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 @@ -26,6 +26,7 @@ internal class NetworkStatusFactory { address = getNetworkAddress(result.selectedAddress, result.addresses), amountToCreateAccount = result.amountToCreateAccount, errorMessage = result.errorMessage, + isCached = false, ) is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( address = getNetworkAddress(result.selectedAddress, result.addresses), @@ -34,9 +35,9 @@ internal class NetworkStatusFactory { transactions = result.currentTransactions, currencies = currencies, ), + isCached = false, ) }, - isCached = 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 cf693aa286..6b7d2c090b 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 @@ -8,12 +8,10 @@ import java.math.BigDecimal * * @property network The network for which the status is provided. * @property value The specific status value, represented as a sealed class to encapsulate the various possible states of the network. - * @property isCached flag that determines whether the status is a cache */ data class NetworkStatus( val network: Network, val value: Value, - val isCached: Boolean, ) { /** @@ -21,24 +19,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 isCached: Boolean + } /** * Represents the state where the network is refreshing. */ - data object Refreshing : Value() + data object Refreshing : Value() { + override val isCached: Boolean = false + } /** * 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 isCached: Boolean = false + } /** * Represents the state where a derivation has been missed. */ - data object MissedDerivation : Value() + data object MissedDerivation : Value() { + override val isCached: Boolean = false + } /** * Represents the verified state of the network, including the amounts associated with different cryptocurrencies @@ -47,12 +54,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 isCached flag that determines whether the status is a cache * within the network. */ data class Verified( val address: NetworkAddress, val amounts: Map, val pendingTransactions: Map>, + override val isCached: Boolean, ) : Value() /** @@ -61,10 +70,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 isCached flag that determines whether the status is a cache */ data class NoAccount( val address: NetworkAddress, val amountToCreateAccount: BigDecimal, val errorMessage: String, + override val isCached: Boolean, ) : Value() } \ 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 e1f5a64040..88ffcdfeaf 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 @@ -61,13 +61,11 @@ internal object MockNetworks { private val networkStatus1 = NetworkStatus( network = network1, value = NetworkStatus.Unreachable(address = null), - isCached = false, ) private val networkStatus2 = NetworkStatus( network = network2, value = NetworkStatus.MissedDerivation, - isCached = false, ) private val networkStatus3 = NetworkStatus( @@ -78,8 +76,8 @@ internal object MockNetworks { defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), errorMessage = "", + isCached = false, ), - isCached = false, ) private val verifiedNetworkStatus1: NetworkStatus @@ -94,6 +92,7 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + isCached = false, ), ) @@ -109,6 +108,7 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + isCached = false, ), ) @@ -125,6 +125,7 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), + isCached = false, ), ) } \ No newline at end of file diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index 060b0ba823..e31e770f5a 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" } From 36e7f2d07f2b0f42e2bced06fd4fbf7a4fb287a4 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Feb 2025 12:03:10 +0400 Subject: [PATCH 09/13] Updated on 2026-08-14 --- common/ui/build.gradle.kts | 1 + .../network/DefaultNetworksStatusesStore.kt | 38 +++++++++++--- .../converter/NetworkStatusConverter.kt | 5 +- .../NetworkStatusDataModelConverter.kt | 6 +-- .../local/quote/DefaultQuotesStore.kt | 44 ++++++++++++---- .../datasource/local/quote/QuotesStore.kt | 3 ++ .../local/quote/converter/QuoteConverter.kt | 3 +- .../local/token/DefaultStakingBalanceStore.kt | 22 +++++--- .../token/converter/YieldBalanceConverter.kt | 5 +- .../converters/HotCryptoCurrencyConverter.kt | 3 +- .../repository/DefaultQuotesRepository.kt | 1 + .../data/tokens/utils/NetworkStatusFactory.kt | 5 +- .../com/tangem/domain/models/StatusSource.kt | 24 +++++++++ domain/staking/models/build.gradle.kts | 1 + .../staking/model/stakekit/YieldBalance.kt | 5 +- domain/tokens/models/build.gradle.kts | 1 + .../tokens/model/CryptoCurrencyStatus.kt | 5 ++ .../domain/tokens/model/NetworkStatus.kt | 17 +++--- .../com/tangem/domain/tokens/model/Quote.kt | 5 +- .../operations/CurrencyStatusOperations.kt | 52 +++++++++++++++---- .../tangem/domain/tokens/mock/MockNetworks.kt | 9 ++-- .../tangem/domain/tokens/mock/MockQuotes.kt | 21 ++++---- .../domain/tokens/mock/MockTokensStates.kt | 6 +++ 23 files changed, 212 insertions(+), 70 deletions(-) create mode 100644 domain/models/src/main/kotlin/com/tangem/domain/models/StatusSource.kt 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/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 b1380981a3..2d5126dd66 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 @@ -5,6 +5,7 @@ import com.tangem.datasource.local.datastore.RuntimeDataStore 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 @@ -98,21 +99,42 @@ internal class DefaultNetworksStatusesStore( cachedStatuses: Set, runtimeStatuses: Set, ): Set { - val runtimeMap = runtimeStatuses.associateBy { it.network } - val mergedCached = cachedStatuses.filter { it.network !in runtimeMap.keys } + return runtimeStatuses + .map { runtime -> + runtime.takeIf { it.value !is NetworkStatus.Unreachable } + ?: getCachedStatusIfPossible(cachedStatuses = cachedStatuses, runtime = runtime) + } + .toSet() + } - return mergedCached.toSet() + runtimeStatuses + private fun getCachedStatusIfPossible(cachedStatuses: Set, runtime: NetworkStatus): NetworkStatus { + val cached = cachedStatuses.firstOrNull { it.network == runtime.network } ?: return runtime + + 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 { + runtime + } } private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) { val network = networkStatus.network + // Converter will return null if the network status is not supported + val newStatus = NetworkStatusDataModelConverter.convert(value = networkStatus) ?: return + persistenceDataStore.updateData { storedStatuses -> - val userWalletStatuses = storedStatuses[userWalletId.stringValue] ?: emptySet() - val updatedStatuses = userWalletStatuses.addOrReplace( - item = NetworkStatusDataModelConverter.convert(value = networkStatus), - predicate = { it.networkId == network.id }, - ) + val userWalletStatuses = storedStatuses[userWalletId.stringValue].orEmpty() + val updatedStatuses = userWalletStatuses.addOrReplace(item = newStatus) { it.networkId == network.id } storedStatuses.toMutableMap().apply { this[userWalletId.stringValue] = updatedStatuses 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 index 5f01c06bec..f0a01555bf 100644 --- 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 @@ -1,6 +1,7 @@ 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 @@ -28,7 +29,7 @@ internal class NetworkStatusConverter( address = address, amounts = NetworkAmountsConverter.convert(value = value.amounts), pendingTransactions = mapOf(), - isCached = isCached, + source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, ) } is NetworkStatusDM.NoAccount -> { @@ -36,7 +37,7 @@ internal class NetworkStatusConverter( address = address, amountToCreateAccount = value.amountToCreateAccount, errorMessage = value.errorMessage, - isCached = isCached, + source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, ) } } 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 index 1a52b27c9e..cf42370c96 100644 --- 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 @@ -9,9 +9,9 @@ import com.tangem.utils.converter.Converter * [REDACTED_AUTHOR] */ -internal object NetworkStatusDataModelConverter : Converter { +internal object NetworkStatusDataModelConverter : Converter { - override fun convert(value: NetworkStatus): NetworkStatusDM { + override fun convert(value: NetworkStatus): NetworkStatusDM? { return when (val status = value.value) { is NetworkStatus.Verified -> { NetworkStatusDM.Verified( @@ -32,7 +32,7 @@ internal object NetworkStatusDataModelConverter : Converter error("Unsupported network status: $value") + else -> null } } } \ No newline at end of file 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 f5798a708a..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 @@ -4,6 +4,7 @@ import androidx.datastore.core.DataStore import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO 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 @@ -146,16 +147,25 @@ internal class DefaultStakingBalanceStore( ): Set { return runtimeBalances .map { runtime -> - if (runtime is YieldBalance.Error) { - cachedBalances.getBalance(address = runtime.address, integrationId = runtime.integrationId) - ?: runtime - } else { - 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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt index 288a1f1323..9df25f3294 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/local/token/converter/YieldBalanceConverter.kt @@ -1,6 +1,7 @@ package com.tangem.datasource.local.token.converter 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 @@ -13,7 +14,7 @@ internal class YieldBalanceConverter(private val isCached: Boolean) : Converter< YieldBalance.Empty( integrationId = value.integrationId, address = value.addresses.address, - isCached = isCached, + source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, ) } else { YieldBalance.Data( @@ -39,7 +40,7 @@ internal class YieldBalanceConverter(private val isCached: Boolean) : Converter< .sortedWith(compareBy({ it.type }, { it.amount })), integrationId = value.integrationId, ), - isCached = isCached, + source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL, ) } } 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/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..e03676a2dc 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 @@ -122,6 +122,7 @@ internal class DefaultQuotesRepository( }, onError = { error -> cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) }) + quotesStore.storeEmptyQuotes(currenciesIds = rawCurrenciesIds) throw error }, 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 af8e2c7fe4..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,7 +27,7 @@ internal class NetworkStatusFactory { address = getNetworkAddress(result.selectedAddress, result.addresses), amountToCreateAccount = result.amountToCreateAccount, errorMessage = result.errorMessage, - isCached = false, + source = StatusSource.ACTUAL, ) is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified( address = getNetworkAddress(result.selectedAddress, result.addresses), @@ -35,7 +36,7 @@ internal class NetworkStatusFactory { transactions = result.currentTransactions, currencies = currencies, ), - isCached = false, + source = StatusSource.ACTUAL, ) }, ) 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 c36f0519fa..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,5 +1,6 @@ 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 @@ -13,7 +14,7 @@ sealed class YieldBalance { override val integrationId: String?, override val address: String, val balance: YieldBalanceItem, - val isCached: Boolean, + val source: StatusSource, ) : YieldBalance() { fun getTotalWithRewardsStakingBalance(): BigDecimal { return balance.items.sumOf { it.amount } @@ -42,7 +43,7 @@ sealed class YieldBalance { data class Empty( override val integrationId: String?, override val address: String, - val isCached: Boolean, + val source: StatusSource, ) : YieldBalance() data class Error(override val integrationId: String?, override val address: String?) : YieldBalance() 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 6b7d2c090b..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 @@ -21,14 +22,14 @@ data class NetworkStatus( */ sealed class Value { - abstract val isCached: Boolean + abstract val source: StatusSource } /** * Represents the state where the network is refreshing. */ data object Refreshing : Value() { - override val isCached: Boolean = false + override val source: StatusSource = StatusSource.ACTUAL } /** @@ -37,14 +38,14 @@ data class NetworkStatus( * @property address Network addresses. */ data class Unreachable(val address: NetworkAddress?) : Value() { - override val isCached: Boolean = false + override val source: StatusSource = StatusSource.ACTUAL } /** * Represents the state where a derivation has been missed. */ data object MissedDerivation : Value() { - override val isCached: Boolean = false + override val source: StatusSource = StatusSource.ACTUAL } /** @@ -54,14 +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 isCached flag that determines whether the status is a cache + * @property source source of data * within the network. */ data class Verified( val address: NetworkAddress, val amounts: Map, val pendingTransactions: Map>, - override val isCached: Boolean, + override val source: StatusSource, ) : Value() /** @@ -70,12 +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 isCached flag that determines whether the status is a cache + * @property source source of data */ data class NoAccount( val address: NetworkAddress, val amountToCreateAccount: BigDecimal, val errorMessage: String, - override val isCached: Boolean, + 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/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/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt b/domain/tokens/src/test/kotlin/com/tangem/domain/tokens/mock/MockNetworks.kt index 88ffcdfeaf..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,6 +2,7 @@ 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 @@ -76,7 +77,7 @@ internal object MockNetworks { defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), errorMessage = "", - isCached = false, + source = StatusSource.ACTUAL, ), ) @@ -92,7 +93,7 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), - isCached = false, + source = StatusSource.ACTUAL, ), ) @@ -108,7 +109,7 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), - isCached = false, + source = StatusSource.ACTUAL, ), ) @@ -125,7 +126,7 @@ internal object MockNetworks { address = NetworkAddress.Single( defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary), ), - isCached = false, + source = StatusSource.ACTUAL, ), ) } \ 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/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) From 6f47e7d742f1e6f264f89c3b78e3f66ca90d1413 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Feb 2025 17:35:03 +0400 Subject: [PATCH 10/13] Updated on 2026-08-14 --- .../components/token/internal/TokenPrice.kt | 8 +++++++- .../components/token/state/TokenItemState.kt | 1 + .../utils/WalletWarningsAnalyticsSender.kt | 1 + .../domain/GetMultiWalletWarningsFactory.kt | 19 +++++++++++++++++++ .../wallet/state/model/WalletNotification.kt | 7 +++++++ .../wallet/state/model/WalletState.kt | 2 +- .../wallet/ui/components/common/WalletCard.kt | 10 +++++----- .../components/common/WalletNotifications.kt | 1 + 8 files changed, 42 insertions(+), 7 deletions(-) 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/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 20073ee9ee..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 @@ -58,6 +58,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor( 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 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 877fb3ad46..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,6 +4,7 @@ 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 @@ -45,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) @@ -62,6 +65,22 @@ 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: SeedPhraseNotificationsStatus, 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 632dff3210..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 @@ -234,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/ui/components/common/WalletCard.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletCard.kt index 2539a970d6..90233fa504 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 @@ -279,21 +279,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 }, ) From 6a5a8300cf698ecb702ec8f739c685c536043bac Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 19 Feb 2025 16:23:03 +0300 Subject: [PATCH 11/13] Updated on 2026-08-14 --- .../tap/di/domain/MarketsDomainModule.kt | 14 +++++++ .../java/com/tangem/utils/extensions/List.kt | 8 ++++ .../repository/DefaultCurrenciesRepository.kt | 16 +++++++- domain/markets/build.gradle.kts | 9 +++++ ...FilterAvailableNetworksForWalletUseCase.kt | 38 +++++++++++++++++++ ...AllWalletsCryptoCurrencyStatusesUseCase.kt | 4 +- .../tokens/repository/CurrenciesRepository.kt | 5 ++- .../repository/MockCurrenciesRepository.kt | 1 + .../impl/loader/PortfolioDataLoader.kt | 29 +++++++------- .../impl/model/MarketsPortfolioModel.kt | 10 ++++- 10 files changed, 116 insertions(+), 18 deletions(-) create mode 100644 domain/markets/src/main/java/com/tangem/domain/markets/FilterAvailableNetworksForWalletUseCase.kt 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/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/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/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/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/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/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/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), ) }, From 3269095c97c79ee2fbadcb8e1951604b9c8e8743 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Feb 2025 12:53:26 +0400 Subject: [PATCH 12/13] Updated on 2026-08-14 --- .../ui/tokens/TokenItemStateConverter.kt | 30 +++++-- .../network/DefaultNetworksStatusesStore.kt | 80 ++++++++----------- .../local/network/NetworksStatusesStore.kt | 2 +- .../java/com/tangem/utils/extensions/Set.kt | 12 +++ .../repository/DefaultNetworksRepository.kt | 2 +- .../repository/DefaultQuotesRepository.kt | 24 +++--- .../domain/tokens/model/TotalFiatBalance.kt | 2 + .../tangem/domain/tokens/model/TokenList.kt | 2 + .../TokenListFiatBalanceOperations.kt | 13 ++- .../domain/tokens/mock/MockTokenLists.kt | 3 + .../TokenDetailsLoadedBalanceConverter.kt | 13 ++- .../SetBalancesAndLimitsTransformer.kt | 17 ++-- .../SetTokenListErrorTransformer.kt | 13 ++- .../MultiWalletCardStateConverter.kt | 14 ++-- .../SingleWalletCardStateConverter.kt | 17 ++-- 15 files changed, 148 insertions(+), 96 deletions(-) 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/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt b/core/datasource/src/main/java/com/tangem/datasource/local/network/DefaultNetworksStatusesStore.kt index 2d5126dd66..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 @@ -10,10 +10,9 @@ 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> @@ -22,8 +21,6 @@ internal class DefaultNetworksStatusesStore( private val persistenceDataStore: DataStore, ) : NetworksStatusesStore { - private val mutex = Mutex() - override fun get(key: UserWalletId): Flow> { return runtimeDataStore.get(provideStringKey(key)) } @@ -42,11 +39,20 @@ internal class DefaultNetworksStatusesStore( 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, ) @@ -57,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) } } } @@ -96,19 +86,24 @@ 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 { - return runtimeStatuses - .map { runtime -> - runtime.takeIf { it.value !is NetworkStatus.Unreachable } - ?: getCachedStatusIfPossible(cachedStatuses = cachedStatuses, runtime = runtime) + return networks.mapNotNullTo(hashSetOf()) { network -> + val runtimeStatus = runtimeStatuses.firstOrNull { it.network == network } + + if (runtimeStatus == null || runtimeStatus.value is NetworkStatus.Unreachable) { + getCachedStatusIfPossible(cachedStatuses = cachedStatuses, network = network) + ?: runtimeStatus + } else { + runtimeStatus } - .toSet() + } } - private fun getCachedStatusIfPossible(cachedStatuses: Set, runtime: NetworkStatus): NetworkStatus { - val cached = cachedStatuses.firstOrNull { it.network == runtime.network } ?: return runtime + private fun getCachedStatusIfPossible(cachedStatuses: Set, network: Network): NetworkStatus? { + val cached = cachedStatuses.firstOrNull { it.network == network } ?: return null val updatedCachedStatus = when (val status = cached.value) { is NetworkStatus.NoAccount -> status.copy(source = StatusSource.ONLY_CACHE) @@ -122,22 +117,17 @@ internal class DefaultNetworksStatusesStore( return if (updatedCachedStatus != null) { cached.copy(value = updatedCachedStatus) } else { - runtime + null } } - private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) { - val network = networkStatus.network - + private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, statuses: Set) { // Converter will return null if the network status is not supported - val newStatus = NetworkStatusDataModelConverter.convert(value = networkStatus) ?: return + val newStatuses = NetworkStatusDataModelConverter.convertSet(input = statuses).filterNotNull().toSet() persistenceDataStore.updateData { storedStatuses -> - val userWalletStatuses = storedStatuses[userWalletId.stringValue].orEmpty() - val updatedStatuses = userWalletStatuses.addOrReplace(item = newStatus) { 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/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/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 716ae1da27..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 @@ -129,7 +129,7 @@ internal class DefaultNetworksRepository( } if (statusesToRefresh != null) { - networksStatusesStore.storeAll(key = userWalletId, values = statusesToRefresh) + networksStatusesStore.storeAll(key = userWalletId, values = statusesToRefresh.toSet()) } } 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 e03676a2dc..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,25 +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 -> + Timber.e(error) + cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) }) quotesStore.storeEmptyQuotes(currenciesIds = rawCurrenciesIds) - - throw error }, ) - - val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies( - response, - replacementIdsResult.idsFiltered, - ) - - quotesStore.store(updatedResponse) } private suspend fun filterExpiredCurrenciesIds( 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/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/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/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/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 91ac3d6a00..1c1e455117 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/state/transformers/SetBalancesAndLimitsTransformer.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/state/transformers/SetBalancesAndLimitsTransformer.kt index 97bbb348fb..9fcfe012b5 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 @@ -78,17 +78,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 3705598bcd..46073ee11f 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 @@ -62,14 +63,12 @@ internal class SetTokenListErrorTransformer( imageResId = imageResId, onRenameClick = onRenameClick, onDeleteClick = onDeleteClick, - 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 2fdbb17338..ba2ce3318c 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 @@ -54,14 +56,12 @@ internal class MultiWalletCardStateConverter( imageResId = imageResId, onRenameClick = onRenameClick, onDeleteClick = onDeleteClick, - 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 83dd7c2422..6f850012d0 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 @@ -65,17 +68,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 From 68e752b57041f288df58ea5863def63968a55c4f Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 20 Feb 2025 11:03:22 +0000 Subject: [PATCH 13/13] Updated on 2026-08-14 --- gradle/tangem_dependencies.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index d49f7f2f1d..967701e091 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -1,7 +1,7 @@ [versions] -tangemBlockchainSdk = "release-app_5.21-955" +tangemBlockchainSdk = "develop-957" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_5.21-435" +tangemCardSdk = "develop-434" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem-developments8" #tangemVico = "0.0.1" # Keep it! - used for local builds ^