Updated on 2026-08-14

This commit is contained in:
Tangem 2025-02-20 16:03:34 +03:00
commit fde7ec8688
99 changed files with 1279 additions and 550 deletions

View file

@ -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 {

View file

@ -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"
}
}
}

View file

@ -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)

View file

@ -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
}
}
}
}

View file

@ -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)

View file

@ -123,6 +123,17 @@ interface TangemTechApi {
@Body body: SeedPhraseNotificationDTO,
): ApiResponse<Unit>
@GET("seedphrase-notification/{wallet_id}/confirmed")
suspend fun getSeedPhraseSecondNotificationStatus(
@Path("wallet_id") walletId: String,
): ApiResponse<SeedPhraseNotificationDTO>
@PUT("seedphrase-notification/{wallet_id}/confirmed")
suspend fun updateSeedPhraseSecondNotificationStatus(
@Path("wallet_id") walletId: String,
@Body body: SeedPhraseNotificationDTO,
): ApiResponse<Unit>
@GET("hot_crypto")
suspend fun getHotCrypto(@Query("currency") currencyId: String): ApiResponse<HotCryptoResponse>

View file

@ -19,6 +19,12 @@ data class SeedPhraseNotificationDTO(val status: Status) {
@Json(name = "confirmed")
CONFIRMED,
@Json(name = "rejected")
REJECTED,
@Json(name = "accepted")
ACCEPTED,
;
}
}

View file

@ -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()

View file

@ -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<NetworkStatusDM>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "networks_statuses") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),

View file

@ -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<YieldBalanceWrapperDTO>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "yield_balances") },
scope = CoroutineScope(context = dispatchers.io + SupervisorJob()),
),
runtimeStore = RuntimeSharedStore(),
)
}

View file

@ -2,25 +2,25 @@ package com.tangem.datasource.local.network
import androidx.datastore.core.DataStore
import com.tangem.datasource.local.datastore.RuntimeDataStore
import com.tangem.datasource.local.network.entity.NetworkStatusesDM
import com.tangem.datasource.local.network.utils.toDataModel
import com.tangem.datasource.local.network.utils.toDomainModel
import com.tangem.datasource.local.network.converter.NetworkStatusConverter
import com.tangem.datasource.local.network.converter.NetworkStatusDataModelConverter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import com.tangem.utils.extensions.replaceBy
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.launch
private typealias NetworkStatusesByWalletId = Map<String, Set<NetworkStatusDM>>
internal class DefaultNetworksStatusesStore(
private val runtimeDataStore: RuntimeDataStore<Set<NetworkStatus>>,
private val persistenceDataStore: DataStore<NetworkStatusesDM>,
private val persistenceDataStore: DataStore<NetworkStatusesByWalletId>,
) : NetworksStatusesStore {
private val mutex = Mutex()
override fun get(key: UserWalletId): Flow<Set<NetworkStatus>> {
return runtimeDataStore.get(provideStringKey(key))
}
@ -33,17 +33,26 @@ internal class DefaultNetworksStatusesStore(
.firstOrNull { it.id == status.networkId }
?: return@mapNotNullTo null
status.toDomainModel(network)
NetworkStatusConverter(network = network, isCached = true).convert(value = status)
}
.orEmpty()
if (cachedStatuses.isNotEmpty()) {
send(cachedStatuses)
/**
* Required for storing cache data.
* This will help to recognize networks that are still uploaded.
*
* @see mergeStatuses
*/
storeAll(key = key, values = cachedStatuses)
}
runtimeDataStore.get(provideStringKey(key))
.onEach { runtimeStatuses ->
val mergedStatuses = mergeStatuses(
networks = networks,
cachedStatuses = cachedStatuses,
runtimeStatuses = runtimeStatuses,
)
@ -54,36 +63,20 @@ internal class DefaultNetworksStatusesStore(
}
override suspend fun getSyncOrNull(key: UserWalletId): Set<NetworkStatus>? {
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<NetworkStatus>) {
mutex.withLock {
val currentValues = getSyncOrNull(key) ?: emptySet()
val updatedValues = currentValues.toMutableSet()
override suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>) {
coroutineScope {
val updatedValues = getSyncOrNull(key).orEmpty()
.addOrReplace(items = values) { prev, new -> prev.network == new.network }
values.forEach { newValue ->
val isReplaced = updatedValues.replaceBy(newValue) {
it.network == newValue.network
}
if (!isReplaced) {
updatedValues.add(newValue)
}
}
runtimeDataStore.store(provideStringKey(key), updatedValues)
launch { runtimeDataStore.store(key = provideStringKey(key), value = updatedValues) }
launch { storeNetworkStatusInPersistence(userWalletId = key, statuses = updatedValues) }
}
}
@ -93,29 +86,48 @@ internal class DefaultNetworksStatusesStore(
* If a status with the same network is in both sets, the status from [runtimeStatuses] is used.
*/
private fun mergeStatuses(
networks: Set<Network>,
cachedStatuses: Set<NetworkStatus>,
runtimeStatuses: Set<NetworkStatus>,
): Set<NetworkStatus> {
val runtimeMap = runtimeStatuses.associateBy { it.network }
val mergedCached = cachedStatuses.filter { it.network !in runtimeMap.keys }
return networks.mapNotNullTo(hashSetOf()) { network ->
val runtimeStatus = runtimeStatuses.firstOrNull { it.network == network }
return mergedCached.toSet() + runtimeStatuses
if (runtimeStatus == null || runtimeStatus.value is NetworkStatus.Unreachable) {
getCachedStatusIfPossible(cachedStatuses = cachedStatuses, network = network)
?: runtimeStatus
} else {
runtimeStatus
}
}
}
private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, networkStatus: NetworkStatus) {
val status = networkStatus.value
val network = networkStatus.network
private fun getCachedStatusIfPossible(cachedStatuses: Set<NetworkStatus>, network: Network): NetworkStatus? {
val cached = cachedStatuses.firstOrNull { it.network == network } ?: return null
if (status !is NetworkStatus.Verified) return
val updatedCachedStatus = when (val status = cached.value) {
is NetworkStatus.NoAccount -> status.copy(source = StatusSource.ONLY_CACHE)
is NetworkStatus.Verified -> status.copy(source = StatusSource.ONLY_CACHE)
is NetworkStatus.Refreshing,
is NetworkStatus.Unreachable,
is NetworkStatus.MissedDerivation,
-> null
}
return if (updatedCachedStatus != null) {
cached.copy(value = updatedCachedStatus)
} else {
null
}
}
private suspend fun storeNetworkStatusInPersistence(userWalletId: UserWalletId, statuses: Set<NetworkStatus>) {
// Converter will return null if the network status is not supported
val newStatuses = NetworkStatusDataModelConverter.convertSet(input = statuses).filterNotNull().toSet()
persistenceDataStore.updateData { storedStatuses ->
val userWalletStatuses = storedStatuses[userWalletId.stringValue] ?: emptySet()
val updatedStatuses = userWalletStatuses.addOrReplace(status.toDataModel(network)) {
it.networkId == network.id
}
storedStatuses.toMutableMap().apply {
this[userWalletId.stringValue] = updatedStatuses
this[userWalletId.stringValue] = newStatuses
}
}
}

View file

@ -15,5 +15,5 @@ interface NetworksStatusesStore {
suspend fun store(key: UserWalletId, value: NetworkStatus)
suspend fun storeAll(key: UserWalletId, values: Collection<NetworkStatus>)
suspend fun storeAll(key: UserWalletId, values: Set<NetworkStatus>)
}

View file

@ -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<NetworkStatusDM.Address>] to [NetworkAddress] and vice versa
*
[REDACTED_AUTHOR]
*/
internal class NetworkAddressConverter(
private val selectedAddress: String,
) : TwoWayConverter<Set<NetworkStatusDM.Address>, NetworkAddress> {
override fun convert(value: Set<NetworkStatusDM.Address>): 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<NetworkStatusDM.Address> {
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)
}
}

View file

@ -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<String, BigDecimal>
private typealias AmountsDomainModel = Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>
/**
* Converter from [AmountsDataModel] to [AmountsDomainModel] and vice versa
*
[REDACTED_AUTHOR]
*/
internal object NetworkAmountsConverter : TwoWayConverter<AmountsDataModel, AmountsDomainModel> {
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
}
}
}
}

View file

@ -0,0 +1,47 @@
package com.tangem.datasource.local.network.converter
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkStatus
import com.tangem.utils.converter.Converter
/**
* Converter from [NetworkStatusDM] to [NetworkStatus]
*
* @property network network
* @property isCached flag that determines whether the status is a cache
*
[REDACTED_AUTHOR]
*/
internal class NetworkStatusConverter(
private val network: Network,
private val isCached: Boolean,
) : Converter<NetworkStatusDM, NetworkStatus> {
override fun convert(value: NetworkStatusDM): NetworkStatus {
val address = NetworkAddressConverter(selectedAddress = value.selectedAddress)
.convert(value = value.availableAddresses)
val status = when (value) {
is NetworkStatusDM.Verified -> {
NetworkStatus.Verified(
address = address,
amounts = NetworkAmountsConverter.convert(value = value.amounts),
pendingTransactions = mapOf(),
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
}
is NetworkStatusDM.NoAccount -> {
NetworkStatus.NoAccount(
address = address,
amountToCreateAccount = value.amountToCreateAccount,
errorMessage = value.errorMessage,
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
}
}
return NetworkStatus(network = network, value = status)
}
}

View file

@ -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<NetworkStatus, NetworkStatusDM?> {
override fun convert(value: NetworkStatus): NetworkStatusDM? {
return when (val status = value.value) {
is NetworkStatus.Verified -> {
NetworkStatusDM.Verified(
networkId = value.network.id,
selectedAddress = status.address.defaultAddress.value,
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
.convertBack(value = status.address),
amounts = NetworkAmountsConverter.convertBack(value = status.amounts),
)
}
is NetworkStatus.NoAccount -> {
NetworkStatusDM.NoAccount(
networkId = value.network.id,
selectedAddress = status.address.defaultAddress.value,
availableAddresses = NetworkAddressConverter(selectedAddress = status.address.defaultAddress.value)
.convertBack(value = status.address),
amountToCreateAccount = status.amountToCreateAccount,
errorMessage = status.errorMessage,
)
}
else -> null
}
}
}

View file

@ -1,27 +1,48 @@
package com.tangem.datasource.local.network.entity
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.domain.tokens.model.Network
import dev.onenowy.moshipolymorphicadapter.annotations.NameLabel
import java.math.BigDecimal
internal typealias NetworkStatusesDM = Map<String, Set<NetworkStatusDM>>
internal sealed interface NetworkStatusDM {
@JsonClass(generateAdapter = true)
internal data class NetworkStatusDM(
val networkId: Network.ID,
val selectedAddress: String,
val availableAddresses: Set<Address>,
val amounts: Map<String, BigDecimal>,
) {
val networkId: Network.ID
val selectedAddress: String
val availableAddresses: Set<Address>
@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<Address>,
@Json(name = "amounts") val amounts: Map<String, BigDecimal>,
) : 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<Address>,
@Json(name = "amount_to_create_account") val amountToCreateAccount: BigDecimal,
@Json(name = "error_message") val errorMessage: String,
) : NetworkStatusDM
@JsonClass(generateAdapter = true)
data class Address(
val value: String,
val type: Type,
@Json(name = "value") val value: String,
@Json(name = "type") val type: Type,
) {
@JsonClass(generateAdapter = false)
enum class Type {
Primary, Secondary,
@Json(name = "primary")
Primary,
@Json(name = "secondary")
Secondary,
}
}
}

View file

@ -1,80 +0,0 @@
package com.tangem.datasource.local.network.utils
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.datasource.local.network.entity.NetworkStatusDM
import com.tangem.domain.tokens.model.*
import timber.log.Timber
import java.math.BigDecimal
internal fun NetworkStatus.Verified.toDataModel(network: Network): NetworkStatusDM {
return NetworkStatusDM(
networkId = network.id,
selectedAddress = address.defaultAddress.value,
availableAddresses = address.availableAddresses
.map { address ->
NetworkStatusDM.Address(
value = address.value,
type = when (address.type) {
NetworkAddress.Address.Type.Primary -> NetworkStatusDM.Address.Type.Primary
NetworkAddress.Address.Type.Secondary -> NetworkStatusDM.Address.Type.Secondary
},
)
}
.toSet(),
amounts = amounts
.mapKeys { (id, _) -> id.value }
.mapNotNullValues { (_, amount) ->
when (amount) {
is CryptoCurrencyAmountStatus.Loaded -> amount.value
is CryptoCurrencyAmountStatus.NotFound -> null
}
},
)
}
internal fun NetworkStatusDM.toDomainModel(network: Network): NetworkStatus {
return NetworkStatus(
network = network,
value = NetworkStatus.Verified(
address = mapToDomainAddress(selectedAddress, availableAddresses),
amounts = mapToDomainAmounts(amounts),
pendingTransactions = mapOf(),
),
)
}
private fun mapToDomainAmounts(amounts: Map<String, BigDecimal>): Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus> {
return amounts
.mapKeys { CryptoCurrency.ID.fromValue(it.key) }
.mapValues { (_, amount) -> CryptoCurrencyAmountStatus.Loaded(amount) }
}
private fun mapToDomainAddress(
selectedAddress: String,
availableAddresses: Set<NetworkStatusDM.Address>,
): 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)
}

View file

@ -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<NetworkStatusesDM> {
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<NetworkStatusesDM>(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))
}
}
}

View file

@ -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

View file

@ -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<CryptoCurrency.RawID>) {
runtimeStore.update(default = emptySet()) { saved ->
val new = currenciesIds.map { Quote.Empty(it) }
(saved + new).distinctBy { it.rawCurrencyId }.toSet()
}
}
private suspend fun getCachedQuotes(currenciesIds: Set<CryptoCurrency.RawID>): Set<Quote.Value> {
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<Quote.Value>, runtimeQuotes: Set<Quote>): Set<Quote> {
return runtimeQuotes.map { runtimeQuote ->
if (runtimeQuote is Quote.Empty) {
cachedQuotes.firstOrNull { runtimeQuote.rawCurrencyId == it.rawCurrencyId } ?: runtimeQuote
} else {
runtimeQuote
private fun mergeQuotes(
currenciesIds: Set<CryptoCurrency.RawID>,
cachedQuotes: Set<Quote.Value>,
runtimeQuotes: Set<Quote>,
): Set<Quote> {
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<Quote.Value>, currencyId: CryptoCurrency.RawID): Quote {
return cachedStatuses.firstOrNull { it.rawCurrencyId == currencyId }
?.copy(source = StatusSource.ONLY_CACHE)
?: Quote.Empty(currencyId)
}
private suspend fun storeInRuntimeStore(response: QuotesResponse) {

View file

@ -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<CryptoCurrency.RawID>)
}

View file

@ -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,
)
}
}

View file

@ -2,52 +2,83 @@ package com.tangem.datasource.local.token
import androidx.datastore.core.DataStore
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.datasource.local.token.entity.YieldBalanceWrappersDTO
import com.tangem.datasource.local.datastore.RuntimeSharedStore
import com.tangem.datasource.local.token.converter.YieldBalanceConverter
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.utils.extensions.addOrReplace
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
internal typealias YieldBalanceWrappersDTO = Map<String, Set<YieldBalanceWrapperDTO>>
internal typealias YieldBalanceListByWalletId = Map<UserWalletId, Set<YieldBalance>>
/**
* Default implementation of [StakingBalanceStore]
*
* @property persistenceStore persistence store
* @property runtimeStore runtime store
*/
internal class DefaultStakingBalanceStore(
private val dataStore: DataStore<YieldBalanceWrappersDTO>,
private val persistenceStore: DataStore<YieldBalanceWrappersDTO>,
private val runtimeStore: RuntimeSharedStore<YieldBalanceListByWalletId>,
) : StakingBalanceStore {
override fun get(userWalletId: UserWalletId): Flow<Set<YieldBalanceWrapperDTO>> {
return dataStore.data.map { it[userWalletId.stringValue].orEmpty() }
}
override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalanceWrapperDTO>? {
return dataStore.data.firstOrNull()
?.get(userWalletId.stringValue)
}
override suspend fun store(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
dataStore.updateData { current ->
current.toMutableMap().apply {
this[userWalletId.stringValue] = items
override fun get(userWalletId: UserWalletId): Flow<Set<YieldBalance>> = 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<YieldBalance?> {
return get(userWalletId).map { balances ->
balances.getBalance(address = address, integrationId = integrationId)
}
}
override fun get(
userWalletId: UserWalletId,
address: String,
integrationId: String,
): Flow<YieldBalanceWrapperDTO?> {
return get(userWalletId)
.map { balances ->
balances.firstOrNull { it.integrationId == integrationId && it.addresses.address == address }
}
override suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>? {
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<YieldBalanceWrapperDTO>) {
coroutineScope {
launch {
storeInRuntimeStore(
userWalletId = userWalletId,
items = YieldBalanceConverter(isCached = false).convertSet(input = items),
)
}
launch { storeInPersistenceStore(userWalletId = userWalletId, items = items) }
}
}
override suspend fun store(
@ -56,10 +87,94 @@ internal class DefaultStakingBalanceStore(
address: String,
item: YieldBalanceWrapperDTO,
) {
val balances = getSyncOrNull(userWalletId)
?.addOrReplace(item) { it.integrationId == integrationId && it.addresses.address == address }
?: setOf(item)
coroutineScope {
launch {
storeInRuntimeStore(userWalletId, integrationId, address, item)
storeInPersistenceStore(userWalletId, integrationId, address, item)
}
}
}
store(userWalletId, balances)
private suspend fun storeInRuntimeStore(
userWalletId: UserWalletId,
integrationId: String,
address: String,
item: YieldBalanceWrapperDTO,
) {
val newBalance = YieldBalanceConverter(isCached = false).convert(value = item)
val balances = getSyncOrNull(userWalletId)
?.addOrReplace(newBalance) { it.integrationId == integrationId && it.address == address }
?: setOf(newBalance)
storeInRuntimeStore(userWalletId = userWalletId, items = balances)
}
private suspend fun storeInRuntimeStore(userWalletId: UserWalletId, items: Set<YieldBalance>) {
runtimeStore.update(default = emptyMap()) {
it.toMutableMap().apply {
this[userWalletId] = items
}
}
}
private suspend fun storeInPersistenceStore(userWalletId: UserWalletId, items: Set<YieldBalanceWrapperDTO>) {
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<YieldBalance>,
runtimeBalances: Set<YieldBalance>,
): Set<YieldBalance> {
return runtimeBalances
.map { runtime ->
runtime.takeIf { runtime !is YieldBalance.Error }
?: getCachedBalanceIfPossible(cachedBalances, runtime)
}
.toSet()
}
private fun getCachedBalanceIfPossible(cachedBalances: Set<YieldBalance>, 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<YieldBalance>.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
}
}
}

View file

@ -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<Set<YieldBalanceWrapperDTO>>
/** Get flow of [YieldBalanceList] by [userWalletId] */
fun get(userWalletId: UserWalletId): Flow<Set<YieldBalance>>
suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalanceWrapperDTO>?
/** Get flow of [YieldBalance] by [userWalletId], [address] and [integrationId] */
fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow<YieldBalance?>
/** Get [YieldBalanceList] synchronously or null by [userWalletId] */
suspend fun getSyncOrNull(userWalletId: UserWalletId): Set<YieldBalance>?
/** 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<YieldBalanceWrapperDTO>)
fun get(userWalletId: UserWalletId, address: String, integrationId: String): Flow<YieldBalanceWrapperDTO?>
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)
}

View file

@ -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<BalanceTypeDTO, BalanceType> {
internal object BalanceTypeConverter : Converter<BalanceTypeDTO, BalanceType> {
override fun convert(value: BalanceTypeDTO): BalanceType {
return when (value) {

View file

@ -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<BalanceDTO.PendingAction, PendingAction> {
private val stakingActionTypeConverter by lazy(LazyThreadSafetyMode.NONE) { StakingActionTypeConverter() }
internal object PendingActionConverter : Converter<BalanceDTO.PendingAction, PendingAction> {
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(

View file

@ -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<StakingActionTypeDTO, StakingActionType> {
object StakingActionTypeConverter : Converter<StakingActionTypeDTO, StakingActionType> {
override fun convert(value: StakingActionTypeDTO): StakingActionType {
return when (value) {

View file

@ -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<NetworkTypeDTO, NetworkType> {
object StakingNetworkTypeConverter : TwoWayConverter<NetworkTypeDTO, NetworkType> {
override fun convert(value: NetworkTypeDTO): NetworkType {
return when (value) {

View file

@ -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<TokenDTO, Token> {
object TokenConverter : TwoWayConverter<TokenDTO, Token> {
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,

View file

@ -1,36 +1,37 @@
package com.tangem.data.staking.converters
package com.tangem.datasource.local.token.converter
import com.tangem.data.staking.converters.action.PendingActionConverter
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.BalanceItem
import com.tangem.domain.staking.model.stakekit.YieldBalance
import com.tangem.domain.staking.model.stakekit.YieldBalanceItem
import com.tangem.utils.converter.Converter
internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBalance> {
internal class YieldBalanceConverter(private val isCached: Boolean) : Converter<YieldBalanceWrapperDTO, YieldBalance> {
private val pendingActionConverter by lazy(LazyThreadSafetyMode.NONE) { PendingActionConverter() }
private val networkTypeConverter by lazy(LazyThreadSafetyMode.NONE) { StakingNetworkTypeConverter() }
private val tokenConverter by lazy(LazyThreadSafetyMode.NONE) { TokenConverter(networkTypeConverter) }
private val balanceTypeConverter by lazy(LazyThreadSafetyMode.NONE) { BalanceTypeConverter() }
override fun convert(value: YieldBalanceWrapperDTO): YieldBalance {
return if (value.balances.isEmpty()) {
YieldBalance.Empty
YieldBalance.Empty(
integrationId = value.integrationId,
address = value.addresses.address,
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
} else {
YieldBalance.Data(
integrationId = value.integrationId,
address = value.addresses.address,
balance = YieldBalanceItem(
items = value.balances.map { item ->
BalanceItem(
groupId = item.groupId,
token = tokenConverter.convert(item.tokenDTO),
type = balanceTypeConverter.convert(item.type),
token = TokenConverter.convert(item.tokenDTO),
type = BalanceTypeConverter.convert(item.type),
amount = item.amount,
rawCurrencyId = item.tokenDTO.coinGeckoId,
// tron-specific. operates validatorAddresses instead of validatorAddress
validatorAddress = item.validatorAddress ?: item.validatorAddresses?.get(0),
date = item.date?.toDateTime(),
pendingActions = pendingActionConverter
pendingActions = PendingActionConverter
.convertList(item.pendingActions)
.sortedBy { it.passthrough },
isPending = false,
@ -39,6 +40,7 @@ internal class YieldBalanceConverter : Converter<YieldBalanceWrapperDTO, YieldBa
.sortedWith(compareBy({ it.type }, { it.amount })),
integrationId = value.integrationId,
),
source = if (isCached) StatusSource.CACHE else StatusSource.ACTUAL,
)
}
}

View file

@ -1,5 +0,0 @@
package com.tangem.datasource.local.token.entity
import com.tangem.datasource.api.stakekit.models.response.model.YieldBalanceWrapperDTO
internal typealias YieldBalanceWrappersDTO = Map<String, Set<YieldBalanceWrapperDTO>>

View file

@ -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<YieldBalanceWrappersDTO> {
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<YieldBalanceWrappersDTO>(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))
}
}
}

View file

@ -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 <reified T> mapWithStringKeyTypes(): ParameterizedType {
return Types.newParameterizedType(Map::class.java, String::class.java, T::class.java)
}

View file

@ -881,13 +881,13 @@
<string name="swap_give_permission_fee_footer">Le réseau facturera des frais d\'approbation de jeton pour vérifier que vous autorisez l\'utilisation de votre jeton pour l\'échange.</string>
<string name="swap_promo_text">Échangez plus de jetons à de meilleurs taux directement dans votre portefeuille.</string>
<string name="swap_promo_title">Nouveau fournisseur d\'échange disponible !</string>
<string name="swap_story_fifth_subtitle">Ayez confiance en bénéficiant d\'un support constant, garantissant que vos transactions se déroulent sans problème à tout moment</string>
<string name="swap_story_fifth_subtitle">Ayez confiance en notre assistance 24 heures sur 24 pour vous aider à résoudre tous vos problèmes</string>
<string name="swap_story_fifth_title">Assistance 24 heures sur 24</string>
<string name="swap_story_first_subtitle">Les fournisseurs d\'échange de confiance vous permettent d\'échanger des actifs sans effort, en gardant tout en sécurité dans votre portefeuille</string>
<string name="swap_story_first_subtitle">Plusieurs fournisseurs de confiance en un seul endroit : échangez n\'importe quel actif facilement</string>
<string name="swap_story_first_title">Échangez Avec Nous</string>
<string name="swap_story_forth_subtitle">Une sécurité de haut niveau et des fournisseurs vérifiés garantissant que vos actifs sont protégés à chaque échange</string>
<string name="swap_story_forth_title">Plus Sûr Que Jamais</string>
<string name="swap_story_second_subtitle">Maximisez votre valeur avec des tarifs provenant d\'un large réseau de fournisseurs de confiance, en choisissant toujours le meilleur</string>
<string name="swap_story_second_subtitle">Maximisez votre valeur avec des tarifs d\'un large réseau de fournisseurs de confiance</string>
<string name="swap_story_second_title">Meilleurs Tarifs</string>
<string name="swap_story_third_subtitle">Simple et intuitif, vous permettant d\'échanger des jetons en quelques clics</string>
<string name="swap_story_third_title">Plus Simple Que Jamais</string>

View file

@ -871,16 +871,16 @@
<string name="swap_give_permission_fee_footer">ネットワークは、あなたがトークンのスワップを承認していることを確認するために、トークン承認手数料を請求します。</string>
<string name="swap_promo_text">より多くのトークンをより良いレートで、ウォレット内にて直接交換します。</string>
<string name="swap_promo_title">新しいスワッププロバイダーが利用可能になりました!</string>
<string name="swap_story_fifth_subtitle">継続的なサポートで、いつでもスムーズに取引を行うことができます</string>
<string name="swap_story_fifth_title">24時間体制のサポート</string>
<string name="swap_story_first_subtitle">信頼できる交換プロバイダーは、ウォレット内の資産を安全に保管しながら、簡単に交換できるようにします。</string>
<string name="swap_story_first_title">当社の交換機能をご利用ください</string>
<string name="swap_story_forth_subtitle">トップレベルのセキュリティと認証済みプロバイダーが、スワップ時にユーザーの資産を確実に保護します。</string>
<string name="swap_story_forth_title">最高に安全</string>
<string name="swap_story_fifth_subtitle">24時間体制のサポートであらゆる問題に対応します。</string>
<string name="swap_story_fifth_title">いつもここに</string>
<string name="swap_story_first_subtitle">複数の信頼できるプロバイダーが一箇所に集結。ウォレット内で様々な暗号資産を簡単に交換できます。</string>
<string name="swap_story_first_title">ぜひスワップしてください</string>
<string name="swap_story_forth_subtitle">失敗も死角もありません。取引は常に保護されます。</string>
<string name="swap_story_forth_title">難攻不落の防御</string>
<string name="swap_story_second_subtitle">幅広いネットワークの中から、常に最適なプロバイダーと料金レートを選択します</string>
<string name="swap_story_second_title">ベストレート</string>
<string name="swap_story_second_title">破格のレート</string>
<string name="swap_story_third_subtitle">手間がかからず直感的に操作でき、数回タップするだけでトークンを交換できます。</string>
<string name="swap_story_third_title">最高に簡単</string>
<string name="swap_story_third_title">とにかく便利</string>
<string name="swapping_alert_cex_description">この金額には以下が含まれます:\n- サービスプロバイダーの手数料\n- 取引所からユーザーのアドレスに%s を送り返すためのネットワーク手数料。</string>
<string name="swapping_alert_cex_description_with_slippage">金額には以下が含まれます: \n • サービス プロバイダーの手数料\n • 取引所からユーザーのアドレスに%1$sを送金するためのネットワーク手数料。 \n\nプロバイダーのスリッページは最大%2$sです</string>
<string name="swapping_alert_dex_description">この金額には、サービスプロバイダーの手数料が含まれています。</string>

View file

@ -674,6 +674,8 @@
<string name="scan_card_settings_message">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.</string>
<string name="scan_card_settings_title">Get your Tangem ready!</string>
<string name="security_alert_title">Security Alert</string>
<string name="seed_warning_no">No, I did not</string>
<string name="seed_warning_yes">Yes, guide me</string>
<string name="selling_insufficient_balance_alert_message">You dont have enough funds in your balance to sell cryptocurrency. Please deposit the desired asset to proceed.</string>
<string name="selling_insufficient_balance_alert_title">Insufficient Balance</string>
<string name="selling_regional_restriction_alert_message">Selling cryptocurrency is unavailable in your region at the moment. Were actively working to bring this option to you soon—stay tuned!</string>
@ -1089,6 +1091,8 @@
<string name="warning_rate_app_title">Enjoying Tangem?</string>
<string name="warning_receive_blocked_hedera_token_association_required_message">You must associate your token before receiving tokens</string>
<string name="warning_rent_fee_title">Network rent fee required</string>
<string name="warning_seedphrase_action_required_title">Action required</string>
<string name="warning_seedphrase_contacted_support">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.</string>
<string name="warning_seedphrase_issue_answer_no">Thank you! All set! No further actions required.</string>
<string name="warning_seedphrase_issue_answer_yes">You will now be redirected to the official Tangem website. Please read and follow instructions there.</string>
<string name="warning_seedphrase_issue_message">Have you ever contacted the Tangem support team directly through this application?</string>

View file

@ -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,

View file

@ -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()

View file

@ -0,0 +1,4 @@
package com.tangem.utils
const val H24_MILLIS = 24L * 60 * 60 * 1000
const val WEEK_MILLIS = 7L * H24_MILLIS

View file

@ -64,4 +64,12 @@ inline fun <T> MutableList<T>.addOrReplace(item: T, predicate: (T) -> Boolean) {
if (!isReplaced) {
add(item)
}
}
fun <T> List<T>.filterIf(condition: Boolean, predicate: (T) -> Boolean): List<T> {
return if (condition) {
this.filter(predicate)
} else {
this
}
}

View file

@ -36,4 +36,16 @@ inline fun <T> Set<T>.addOrReplace(item: T, predicate: (T) -> Boolean): Set<T> {
}
return mutableList
}
inline fun <T> Set<T>.addOrReplace(items: Set<T>, predicate: (T, T) -> Boolean): Set<T> {
val updatedValues = this.toMutableSet()
items.forEach { newValue ->
val isReplaced = updatedValues.replaceBy(item = newValue, predicate = { predicate(it, newValue) })
if (!isReplaced) updatedValues.add(newValue)
}
return updatedValues
}

View file

@ -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)

View file

@ -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<CryptoCurrency>,
): Flow<YieldBalanceList> {
return stakingBalanceStore.get(userWalletId)
.map(yieldBalanceListConverter::convert)
.map(YieldBalanceListConverter::convert)
.flowOn(dispatchers.io)
}
@ -495,7 +478,7 @@ internal class DefaultStakingRepository(
): Flow<YieldBalanceList> = 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<CryptoCurrency>,
): 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<Yield> {
return yieldConverter.convertListIgnoreErrors(
return YieldConverter.convertListIgnoreErrors(
input = stakingYieldsStore.getSync(),
onError = { Timber.e("Error converting one of the items in enabled yields: $it") },
)

View file

@ -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<Set<YieldBalanceWrapperDTO>, YieldBalanceList> {
internal object YieldBalanceListConverter : Converter<Set<YieldBalance>, YieldBalanceList> {
override fun convert(value: Set<YieldBalanceWrapperDTO>): YieldBalanceList {
override fun convert(value: Set<YieldBalance>): YieldBalanceList {
return if (value.isEmpty()) {
YieldBalanceList.Empty
} else {
YieldBalanceList.Data(
balances = value.map(yieldBalanceConverter::convert),
)
YieldBalanceList.Data(balances = value.toList())
}
}
}

View file

@ -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<YieldDTO, Yield> {
internal object YieldConverter : Converter<YieldDTO, Yield> {
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",
)
}
}

View file

@ -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<ActionDTO, StakingAction> {
@ -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,

View file

@ -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<StakingGasEstimateDTO, StakingGasEstimate> {
internal object GasEstimateConverter : Converter<StakingGasEstimateDTO, StakingGasEstimate> {
override fun convert(value: StakingGasEstimateDTO): StakingGasEstimate {
return StakingGasEstimate(
amount = value.amount,
token = tokenConverter.convert(value.token),
token = TokenConverter.convert(value.token),
gasLimit = value.gasLimit,
)
}

View file

@ -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<StakingTransactionDTO, StakingTransaction> {
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,

View file

@ -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<Map<UserWallet, List<CryptoCurrency>>> {
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

View file

@ -120,8 +120,17 @@ internal class DefaultNetworksRepository(
refresh: Boolean,
) = coroutineScope {
if (refresh) {
val statusesToRefresh = networks.map { NetworkStatus(it, NetworkStatus.Refreshing) }
networksStatusesStore.storeAll(userWalletId, statusesToRefresh)
val statusesToRefresh = networksStatusesStore.getSyncOrNull(userWalletId)?.mapNotNull {
if (it.network in networks) {
it.copy(value = NetworkStatus.Refreshing)
} else {
null
}
}
if (statusesToRefresh != null) {
networksStatusesStore.storeAll(key = userWalletId, values = statusesToRefresh.toSet())
}
}
val currencies = getCurrencies(userWalletId, networks)

View file

@ -19,6 +19,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import timber.log.Timber
internal class DefaultQuotesRepository(
private val tangemTechApi: TangemTechApi,
@ -115,24 +116,26 @@ internal class DefaultQuotesRepository(
val replacementIdsResult = quotesUnsupportedCurrenciesAdapter.replaceUnsupportedCurrencies(
rawCurrenciesIds.map { it.value }.toSet(),
)
val response = safeApiCallWithTimeout(
safeApiCallWithTimeout(
call = {
val coinIds = replacementIdsResult.idsForRequest.joinToString(separator = ",")
tangemTechApi.getQuotes(appCurrencyId, coinIds).bind()
val response = tangemTechApi.getQuotes(appCurrencyId, coinIds).bind()
val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies(
response = response,
filteredIds = replacementIdsResult.idsFiltered,
)
quotesStore.store(updatedResponse)
},
onError = { error ->
cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) })
Timber.e(error)
throw error
cacheRegistry.invalidate(rawCurrenciesIds.map { getQuoteCacheKey(it) })
quotesStore.storeEmptyQuotes(currenciesIds = rawCurrenciesIds)
},
)
val updatedResponse = quotesUnsupportedCurrenciesAdapter.getResponseWithUnsupportedCurrencies(
response,
replacementIdsResult.idsFiltered,
)
quotesStore.store(updatedResponse)
}
private suspend fun filterExpiredCurrenciesIds(

View file

@ -1,5 +1,6 @@
package com.tangem.data.tokens.utils
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.*
import com.tangem.domain.txhistory.models.TxHistoryItem
import com.tangem.domain.walletmanager.model.Address
@ -26,6 +27,7 @@ internal class NetworkStatusFactory {
address = getNetworkAddress(result.selectedAddress, result.addresses),
amountToCreateAccount = result.amountToCreateAccount,
errorMessage = result.errorMessage,
source = StatusSource.ACTUAL,
)
is UpdateWalletManagerResult.Verified -> NetworkStatus.Verified(
address = getNetworkAddress(result.selectedAddress, result.addresses),
@ -34,6 +36,7 @@ internal class NetworkStatusFactory {
transactions = result.currentTransactions,
currencies = currencies,
),
source = StatusSource.ACTUAL,
)
},
)

View file

@ -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<UserWalletId, SeedPhraseNotificationsStatus>
internal class DefaultWalletsRepository(
private val appPreferencesStore: AppPreferencesStore,
private val tangemTechApi: TangemTechApi,
private val userWalletsStore: UserWalletsStore,
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<Map<UserWalletId, Boolean>>,
private val seedPhraseNotificationVisibilityStore: RuntimeStateStore<SeedPhraseNotificationsStatuses>,
private val dispatchers: CoroutineDispatcherProvider,
) : WalletsRepository {
@ -57,11 +62,16 @@ internal class DefaultWalletsRepository(
}
}
override fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow<Boolean> {
override fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow<SeedPhraseNotificationsStatus> {
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

View file

@ -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)

View file

@ -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<TokenMarketInfo.Network>,
): Set<TokenMarketInfo.Network> {
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()
}
}

View file

@ -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,
}

View file

@ -9,6 +9,7 @@ plugins {
dependencies {
implementation(projects.domain.core)
implementation(projects.domain.models)
implementation(deps.kotlin.serialization)
implementation(deps.jodatime)

View file

@ -1,14 +1,20 @@
package com.tangem.domain.staking.model.stakekit
import com.tangem.domain.models.StatusSource
import com.tangem.domain.staking.model.stakekit.action.StakingActionType
import org.joda.time.DateTime
import java.math.BigDecimal
sealed class YieldBalance {
abstract val integrationId: String?
abstract val address: String?
data class Data(
override val integrationId: String?,
override val address: String,
val balance: YieldBalanceItem,
val address: String,
val source: StatusSource,
) : YieldBalance() {
fun getTotalWithRewardsStakingBalance(): BigDecimal {
return balance.items.sumOf { it.amount }
@ -34,9 +40,13 @@ sealed class YieldBalance {
}
}
data object Empty : YieldBalance()
data class Empty(
override val integrationId: String?,
override val address: String,
val source: StatusSource,
) : YieldBalance()
data object Error : YieldBalance()
data class Error(override val integrationId: String?, override val address: String?) : YieldBalance()
}
data class YieldBalanceItem(

View file

@ -2,9 +2,7 @@ package com.tangem.domain.staking.model.stakekit
sealed class YieldBalanceList {
data class Data(
val balances: List<YieldBalance>,
) : YieldBalanceList() {
data class Data(val balances: List<YieldBalance>) : 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)
}
}

View file

@ -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)

View file

@ -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<TxHistoryItem>,
override val networkAddress: NetworkAddress,
val source: StatusSource,
) : Value(isError = false)
/**

View file

@ -1,5 +1,6 @@
package com.tangem.domain.tokens.model
import com.tangem.domain.models.StatusSource
import com.tangem.domain.txhistory.models.TxHistoryItem
import java.math.BigDecimal
@ -19,24 +20,33 @@ data class NetworkStatus(
*
* This sealed class includes different states like unreachable, missed derivation, verified, and no account.
*/
sealed class Value
sealed class Value {
abstract val source: StatusSource
}
/**
* Represents the state where the network is refreshing.
*/
data object Refreshing : Value()
data object Refreshing : Value() {
override val source: StatusSource = StatusSource.ACTUAL
}
/**
* Represents the state where the network is unreachable.
*
* @property address Network addresses.
*/
data class Unreachable(val address: NetworkAddress?) : Value()
data class Unreachable(val address: NetworkAddress?) : Value() {
override val source: StatusSource = StatusSource.ACTUAL
}
/**
* Represents the state where a derivation has been missed.
*/
data object MissedDerivation : Value()
data object MissedDerivation : Value() {
override val source: StatusSource = StatusSource.ACTUAL
}
/**
* Represents the verified state of the network, including the amounts associated with different cryptocurrencies
@ -45,12 +55,14 @@ data class NetworkStatus(
* @property address Network addresses.
* @property amounts A map containing the amounts associated with different cryptocurrencies within the network.
* @property pendingTransactions A map containing pending transactions associated with different cryptocurrencies
* @property source source of data
* within the network.
*/
data class Verified(
val address: NetworkAddress,
val amounts: Map<CryptoCurrency.ID, CryptoCurrencyAmountStatus>,
val pendingTransactions: Map<CryptoCurrency.ID, Set<TxHistoryItem>>,
override val source: StatusSource,
) : Value()
/**
@ -59,10 +71,12 @@ data class NetworkStatus(
* @property address Network addresses.
* @property amountToCreateAccount The amount required to create an account within the network.
* @property errorMessage error message
* @property source source of data
*/
data class NoAccount(
val address: NetworkAddress,
val amountToCreateAccount: BigDecimal,
val errorMessage: String,
override val source: StatusSource,
) : Value()
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -42,8 +42,9 @@ class GetAllWalletsCryptoCurrencyStatusesUseCase(
@OptIn(ExperimentalCoroutinesApi::class)
operator fun invoke(
currencyRawId: CryptoCurrency.RawID,
needFilterByAvailable: Boolean = false,
): Flow<Map<UserWallet, List<Either<CurrencyStatusError, CryptoCurrencyStatus>>>> {
return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId)
return currenciesRepository.getAllWalletsCryptoCurrencies(currencyRawId, needFilterByAvailable)
.flatMapLatest { userWalletsWithCurrencies: Map<UserWallet, List<CryptoCurrency>> ->
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)
}

View file

@ -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,
)
}

View file

@ -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>): StatusSource {
return when {
sources.any { it == StatusSource.ONLY_CACHE } -> StatusSource.ONLY_CACHE
sources.any { it == StatusSource.CACHE } -> StatusSource.CACHE
else -> StatusSource.ACTUAL
}
}
}

View file

@ -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,
)
}
}

View file

@ -248,7 +248,10 @@ interface CurrenciesRepository {
): CryptoCurrency.Token
/** Get crypto currencies by [currencyRawId] from all user wallets */
fun getAllWalletsCryptoCurrencies(currencyRawId: CryptoCurrency.RawID): Flow<Map<UserWallet, List<CryptoCurrency>>>
fun getAllWalletsCryptoCurrencies(
currencyRawId: CryptoCurrency.RawID,
needFilterByAvailable: Boolean,
): Flow<Map<UserWallet, List<CryptoCurrency>>>
fun isNetworkFeeZero(userWalletId: UserWalletId, network: Network): Boolean
}

View file

@ -2,13 +2,13 @@ package com.tangem.domain.tokens.mock
import arrow.core.NonEmptySet
import arrow.core.nonEmptySetOf
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.CryptoCurrencyAmountStatus
import com.tangem.domain.tokens.model.Network
import com.tangem.domain.tokens.model.NetworkAddress
import com.tangem.domain.tokens.model.NetworkStatus
import java.math.BigDecimal
@Suppress("MemberVisibilityCanBePrivate")
internal object MockNetworks {
val amountToCreateAccount: BigDecimal = BigDecimal.TEN
@ -52,17 +52,24 @@ internal object MockNetworks {
transactionExtrasType = Network.TransactionExtrasType.NONE,
)
val networkStatus1 = NetworkStatus(
val verifiedNetworksStatuses: NonEmptySet<NetworkStatus>
get() = nonEmptySetOf(
verifiedNetworkStatus1,
verifiedNetworkStatus2,
verifiedNetworkStatus3,
)
private val networkStatus1 = NetworkStatus(
network = network1,
value = NetworkStatus.Unreachable(address = null),
)
val networkStatus2 = NetworkStatus(
private val networkStatus2 = NetworkStatus(
network = network2,
value = NetworkStatus.MissedDerivation,
)
val networkStatus3 = NetworkStatus(
private val networkStatus3 = NetworkStatus(
network = network3,
value = NetworkStatus.NoAccount(
amountToCreateAccount = amountToCreateAccount,
@ -70,12 +77,11 @@ internal object MockNetworks {
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
errorMessage = "",
source = StatusSource.ACTUAL,
),
)
val errorNetworksStatuses = nonEmptySetOf(networkStatus1, networkStatus2, networkStatus3)
val verifiedNetworkStatus1: NetworkStatus
private val verifiedNetworkStatus1: NetworkStatus
get() = networkStatus1.copy(
value = NetworkStatus.Verified(
amounts = mapOf(
@ -87,10 +93,11 @@ internal object MockNetworks {
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
source = StatusSource.ACTUAL,
),
)
val verifiedNetworkStatus2: NetworkStatus
private val verifiedNetworkStatus2: NetworkStatus
get() = networkStatus2.copy(
value = NetworkStatus.Verified(
amounts = mapOf(
@ -102,10 +109,11 @@ internal object MockNetworks {
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
source = StatusSource.ACTUAL,
),
)
val verifiedNetworkStatus3: NetworkStatus
private val verifiedNetworkStatus3: NetworkStatus
get() = networkStatus3.copy(
value = NetworkStatus.Verified(
amounts = mapOf(
@ -118,13 +126,7 @@ internal object MockNetworks {
address = NetworkAddress.Single(
defaultAddress = NetworkAddress.Address(value = "mock", NetworkAddress.Address.Type.Primary),
),
source = StatusSource.ACTUAL,
),
)
val verifiedNetworksStatuses: NonEmptySet<NetworkStatus>
get() = nonEmptySetOf(
verifiedNetworkStatus1,
verifiedNetworkStatus2,
verifiedNetworkStatus3,
)
}

View file

@ -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"))

View file

@ -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<CryptoCurrencyStatus> }
.sumOf { it.value.fiatAmount ?: BigDecimal.ZERO },
isAllAmountsSummarized = true,
source = StatusSource.ACTUAL,
),
)
}

View file

@ -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)

View file

@ -155,6 +155,7 @@ internal class MockCurrenciesRepository(
override fun getAllWalletsCryptoCurrencies(
currencyRawId: CryptoCurrency.RawID,
needFilterByAvailable: Boolean,
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
return emptyFlow()
}

View file

@ -138,13 +138,13 @@ class MockStakingRepository : StakingRepository {
userWalletId: UserWalletId,
cryptoCurrency: CryptoCurrency,
): Flow<YieldBalance> = 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<CryptoCurrency>,
): Flow<YieldBalanceList> {
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<CryptoCurrency>,
): YieldBalanceList = YieldBalanceList.Data(
balances = listOf(YieldBalance.Error),
balances = listOf(YieldBalance.Error(integrationId = null, address = null)),
)
override suspend fun createAction(

View file

@ -0,0 +1,8 @@
package com.tangem.domain.wallets.models
enum class SeedPhraseNotificationsStatus {
SHOW_FIRST,
SHOW_SECOND,
NOT_NEEDED,
}

View file

@ -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<Boolean>
fun seedPhraseNotificationStatus(userWalletId: UserWalletId): Flow<SeedPhraseNotificationsStatus>
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)
}

View file

@ -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<Boolean> {
operator fun invoke(userWalletId: UserWalletId): Flow<SeedPhraseNotificationsStatus> {
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)
}
}

View file

@ -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")
}

View file

@ -65,7 +65,7 @@ internal class PortfolioDataLoader @Inject constructor(
private fun getAllWalletsCryptoCurrenciesData(
currencyRawId: CryptoCurrency.RawID,
): Flow<Map<UserWallet, List<PortfolioData.CryptoCurrencyData>>> {
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()
}

View file

@ -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),
)
},

View file

@ -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
}
}
}

View file

@ -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")
}
}

View file

@ -51,15 +51,17 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.NoteMigration -> MainScreen.NotePromo
is WalletNotification.SwapPromo -> TokenSwapPromoAnalyticsEvent.NoticePromotionBanner(
source = AnalyticsParam.ScreensSources.Main,
programName = TokenSwapPromoAnalyticsEvent.ProgramName.Empty, // Use it on new promo action
programName = ProgramName.Empty, // Use it on new promo action
)
is WalletNotification.UnlockWallets -> null // See [SelectedWalletAnalyticsSender]
is WalletNotification.Informational.NoAccount,
is WalletNotification.Warning.LowSignatures,
is WalletNotification.Warning.SomeNetworksUnreachable,
is WalletNotification.Warning.NetworksUnreachable,
is WalletNotification.UsedOutdatedData,
-> null
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport
is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond
}
}
}

View file

@ -4,11 +4,13 @@ import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.lce.Lce
import com.tangem.domain.demo.IsDemoCardUseCase
import com.tangem.domain.models.StatusSource
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenList
import com.tangem.domain.wallets.models.SeedPhraseNotificationsStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
@ -44,6 +46,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
flow4 = seedPhraseNotificationUseCase(userWalletId = userWallet.walletId),
) { maybeTokenList, isReadyToShowRating, isNeedToBackup, seedPhraseIssueStatus ->
buildList {
addUsedOutdatedDataNotification(maybeTokenList)
addCriticalNotifications(userWallet, seedPhraseIssueStatus, clickIntents)
addInformationalNotifications(cardTypesResolver, maybeTokenList, clickIntents)
@ -61,23 +65,28 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
}
}
private fun MutableList<WalletNotification>.addUsedOutdatedDataNotification(
maybeTokenList: Lce<TokenListError, TokenList>,
) {
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<WalletNotification>.addCriticalNotifications(
userWallet: UserWallet,
seedPhraseIssueStatus: Boolean,
seedPhraseIssueStatus: SeedPhraseNotificationsStatus,
clickIntents: WalletClickIntents,
) {
addIf(
element = WalletNotification.Critical.SeedPhraseNotification(
onDeclineClick = clickIntents::onSeedPhraseNotificationDecline,
onConfirmClick = clickIntents::onSeedPhraseNotificationConfirm,
),
condition = with(userWallet) {
val isDemo = isDemoCardUseCase(cardId = userWallet.cardId)
val isWalletWithSeedPhrase = scanResponse.cardTypesResolver.isWallet2() && userWallet.isImported
!isDemo && isWalletWithSeedPhrase && seedPhraseIssueStatus
},
)
addSeedNotificationIfNeeded(userWallet, seedPhraseIssueStatus, clickIntents)
val cardTypesResolver = userWallet.scanResponse.cardTypesResolver
addIf(
@ -103,6 +112,39 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
}
}
private fun MutableList<WalletNotification>.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<WalletNotification>.addInformationalNotifications(
cardTypesResolver: CardTypesResolver,
maybeTokenList: Lce<TokenListError, TokenList>,

View file

@ -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"),
),
}

View file

@ -51,7 +51,10 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
)
data class SeedPhraseNotification(val onDeclineClick: () -> Unit, val onConfirmClick: () -> Unit) : Critical(
data class SeedPhraseNotification(
val onDeclineClick: () -> Unit,
val onConfirmClick: () -> Unit,
) : Critical(
title = resourceReference(R.string.warning_seedphrase_issue_title),
subtitle = resourceReference(R.string.warning_seedphrase_issue_message),
buttonsState = NotificationConfig.ButtonsState.SecondaryPairButtonsConfig(
@ -61,6 +64,20 @@ sealed class WalletNotification(val config: NotificationConfig) {
onRightClick = onConfirmClick,
),
)
data class SeedPhraseSecondNotification(
val onDeclineClick: () -> Unit,
val onConfirmClick: () -> Unit,
) : Critical(
title = resourceReference(R.string.warning_seedphrase_action_required_title),
subtitle = resourceReference(R.string.warning_seedphrase_contacted_support),
buttonsState = NotificationConfig.ButtonsState.SecondaryPairButtonsConfig(
leftText = resourceReference(R.string.seed_warning_no),
onLeftClick = onDeclineClick,
rightText = resourceReference(R.string.seed_warning_yes),
onRightClick = onConfirmClick,
),
)
}
sealed class Warning(
@ -217,4 +234,11 @@ sealed class WalletNotification(val config: NotificationConfig) {
),
),
)
data object UsedOutdatedData : WalletNotification(
config = NotificationConfig(
subtitle = resourceReference(R.string.warning_some_token_balances_not_updated),
iconResId = R.drawable.ic_error_sync_24,
),
)
}

View file

@ -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

View file

@ -4,8 +4,8 @@ import arrow.core.Either
import arrow.core.getOrElse
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.visa.model.VisaCurrency
import com.tangem.domain.wallets.models.UserWallet
@ -76,17 +76,20 @@ internal class SetBalancesAndLimitsTransformer(
},
cardCount = userWallet.getCardsCount(),
isZeroBalance = visaCurrency.balances.available.isZero(),
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
isBalanceFlickering = false,
)
}
}
private fun createAdditionalInfo(visaCurrency: VisaCurrency): WalletAdditionalInfo {
val fiatAmount = BigDecimalFormatter.formatFiatAmount(
fiatAmount = visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) },
fiatCurrencyCode = visaCurrency.fiatCurrency.code,
fiatCurrencySymbol = visaCurrency.fiatCurrency.symbol,
)
val fiatAmount = visaCurrency.fiatRate?.let { visaCurrency.balances.available.multiply(it) }
.format {
fiat(
fiatCurrencyCode = visaCurrency.fiatCurrency.code,
fiatCurrencySymbol = visaCurrency.fiatCurrency.symbol,
)
}
val infoContent = stringReference(
value = buildString {
append(fiatAmount)

View file

@ -1,6 +1,7 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.tokens.error.TokenListError
@ -61,14 +62,12 @@ internal class SetTokenListErrorTransformer(
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet),
imageResId = imageResId,
dropDownItems = dropDownItems,
balance = BigDecimalFormatter.formatFiatAmount(
fiatAmount = BigDecimal.ZERO,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
balance = BigDecimal.ZERO.format {
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
},
cardCount = selectedWallet.getCardsCount(),
isZeroBalance = true,
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
isBalanceFlickering = false,
)
}
}

View file

@ -1,8 +1,10 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
@ -51,14 +53,12 @@ internal class MultiWalletCardStateConverter(
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = selectedWallet),
imageResId = imageResId,
dropDownItems = dropDownItems,
balance = BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatBalance.amount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
),
balance = fiatBalance.amount.format {
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
},
isZeroBalance = fiatBalance.amount.isZero(),
cardCount = selectedWallet.getCardsCount(),
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
isBalanceFlickering = fiatBalance.source == StatusSource.CACHE,
)
}
}

View file

@ -1,12 +1,15 @@
package com.tangem.feature.wallet.presentation.wallet.state.transformers.converter
import com.tangem.core.ui.utils.BigDecimalFormatter
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.common.util.getCardsCount
import com.tangem.domain.models.StatusSource
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
import com.tangem.utils.StringsSigns.DASH_SIGN
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.isZero
@ -62,17 +65,15 @@ internal class SingleWalletCardStateConverter(
balance = formatFiatAmount(status = status, appCurrency = appCurrency),
cardCount = selectedWallet.getCardsCount(),
isZeroBalance = status.fiatAmount?.isZero(),
isBalanceFlickering = false, // TODO: Implement in [REDACTED_JIRA]
isBalanceFlickering = (status as? CryptoCurrencyStatus.Loaded)?.source == StatusSource.CACHE,
)
}
private fun formatFiatAmount(status: CryptoCurrencyStatus.Value, appCurrency: AppCurrency): String {
val fiatAmount = status.fiatAmount ?: return BigDecimalFormatter.EMPTY_BALANCE_SIGN
val fiatAmount = status.fiatAmount ?: return DASH_SIGN
return BigDecimalFormatter.formatFiatAmount(
fiatAmount = fiatAmount,
fiatCurrencyCode = appCurrency.code,
fiatCurrencySymbol = appCurrency.symbol,
)
return fiatAmount.format {
fiat(fiatCurrencyCode = appCurrency.code, fiatCurrencySymbol = appCurrency.symbol)
}
}
}

View file

@ -266,21 +266,21 @@ private fun TitleText(text: String, modifier: Modifier = Modifier) {
@Composable
private fun Balance(state: WalletCardState, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
AnimatedContent(
targetState = state,
targetState = (state as? WalletCardState.Content)?.balance?.orMaskWithStars(isBalanceHidden).orEmpty(),
label = "Update the balance",
modifier = modifier,
transitionSpec = {
fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith
fadeOut(animationSpec = tween(durationMillis = 90))
},
) { walletCardState ->
when (walletCardState) {
) { balance ->
when (state) {
is WalletCardState.Content -> {
ResizableText(
modifier = Modifier
.defaultMinSize(minHeight = TangemTheme.dimens.size32)
.flicker(isFlickering = walletCardState.isBalanceFlickering),
text = walletCardState.balance.orMaskWithStars(isBalanceHidden),
.flicker(isFlickering = state.isBalanceFlickering),
text = balance,
fontSizeRange = FontSizeRange(min = 16.sp, max = TangemTheme.typography.h2.fontSize),
color = TangemTheme.colors.text.primary1,
overflow = TextOverflow.Ellipsis,

View file

@ -43,6 +43,7 @@ internal fun LazyListScope.notifications(configs: ImmutableList<WalletNotificati
is WalletNotification.Informational -> 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
},
)

View file

@ -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 {

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -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" }