Updated on 2026-08-14
This commit is contained in:
commit
fde7ec8688
99 changed files with 1279 additions and 550 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ data class SeedPhraseNotificationDTO(val status: Status) {
|
|||
@Json(name = "confirmed")
|
||||
CONFIRMED,
|
||||
|
||||
@Json(name = "rejected")
|
||||
REJECTED,
|
||||
|
||||
@Json(name = "accepted")
|
||||
ACCEPTED,
|
||||
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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(
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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>>
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 don’t 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. We’re 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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
package com.tangem.utils
|
||||
|
||||
const val H24_MILLIS = 24L * 60 * 60 * 1000
|
||||
const val WEEK_MILLIS = 7L * H24_MILLIS
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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") },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ plugins {
|
|||
|
||||
dependencies {
|
||||
implementation(projects.domain.core)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
implementation(deps.kotlin.serialization)
|
||||
implementation(deps.jodatime)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ internal class MockCurrenciesRepository(
|
|||
|
||||
override fun getAllWalletsCryptoCurrencies(
|
||||
currencyRawId: CryptoCurrency.RawID,
|
||||
needFilterByAvailable: Boolean,
|
||||
): Flow<Map<UserWallet, List<CryptoCurrency>>> {
|
||||
return emptyFlow()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.wallets.models
|
||||
|
||||
enum class SeedPhraseNotificationsStatus {
|
||||
|
||||
SHOW_FIRST,
|
||||
SHOW_SECOND,
|
||||
NOT_NEEDED,
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
),
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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" }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue