Updated on 2026-08-14
This commit is contained in:
parent
4c12570008
commit
97f41bd800
38 changed files with 768 additions and 163 deletions
|
|
@ -4,7 +4,7 @@ import com.tangem.domain.account.status.usecase.ManageCryptoCurrenciesUseCase
|
|||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase
|
||||
import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase
|
||||
import com.tangem.domain.tokensync.usecase.SyncTokensUseCase
|
||||
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
|
||||
import com.tangem.utils.coroutines.AppCoroutineScope
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
|
@ -36,12 +36,12 @@ internal object TokenSyncDomainModule {
|
|||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncTokensUseCase(
|
||||
fun provideStartTokenSyncUseCase(
|
||||
tokenSyncRepository: TokenSyncRepository,
|
||||
manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
appCoroutineScope: AppCoroutineScope,
|
||||
): SyncTokensUseCase {
|
||||
return SyncTokensUseCase(
|
||||
): StartTokenSyncUseCase {
|
||||
return StartTokenSyncUseCase(
|
||||
tokenSyncRepository = tokenSyncRepository,
|
||||
manageCryptoCurrenciesUseCase = manageCryptoCurrenciesUseCase,
|
||||
appCoroutineScope = appCoroutineScope,
|
||||
|
|
|
|||
|
|
@ -138,7 +138,6 @@ internal class ChildFactory @Inject constructor(
|
|||
AppRoute.ManageTokens.Source.SETTINGS -> ManageTokensSource.SETTINGS
|
||||
AppRoute.ManageTokens.Source.STORIES -> ManageTokensSource.STORIES
|
||||
AppRoute.ManageTokens.Source.ACCOUNT -> ManageTokensSource.ACCOUNT
|
||||
AppRoute.ManageTokens.Source.TOKEN_SYNC_BANNER -> ManageTokensSource.TOKEN_SYNC_BANNER
|
||||
}
|
||||
|
||||
val mode = route.accountId?.let { ManageTokensMode.Account(it) } ?: ManageTokensMode.None
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@ sealed class AppRoute(val path: String) : Route {
|
|||
STORIES,
|
||||
SETTINGS,
|
||||
ACCOUNT,
|
||||
TOKEN_SYNC_BANNER,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,14 +11,24 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.domain.tokensync)
|
||||
implementation(projects.domain.tokens)
|
||||
implementation(projects.domain.tokens.models)
|
||||
implementation(projects.domain.models)
|
||||
implementation(projects.domain.walletManager)
|
||||
implementation(projects.domain.wallets)
|
||||
implementation(projects.data.common)
|
||||
implementation(projects.libs.blockchainSdk)
|
||||
implementation(projects.core.datasource)
|
||||
implementation(projects.core.utils)
|
||||
implementation(projects.domain.models)
|
||||
|
||||
implementation(tangemDeps.blockchain)
|
||||
|
||||
implementation(deps.androidx.datastore)
|
||||
|
||||
implementation(deps.hilt.android)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
implementation(deps.androidx.datastore)
|
||||
implementation(deps.kotlin.coroutines)
|
||||
implementation(deps.moshi)
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
package com.tangem.data.tokensync.di
|
||||
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.tokensync.repository.DefaultTokenSyncRepository
|
||||
import com.tangem.data.tokensync.store.TokenSyncStoreFactory
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object TokenSyncDataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideTokenSyncRepository(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
tangemTechApi: TangemTechApi,
|
||||
userWalletsListRepository: UserWalletsListRepository,
|
||||
networkFactory: NetworkFactory,
|
||||
appPreferencesStore: AppPreferencesStore,
|
||||
tokenSyncStoreFactory: TokenSyncStoreFactory,
|
||||
responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
excludedBlockchains: ExcludedBlockchains,
|
||||
): TokenSyncRepository {
|
||||
return DefaultTokenSyncRepository(
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
tangemTechApi = tangemTechApi,
|
||||
userWalletsListRepository = userWalletsListRepository,
|
||||
networkFactory = networkFactory,
|
||||
appPreferencesStore = appPreferencesStore,
|
||||
tokenSyncStoreFactory = tokenSyncStoreFactory,
|
||||
responseCryptoCurrenciesFactory = responseCryptoCurrenciesFactory,
|
||||
dispatchers = dispatchers,
|
||||
excludedBlockchains = excludedBlockchains,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,346 @@
|
|||
package com.tangem.data.tokensync.repository
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.tokenbalance.models.TokenBalance
|
||||
import com.tangem.blockchainsdk.utils.ExcludedBlockchains
|
||||
import com.tangem.data.common.currency.ResponseCryptoCurrenciesFactory
|
||||
import com.tangem.data.common.network.NetworkFactory
|
||||
import com.tangem.data.tokensync.store.TokenSyncStore
|
||||
import com.tangem.data.tokensync.store.TokenSyncStoreFactory
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
|
||||
import com.tangem.datasource.local.preferences.AppPreferencesStore
|
||||
import com.tangem.datasource.local.preferences.PreferencesKeys
|
||||
import com.tangem.datasource.local.preferences.utils.getObjectMapSync
|
||||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.common.wallets.getSyncStrict
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.network.Network
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokensync.model.TokenSyncProgress
|
||||
import com.tangem.domain.tokensync.repository.TokenSyncRepository
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.logging.TangemLogger
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
internal class DefaultTokenSyncRepository(
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val networkFactory: NetworkFactory,
|
||||
private val appPreferencesStore: AppPreferencesStore,
|
||||
private val tokenSyncStoreFactory: TokenSyncStoreFactory,
|
||||
private val responseCryptoCurrenciesFactory: ResponseCryptoCurrenciesFactory,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val excludedBlockchains: ExcludedBlockchains,
|
||||
) : TokenSyncRepository {
|
||||
|
||||
private val semaphore = Semaphore(MAX_CONCURRENT_REQUESTS)
|
||||
private val progressStates = ConcurrentHashMap<String, MutableStateFlow<TokenSyncProgress>>()
|
||||
|
||||
override fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress> {
|
||||
return getProgressFlow(userWalletId)
|
||||
}
|
||||
|
||||
override fun acknowledgeCompletion(userWalletId: UserWalletId) {
|
||||
val key = userWalletId.stringValue
|
||||
val stateFlow = progressStates[key] ?: return
|
||||
stateFlow.value = TokenSyncProgress.Idle
|
||||
progressStates.remove(key, stateFlow)
|
||||
}
|
||||
|
||||
override suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency> {
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
val storedTokens = tokenSyncStore.get()
|
||||
if (storedTokens.isEmpty()) return emptyList()
|
||||
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
return responseCryptoCurrenciesFactory.createCurrencies(
|
||||
tokens = storedTokens,
|
||||
userWallet = userWallet,
|
||||
accountIndex = DerivationIndex.Main,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun clearDiscoveredTokens(userWalletId: UserWalletId) {
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
tokenSyncStore.clear()
|
||||
}
|
||||
|
||||
override suspend fun clearPendingFlag(userWalletId: UserWalletId) {
|
||||
setPendingFlag(userWalletId, value = false)
|
||||
}
|
||||
|
||||
override suspend fun getPendingSyncWalletIds(): List<UserWalletId> {
|
||||
val pendingMap = appPreferencesStore
|
||||
.getObjectMapSync<Boolean>(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY)
|
||||
return pendingMap
|
||||
.filter { it.value }
|
||||
.map { UserWalletId(it.key) }
|
||||
}
|
||||
|
||||
override suspend fun runSync(userWalletId: UserWalletId) {
|
||||
val networks = getSupportedNetworks(userWalletId)
|
||||
|
||||
if (networks.isEmpty()) return
|
||||
|
||||
setPendingFlag(userWalletId, value = true)
|
||||
val tokenSyncStore = tokenSyncStoreFactory.provide(userWalletId)
|
||||
tokenSyncStore.clear()
|
||||
|
||||
val batches = networks.chunked(MAX_CONCURRENT_REQUESTS)
|
||||
var completedNetworks = 0
|
||||
getProgressFlow(userWalletId).value = TokenSyncProgress.InProgress(
|
||||
completedNetworks = 0,
|
||||
totalNetworks = networks.size,
|
||||
)
|
||||
|
||||
for (batch in batches) {
|
||||
val batchResults = processBatch(userWalletId, batch)
|
||||
completedNetworks = handleBatchResults(
|
||||
userWalletId = userWalletId,
|
||||
results = batchResults,
|
||||
tokenSyncStore = tokenSyncStore,
|
||||
completedNetworks = completedNetworks,
|
||||
totalNetworks = networks.size,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun completeSync(userWalletId: UserWalletId) {
|
||||
setPendingFlag(userWalletId, value = false)
|
||||
getProgressFlow(userWalletId).value = TokenSyncProgress.Completed
|
||||
}
|
||||
|
||||
private suspend fun processBatch(userWalletId: UserWalletId, batch: List<Network>): List<NetworkResult> {
|
||||
return coroutineScope {
|
||||
batch.map { network ->
|
||||
async(dispatchers.io) {
|
||||
semaphore.withPermit {
|
||||
processNetwork(userWalletId, network)
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleBatchResults(
|
||||
userWalletId: UserWalletId,
|
||||
results: List<NetworkResult>,
|
||||
tokenSyncStore: TokenSyncStore,
|
||||
completedNetworks: Int,
|
||||
totalNetworks: Int,
|
||||
): Int {
|
||||
var completed = completedNetworks
|
||||
val progressFlow = getProgressFlow(userWalletId)
|
||||
|
||||
for (result in results) {
|
||||
completed++
|
||||
handleNetworkResult(result, tokenSyncStore)
|
||||
progressFlow.value = TokenSyncProgress.InProgress(
|
||||
completedNetworks = completed,
|
||||
totalNetworks = totalNetworks,
|
||||
)
|
||||
}
|
||||
|
||||
return completed
|
||||
}
|
||||
|
||||
private suspend fun handleNetworkResult(result: NetworkResult, tokenSyncStore: TokenSyncStore) {
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
if (result.responseTokens.isNotEmpty()) {
|
||||
try {
|
||||
tokenSyncStore.append(result.responseTokens)
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Failed to store discovered tokens for network: ${result.networkId}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
is NetworkResult.Error -> {
|
||||
TangemLogger.e("Token sync failed for network: ${result.networkId}", result.cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun processNetwork(userWalletId: UserWalletId, network: Network): NetworkResult {
|
||||
return try {
|
||||
val tokenBalances = fetchAndFilterTokenBalances(userWalletId, network)
|
||||
|
||||
if (tokenBalances.isEmpty()) {
|
||||
return NetworkResult.Success(
|
||||
networkId = network.backendId,
|
||||
responseTokens = emptyList(),
|
||||
)
|
||||
}
|
||||
|
||||
val enrichedTokens = enrichTokensWithCatalog(tokenBalances, network)
|
||||
|
||||
val responseTokens = enrichedTokens
|
||||
.filter { it.contractAddress != null }
|
||||
.map { it.toResponseToken() }
|
||||
|
||||
NetworkResult.Success(
|
||||
networkId = network.backendId,
|
||||
responseTokens = responseTokens,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
NetworkResult.Error(networkId = network.backendId, cause = e)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndFilterTokenBalances(userWalletId: UserWalletId, network: Network): List<TokenBalance> {
|
||||
return withContext(dispatchers.io) {
|
||||
walletManagersFacade.getTokenBalances(userWalletId, network)
|
||||
.filter { it.amount > BigDecimal.ZERO }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun enrichTokensWithCatalog(
|
||||
tokenBalances: List<TokenBalance>,
|
||||
network: Network,
|
||||
): List<DiscoveredToken> = withContext(dispatchers.io) {
|
||||
val tokensToEnrich = tokenBalances.filter { !it.isNativeToken }
|
||||
|
||||
val catalogMap = fetchCatalogInfo(
|
||||
networkId = network.backendId,
|
||||
contractAddresses = tokensToEnrich.mapNotNull(TokenBalance::contractAddress),
|
||||
)
|
||||
|
||||
tokenBalances.mapNotNull { balance ->
|
||||
if (balance.isNativeToken) return@mapNotNull null
|
||||
|
||||
val contractAddressLower = balance.contractAddress?.lowercase()
|
||||
val coin = contractAddressLower?.let { catalogMap[it] } ?: return@mapNotNull null
|
||||
val decimals = coin.networks
|
||||
.find { it.contractAddress?.lowercase() == contractAddressLower }
|
||||
?.decimalCount
|
||||
?.toInt()
|
||||
?: 0
|
||||
|
||||
DiscoveredToken(
|
||||
contractAddress = balance.contractAddress,
|
||||
symbol = coin.symbol,
|
||||
name = coin.name,
|
||||
decimals = decimals,
|
||||
amount = balance.amount,
|
||||
isNativeToken = false,
|
||||
currencyId = coin.id,
|
||||
networkId = network.backendId,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchCatalogInfo(
|
||||
networkId: String,
|
||||
contractAddresses: List<String>,
|
||||
): Map<String, CoinsResponse.Coin> {
|
||||
if (contractAddresses.isEmpty()) return emptyMap()
|
||||
|
||||
return try {
|
||||
val response = tangemTechApi.getCoins(
|
||||
networkId = networkId,
|
||||
contractAddresses = contractAddresses.joinToString(","),
|
||||
active = true,
|
||||
).getOrThrow()
|
||||
|
||||
buildMap {
|
||||
for (coin in response.coins) {
|
||||
for (network in coin.networks) {
|
||||
val address = network.contractAddress?.lowercase() ?: continue
|
||||
put(address, coin)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.w(
|
||||
"Failed to fetch catalog info for networkId=$networkId, addresses=${contractAddresses.size}",
|
||||
e,
|
||||
)
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSupportedNetworks(userWalletId: UserWalletId): List<Network> {
|
||||
val userWallet = userWalletsListRepository.getSyncStrict(userWalletId)
|
||||
|
||||
return Blockchain.entries
|
||||
.filter { !it.isTestnet() }
|
||||
.filter { it !in excludedBlockchains }
|
||||
.mapNotNull { blockchain ->
|
||||
networkFactory.create(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
userWallet = userWallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgressFlow(userWalletId: UserWalletId): MutableStateFlow<TokenSyncProgress> {
|
||||
return progressStates.getOrPut(userWalletId.stringValue) {
|
||||
MutableStateFlow(TokenSyncProgress.Idle)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun setPendingFlag(userWalletId: UserWalletId, value: Boolean) {
|
||||
appPreferencesStore.editData { prefs ->
|
||||
prefs.setObjectMap(
|
||||
key = PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY,
|
||||
value = prefs.getObjectMap<Boolean>(PreferencesKeys.PENDING_DISCOVERY_SYNC_KEY)
|
||||
.plus(userWalletId.stringValue to value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun DiscoveredToken.toResponseToken(): UserTokensResponse.Token {
|
||||
return UserTokensResponse.Token(
|
||||
id = currencyId,
|
||||
networkId = networkId,
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
contractAddress = contractAddress,
|
||||
)
|
||||
}
|
||||
|
||||
private data class DiscoveredToken(
|
||||
val contractAddress: String?,
|
||||
val symbol: String,
|
||||
val name: String,
|
||||
val decimals: Int,
|
||||
val amount: BigDecimal,
|
||||
val isNativeToken: Boolean,
|
||||
val currencyId: String?,
|
||||
val networkId: String,
|
||||
)
|
||||
|
||||
private sealed class NetworkResult {
|
||||
data class Success(
|
||||
val networkId: String,
|
||||
val responseTokens: List<UserTokensResponse.Token>,
|
||||
) : NetworkResult()
|
||||
|
||||
data class Error(
|
||||
val networkId: String,
|
||||
val cause: Throwable,
|
||||
) : NetworkResult()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_CONCURRENT_REQUESTS = 3
|
||||
}
|
||||
}
|
||||
|
|
@ -78,6 +78,33 @@ class ManageCryptoCurrenciesUseCase(
|
|||
add: List<CryptoCurrency> = emptyList(),
|
||||
remove: List<CryptoCurrency> = emptyList(),
|
||||
skipDerivationErrors: Boolean = true,
|
||||
): Either<Throwable, Unit> = invokeInternal(
|
||||
accountId = accountId,
|
||||
add = add,
|
||||
remove = remove,
|
||||
skipDerivationErrors = skipDerivationErrors,
|
||||
awaitTokensSyncFinished = false,
|
||||
)
|
||||
|
||||
suspend fun invokeAndAwait(
|
||||
accountId: AccountId,
|
||||
add: List<CryptoCurrency> = emptyList(),
|
||||
remove: List<CryptoCurrency> = emptyList(),
|
||||
skipDerivationErrors: Boolean = true,
|
||||
): Either<Throwable, Unit> = invokeInternal(
|
||||
accountId = accountId,
|
||||
add = add,
|
||||
remove = remove,
|
||||
skipDerivationErrors = skipDerivationErrors,
|
||||
awaitTokensSyncFinished = true,
|
||||
)
|
||||
|
||||
private suspend fun invokeInternal(
|
||||
accountId: AccountId,
|
||||
add: List<CryptoCurrency>,
|
||||
remove: List<CryptoCurrency>,
|
||||
skipDerivationErrors: Boolean,
|
||||
awaitTokensSyncFinished: Boolean,
|
||||
): Either<Throwable, Unit> = eitherOn(dispatchers.default) {
|
||||
if (add.isEmpty() && remove.isEmpty()) {
|
||||
TangemLogger.d("No currencies to add or remove, skipping")
|
||||
|
|
@ -111,9 +138,11 @@ class ManageCryptoCurrenciesUseCase(
|
|||
account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total),
|
||||
)
|
||||
|
||||
parallelUpdatingScope.launch {
|
||||
syncTokens(userWalletId, modifiedCurrencyList)
|
||||
|
||||
syncTokensAndLaunchUpdates(
|
||||
userWalletId = userWalletId,
|
||||
modifiedCurrencyList = modifiedCurrencyList,
|
||||
awaitSync = awaitTokensSyncFinished,
|
||||
) {
|
||||
cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = modifiedCurrencyList.added)
|
||||
refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total)
|
||||
clearMetadata(userWalletId = userWalletId, currencies = modifiedCurrencyList.removed)
|
||||
|
|
@ -129,6 +158,7 @@ class ManageCryptoCurrenciesUseCase(
|
|||
accountId: AccountId,
|
||||
networkId: String,
|
||||
contractAddress: String,
|
||||
awaitTokensSyncFinished: Boolean = false,
|
||||
): Either<Throwable, CryptoCurrency> = eitherOn(dispatchers.default) {
|
||||
val userWalletId = accountId.userWalletId
|
||||
|
||||
|
|
@ -152,9 +182,11 @@ class ManageCryptoCurrenciesUseCase(
|
|||
|
||||
saveAccount(account = accountStatus.account.copy(cryptoCurrencies = modifiedCurrencyList.total))
|
||||
|
||||
parallelUpdatingScope.launch {
|
||||
syncTokens(userWalletId, modifiedCurrencyList)
|
||||
|
||||
syncTokensAndLaunchUpdates(
|
||||
userWalletId = userWalletId,
|
||||
modifiedCurrencyList = modifiedCurrencyList,
|
||||
awaitSync = awaitTokensSyncFinished,
|
||||
) {
|
||||
cryptoCurrencyBalanceFetcher(userWalletId = userWalletId, currencies = listOf(tokenToAdd))
|
||||
refreshExpress(userWalletId = userWalletId, currencies = modifiedCurrencyList.total)
|
||||
}
|
||||
|
|
@ -285,6 +317,25 @@ class ManageCryptoCurrenciesUseCase(
|
|||
.onFailure { TangemLogger.e("Failed to sync tokens for wallet $userWalletId", it) }
|
||||
}
|
||||
|
||||
private suspend fun syncTokensAndLaunchUpdates(
|
||||
userWalletId: UserWalletId,
|
||||
modifiedCurrencyList: ModifiedCurrencyList,
|
||||
awaitSync: Boolean,
|
||||
updates: suspend () -> Unit,
|
||||
) {
|
||||
if (awaitSync) {
|
||||
syncTokens(userWalletId, modifiedCurrencyList)
|
||||
parallelUpdatingScope.launch {
|
||||
updates()
|
||||
}
|
||||
} else {
|
||||
parallelUpdatingScope.launch {
|
||||
syncTokens(userWalletId, modifiedCurrencyList)
|
||||
updates()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates wallet managers for the given [currencies] if they do not already exist.
|
||||
* The method will generate addresses for new networks to ensure the stability of the "Push notifications" feature.
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
package com.tangem.domain.tokens.model.tokensync
|
||||
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class DiscoveredToken(
|
||||
val contractAddress: String?,
|
||||
val symbol: String,
|
||||
val name: String,
|
||||
val decimals: Int,
|
||||
val amount: BigDecimal,
|
||||
val isNativeToken: Boolean,
|
||||
val currencyId: String?,
|
||||
val networkId: String,
|
||||
)
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
package com.tangem.domain.tokens.model.tokensync
|
||||
|
||||
sealed class TokenSyncProgress {
|
||||
|
||||
data object Idle : TokenSyncProgress()
|
||||
|
||||
data class InProgress(
|
||||
val completedNetworks: Int,
|
||||
val totalNetworks: Int,
|
||||
) : TokenSyncProgress() {
|
||||
val progressPercent: Int
|
||||
get() = if (totalNetworks > 0) {
|
||||
completedNetworks * 100 / totalNetworks
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
data object Completed : TokenSyncProgress()
|
||||
|
||||
data class Error(val cause: Throwable) : TokenSyncProgress()
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.domain.tokens.repository
|
||||
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.domain.tokens.model.tokensync.TokenSyncProgress
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface TokenSyncRepository {
|
||||
|
||||
suspend fun runSync(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getPendingSyncWalletIds(): List<UserWalletId>
|
||||
|
||||
fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress>
|
||||
|
||||
fun acknowledgeCompletion(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun clearPendingFlag(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getDiscoveredCurrencies(userWalletId: UserWalletId): List<CryptoCurrency>
|
||||
|
||||
suspend fun clearDiscoveredTokens(userWalletId: UserWalletId)
|
||||
}
|
||||
|
|
@ -17,6 +17,4 @@ sealed class TokenSyncProgress {
|
|||
}
|
||||
|
||||
data object Completed : TokenSyncProgress()
|
||||
|
||||
data class Error(val cause: Throwable) : TokenSyncProgress()
|
||||
}
|
||||
|
|
@ -9,6 +9,8 @@ interface TokenSyncRepository {
|
|||
|
||||
suspend fun runSync(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun completeSync(userWalletId: UserWalletId)
|
||||
|
||||
suspend fun getPendingSyncWalletIds(): List<UserWalletId>
|
||||
|
||||
fun observeSyncProgress(userWalletId: UserWalletId): Flow<TokenSyncProgress>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import kotlinx.coroutines.Job
|
|||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class SyncTokensUseCase(
|
||||
class StartTokenSyncUseCase(
|
||||
private val tokenSyncRepository: TokenSyncRepository,
|
||||
private val manageCryptoCurrenciesUseCase: ManageCryptoCurrenciesUseCase,
|
||||
private val appCoroutineScope: AppCoroutineScope,
|
||||
|
|
@ -25,6 +25,7 @@ class SyncTokensUseCase(
|
|||
try {
|
||||
tokenSyncRepository.runSync(userWalletId)
|
||||
applyDiscoveredTokens(userWalletId)
|
||||
tokenSyncRepository.completeSync(userWalletId)
|
||||
} catch (e: Exception) {
|
||||
TangemLogger.e("Token sync failed for wallet: $userWalletId", e)
|
||||
} finally {
|
||||
|
|
@ -61,7 +62,7 @@ class SyncTokensUseCase(
|
|||
if (currencies.isEmpty()) return true
|
||||
|
||||
val accountId = AccountId.forMainCryptoPortfolio(userWalletId)
|
||||
return manageCryptoCurrenciesUseCase(
|
||||
return manageCryptoCurrenciesUseCase.invokeAndAwait(
|
||||
accountId = accountId,
|
||||
add = currencies,
|
||||
).fold(
|
||||
|
|
@ -38,6 +38,7 @@ dependencies {
|
|||
implementation(projects.domain.feedback)
|
||||
implementation(projects.domain.feedback.models)
|
||||
implementation(projects.domain.hotWallet)
|
||||
implementation(projects.domain.tokensync)
|
||||
|
||||
/** Common */
|
||||
implementation(projects.common.ui)
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ import com.tangem.core.ui.extensions.wrappedList
|
|||
import com.tangem.domain.common.wallets.UserWalletsListRepository
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Attempts
|
||||
import com.tangem.domain.wallets.hot.HotWalletAccessCodeAttemptsRepository.Companion.MAX_FAST_FORWARD_ATTEMPTS
|
||||
import com.tangem.domain.wallets.hot.HotWalletPasswordRequester
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.hotwallet.accesscode.ACCESS_CODE_LENGTH
|
||||
import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
|
|
@ -30,12 +32,15 @@ import com.tangem.utils.logging.TangemLogger
|
|||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
@Suppress("LongParameterList")
|
||||
internal class HotAccessCodeRequestModel @Inject constructor(
|
||||
override val dispatchers: CoroutineDispatcherProvider,
|
||||
private val hotAccessCodeAttemptsRepository: HotWalletAccessCodeAttemptsRepository,
|
||||
private val userWalletsListRepository: UserWalletsListRepository,
|
||||
private val canUseBiometryUseCase: CanUseBiometryUseCase,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val startTokenSyncUseCase: StartTokenSyncUseCase,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val result = MutableStateFlow<HotWalletPasswordRequester.Result?>(null)
|
||||
|
|
@ -214,6 +219,11 @@ internal class HotAccessCodeRequestModel @Inject constructor(
|
|||
val currentRequest = currentRequest.value ?: return
|
||||
val userWallet = userWalletsListRepository.userWalletsSync()
|
||||
.firstOrNull { it is UserWallet.Hot && it.hotWalletId == currentRequest.hotWalletId } ?: return
|
||||
|
||||
if (hotWalletFeatureToggles.isTokenSyncEnabled) {
|
||||
startTokenSyncUseCase.cancel(userWallet.walletId)
|
||||
}
|
||||
|
||||
userWalletsListRepository.delete(listOf(userWallet.walletId))
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ import com.tangem.core.ui.message.bottomSheetMessage
|
|||
import com.tangem.crypto.bip39.Mnemonic
|
||||
import com.tangem.datasource.local.appsflyer.AppsFlyerStore
|
||||
import com.tangem.domain.common.wallets.error.SaveWalletError
|
||||
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
|
||||
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
|
||||
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.hotwallet.MnemonicRepository
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
|
||||
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
|
||||
|
|
@ -40,6 +42,8 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
private val tangemHotSdk: TangemHotSdk,
|
||||
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
|
||||
private val saveUserWalletUseCase: SaveWalletUseCase,
|
||||
private val startTokenSyncUseCase: StartTokenSyncUseCase,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
@GlobalUiMessageSender private val uiMessageSender: UiMessageSender,
|
||||
private val analyticsEventHandler: AnalyticsEventHandler,
|
||||
private val appsFlyerStore: AppsFlyerStore,
|
||||
|
|
@ -109,6 +113,11 @@ internal class AddExistingWalletImportModel @Inject constructor(
|
|||
}
|
||||
.onRight {
|
||||
setImportProgress(false)
|
||||
|
||||
if (hotWalletFeatureToggles.isTokenSyncEnabled) {
|
||||
startTokenSyncUseCase(userWallet.walletId)
|
||||
}
|
||||
|
||||
analyticsEventHandler.send(
|
||||
event = OnboardingAnalyticsEvent.Onboarding.Finished(
|
||||
source = AnalyticsParam.ScreensSources.ImportWallet.value,
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ import com.tangem.core.ui.extensions.resourceReference
|
|||
import com.tangem.core.ui.message.DialogMessage
|
||||
import com.tangem.core.ui.message.EventMessageAction
|
||||
import com.tangem.core.ui.message.SnackbarMessage
|
||||
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
|
||||
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
|
||||
import com.tangem.features.hotwallet.ForgetWalletComponent
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.hotwallet.forgetwallet.entity.ForgetWalletUM
|
||||
import com.tangem.features.hotwallet.impl.R
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -23,6 +25,7 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@ModelScoped
|
||||
internal class ForgetWalletModel @Inject constructor(
|
||||
paramsContainer: ParamsContainer,
|
||||
|
|
@ -30,6 +33,8 @@ internal class ForgetWalletModel @Inject constructor(
|
|||
private val router: Router,
|
||||
private val deleteWalletUseCase: DeleteWalletUseCase,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val startTokenSyncUseCase: StartTokenSyncUseCase,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
private val params = paramsContainer.require<ForgetWalletComponent.Params>()
|
||||
|
|
@ -79,6 +84,10 @@ internal class ForgetWalletModel @Inject constructor(
|
|||
|
||||
private fun forgetWallet() {
|
||||
modelScope.launch {
|
||||
if (hotWalletFeatureToggles.isTokenSyncEnabled) {
|
||||
startTokenSyncUseCase.cancel(params.userWalletId)
|
||||
}
|
||||
|
||||
val hasUserWallets = deleteWalletUseCase(params.userWalletId)
|
||||
.getOrElse { error ->
|
||||
TangemLogger.e("Unable to delete wallet: $error")
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ enum class ManageTokensSource(val analyticsName: String) {
|
|||
ONBOARDING(analyticsName = "Onboarding"),
|
||||
SETTINGS(analyticsName = "Wallet Settings"),
|
||||
ACCOUNT(analyticsName = "Account"),
|
||||
TOKEN_SYNC_BANNER(analyticsName = "Token Sync Banner"),
|
||||
SEND_VIA_SWAP(analyticsName = "SendViaSwap"),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ dependencies {
|
|||
implementation(projects.domain.settings)
|
||||
implementation(projects.domain.notifications.models)
|
||||
implementation(projects.domain.notifications)
|
||||
implementation(projects.domain.tokensync)
|
||||
|
||||
/* AndroidX */
|
||||
implementation(deps.androidx.fragment.ktx)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import com.tangem.domain.nft.EnableWalletNFTUseCase
|
|||
import com.tangem.domain.nft.GetWalletNFTEnabledUseCase
|
||||
import com.tangem.domain.notifications.repository.NotificationsRepository
|
||||
import com.tangem.domain.settings.repositories.PermissionRepository
|
||||
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
|
||||
import com.tangem.domain.wallets.analytics.Settings
|
||||
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents
|
||||
import com.tangem.domain.wallets.analytics.WalletSettingsAnalyticEvents.RecoveryPhraseScreenAction
|
||||
|
|
@ -48,6 +49,7 @@ import com.tangem.feature.walletsettings.utils.AccountItemsDelegate
|
|||
import com.tangem.feature.walletsettings.utils.AccountListSortingSaver
|
||||
import com.tangem.feature.walletsettings.utils.ItemsBuilder
|
||||
import com.tangem.feature.walletsettings.utils.WalletCardItemDelegate
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.features.pushnotifications.api.analytics.PushNotificationAnalyticEvents
|
||||
import com.tangem.hot.sdk.model.HotWalletId
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
|
|
@ -88,6 +90,8 @@ internal class WalletSettingsModel @Inject constructor(
|
|||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val accountListSortingSaver: AccountListSortingSaver,
|
||||
private val startTokenSyncUseCase: StartTokenSyncUseCase,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : Model() {
|
||||
|
||||
val params: WalletSettingsComponent.Params = paramsContainer.require()
|
||||
|
|
@ -249,6 +253,13 @@ internal class WalletSettingsModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun forgetWallet() = modelScope.launch {
|
||||
val userWallet = getUserWalletUseCase(params.userWalletId)
|
||||
.getOrNull()
|
||||
|
||||
if (userWallet is UserWallet.Hot && hotWalletFeatureToggles.isTokenSyncEnabled) {
|
||||
startTokenSyncUseCase.cancel(params.userWalletId)
|
||||
}
|
||||
|
||||
val hasUserWallets = deleteWalletUseCase(params.userWalletId).getOrElse { error ->
|
||||
TangemLogger.e("Unable to delete wallet: $error")
|
||||
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@ dependencies {
|
|||
implementation(projects.domain.yieldSupply.models)
|
||||
implementation(projects.domain.appTheme)
|
||||
implementation(projects.domain.appTheme.models)
|
||||
implementation(projects.domain.tokensync)
|
||||
|
||||
/** Feature Apis */
|
||||
implementation(projects.features.details.api)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import com.tangem.domain.settings.*
|
|||
import com.tangem.domain.tokens.RefreshMultiCurrencyWalletQuotesUseCase
|
||||
import com.tangem.domain.walletconnect.WcPairService
|
||||
import com.tangem.domain.walletconnect.model.WcPairRequest
|
||||
import com.tangem.domain.tokensync.usecase.StartTokenSyncUseCase
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.domain.wallets.usecase.*
|
||||
import com.tangem.domain.yield.supply.usecase.YieldSupplyApyUpdateUseCase
|
||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
|
|
@ -123,6 +125,8 @@ internal class WalletModel @Inject constructor(
|
|||
private val paymentAccountStatusFetcher: PaymentAccountStatusFetcher,
|
||||
private val tangemPayFeatureToggles: TangemPayFeatureToggles,
|
||||
private val uiMessageSender: UiMessageSender,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
private val startTokenSyncUseCase: StartTokenSyncUseCase,
|
||||
val screenLifecycleProvider: ScreenLifecycleProvider,
|
||||
val innerWalletRouter: InnerWalletRouter,
|
||||
) : Model() {
|
||||
|
|
@ -155,6 +159,7 @@ internal class WalletModel @Inject constructor(
|
|||
subscribeTangemPayOnWalletState()
|
||||
subscribeToMainScreenQrScanning()
|
||||
enableNotificationsIfNeeded()
|
||||
applyPendingTokenSyncs()
|
||||
|
||||
clickIntents.initialize(innerWalletRouter, modelScope)
|
||||
|
||||
|
|
@ -819,6 +824,12 @@ internal class WalletModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun applyPendingTokenSyncs() {
|
||||
if (hotWalletFeatureToggles.isTokenSyncEnabled) {
|
||||
startTokenSyncUseCase.applyPendingSyncs()
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableNotificationsIfNeeded() {
|
||||
modelScope.launch {
|
||||
val isUserAllowToEnableNotifications = notificationsRepository.isUserAllowToSubscribeOnPushNotifications()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
|
|||
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.hotwallet.CloseHotWalletUpgradeBannerUseCase
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.currency.CryptoCurrency
|
||||
import com.tangem.domain.models.currency.CryptoCurrencyStatus
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
|
|
@ -39,6 +40,7 @@ import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent
|
|||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.Program
|
||||
import com.tangem.domain.tokens.model.analytics.PromoAnalyticsEvent.PromotionBannerClicked
|
||||
import com.tangem.domain.tokens.model.details.NavigationAction
|
||||
import com.tangem.domain.tokensync.usecase.AcknowledgeTokenSyncCompletionUseCase
|
||||
import com.tangem.domain.wallets.usecase.*
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent
|
||||
import com.tangem.feature.wallet.presentation.wallet.analytics.WalletScreenAnalyticsEvent.Basic
|
||||
|
|
@ -94,6 +96,10 @@ internal interface WalletWarningsClickIntents {
|
|||
fun onUpgradeHotWalletClick(userWalletId: UserWalletId)
|
||||
|
||||
fun onCloseUpgradeBannerClick(userWalletId: UserWalletId)
|
||||
|
||||
fun onDismissTokenSyncNotification(userWalletId: UserWalletId)
|
||||
|
||||
fun onTokenSyncManageClick(userWalletId: UserWalletId)
|
||||
}
|
||||
|
||||
@Suppress("LargeClass", "LongParameterList", "TooManyFunctions")
|
||||
|
|
@ -126,6 +132,7 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
private val uiMessageSender: UiMessageSender,
|
||||
private val reviewManager: ReviewManager,
|
||||
private val closeHotWalletUpgradeBannerUseCase: CloseHotWalletUpgradeBannerUseCase,
|
||||
private val acknowledgeTokenSyncCompletionUseCase: AcknowledgeTokenSyncCompletionUseCase,
|
||||
) : BaseWalletClickIntents(), WalletWarningsClickIntents {
|
||||
|
||||
override fun onAddBackupCardClick() {
|
||||
|
|
@ -501,6 +508,17 @@ internal class WalletWarningsClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
override fun onDismissTokenSyncNotification(userWalletId: UserWalletId) {
|
||||
acknowledgeTokenSyncCompletionUseCase(userWalletId)
|
||||
}
|
||||
|
||||
override fun onTokenSyncManageClick(userWalletId: UserWalletId) {
|
||||
acknowledgeTokenSyncCompletionUseCase(userWalletId)
|
||||
router.openManageTokensScreen(
|
||||
AccountId.forMainCryptoPortfolio(userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val VISA_PROMO_LINK = "https://tangem.com/en/cardwaitlist/?utm_source=tangem-app-banner" +
|
||||
"&utm_medium=banner" +
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ internal object WalletPreviewDataLegacy {
|
|||
balance = "8923,05312312312312312312331231231233432423423424234 $",
|
||||
additionalInfo = WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"),
|
||||
content = WalletAdditionalInfo.Content.Text(
|
||||
TextReference.Str("3 cards • Seed phrase3 cards • Seed phrasephrasephrasephrase"),
|
||||
),
|
||||
),
|
||||
imageResId = R.drawable.ill_wallet2_cards3_120_106,
|
||||
dropDownItems = persistentListOf(),
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ internal object WalletScreenPreviewDataLegacy {
|
|||
title = "Note",
|
||||
additionalInfo = WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = TextReference.Str("Locked"),
|
||||
content = WalletAdditionalInfo.Content.Text(TextReference.Str("Locked")),
|
||||
),
|
||||
imageResId = R.drawable.ill_note_btc_120_106,
|
||||
dropDownItems = persistentListOf(),
|
||||
|
|
@ -172,7 +172,7 @@ internal object WalletScreenPreviewDataLegacy {
|
|||
title = "Wallet 1",
|
||||
additionalInfo = WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = TextReference.Str("Seed phrase"),
|
||||
content = WalletAdditionalInfo.Content.Text(TextReference.Str("Seed phrase")),
|
||||
),
|
||||
imageResId = R.drawable.ill_wallet2_cards3_120_106,
|
||||
cardCount = 3,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ import com.tangem.domain.notifications.repository.NotificationsRepository
|
|||
import com.tangem.domain.promo.ShouldShowPromoWalletUseCase
|
||||
import com.tangem.domain.promo.models.PromoId
|
||||
import com.tangem.domain.settings.IsReadyToShowRateAppUseCase
|
||||
import com.tangem.domain.tokensync.model.TokenSyncProgress
|
||||
import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
|
||||
import com.tangem.feature.wallet.child.wallet.model.WalletActivationBannerType
|
||||
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
|
||||
|
|
@ -43,6 +46,7 @@ import kotlinx.collections.immutable.toImmutableList
|
|||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -61,6 +65,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
private val shouldShowUpgradeHotWalletBannerUseCase: ShouldShowUpgradeHotWalletBannerUseCase,
|
||||
private val getUpgradeBannerClosureTimestampUseCase: GetUpgradeBannerClosureTimestampUseCase,
|
||||
private val checkHotWalletUpgradeBannerUseCase: CheckHotWalletUpgradeBannerUseCase,
|
||||
private val observeTokenSyncUseCase: ObserveTokenSyncUseCase,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) {
|
||||
|
||||
@Suppress("UNCHECKED_CAST", "MagicNumber", "LongMethod", "CastNullableToNonNullableType")
|
||||
|
|
@ -69,6 +75,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
val params = SingleAccountStatusListProducer.Params(userWallet.walletId)
|
||||
val accountStatusListFlow = accountDependencies.singleAccountStatusListSupplier(params)
|
||||
|
||||
val tokenSyncProgressFlow = if (hotWalletFeatureToggles.isTokenSyncEnabled && userWallet is UserWallet.Hot) {
|
||||
observeTokenSyncUseCase(userWallet.walletId).distinctUntilChanged()
|
||||
} else {
|
||||
flowOf(TokenSyncProgress.Idle)
|
||||
}
|
||||
|
||||
return combine(
|
||||
accountStatusListFlow,
|
||||
isReadyToShowRateAppUseCase().distinctUntilChanged(),
|
||||
|
|
@ -84,6 +96,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
.distinctUntilChanged(),
|
||||
getUpgradeBannerClosureTimestampUseCase(userWallet.walletId)
|
||||
.distinctUntilChanged(),
|
||||
tokenSyncProgressFlow,
|
||||
) { array -> array }
|
||||
.map { array ->
|
||||
val accountStatusList = array[0] as AccountStatusList
|
||||
|
|
@ -95,6 +108,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
val shouldShowYieldPromo = array[6] as Boolean
|
||||
val shouldShowUpgradeBanner = array[7] as Boolean
|
||||
val closureTimestamp = array[8] as? Long
|
||||
val tokenSyncProgress = array[9] as TokenSyncProgress
|
||||
|
||||
val flattenCurrencies = accountStatusList.flattenCurrencies()
|
||||
val paymentAccountStatus = accountStatusList.accountStatuses
|
||||
|
|
@ -139,6 +153,12 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
addTokenSyncCompletedNotification(
|
||||
userWallet = userWallet,
|
||||
tokenSyncProgress = tokenSyncProgress,
|
||||
clickIntents = clickIntents,
|
||||
)
|
||||
|
||||
addPushReminderNotification(
|
||||
clickIntents = clickIntents,
|
||||
shouldShowPushReminderBanner = shouldShowEnablePushesReminderNotification &&
|
||||
|
|
@ -384,6 +404,20 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
|
|||
// }
|
||||
// }
|
||||
|
||||
private fun MutableList<WalletNotification>.addTokenSyncCompletedNotification(
|
||||
userWallet: UserWallet,
|
||||
tokenSyncProgress: TokenSyncProgress,
|
||||
clickIntents: WalletClickIntents,
|
||||
) {
|
||||
addIf(
|
||||
element = WalletNotification.TokenSyncCompleted(
|
||||
onCloseClick = { clickIntents.onDismissTokenSyncNotification(userWallet.walletId) },
|
||||
onManageTokensClick = { clickIntents.onTokenSyncManageClick(userWallet.walletId) },
|
||||
),
|
||||
condition = tokenSyncProgress is TokenSyncProgress.Completed,
|
||||
)
|
||||
}
|
||||
|
||||
private fun MutableList<WalletNotification>.addRateTheAppNotification(
|
||||
isReadyToShowRating: Boolean,
|
||||
clickIntents: WalletClickIntents,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import com.tangem.domain.card.common.util.cardTypesResolver
|
|||
import com.tangem.domain.card.common.util.getCardsCount
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
@ -28,7 +29,11 @@ internal object WalletAdditionalInfoFactory {
|
|||
* @param wallet current wallet
|
||||
* @param currencyAmount amount of currency
|
||||
*/
|
||||
fun resolve(wallet: UserWallet, currencyAmount: BigDecimal? = null): WalletAdditionalInfo {
|
||||
fun resolve(
|
||||
wallet: UserWallet,
|
||||
currencyAmount: BigDecimal? = null,
|
||||
syncProgress: TokenSyncProgressUM = TokenSyncProgressUM.Idle,
|
||||
): WalletAdditionalInfo {
|
||||
return when (wallet) {
|
||||
is UserWallet.Cold -> {
|
||||
if (wallet.isMultiCurrency) {
|
||||
|
|
@ -37,19 +42,26 @@ internal object WalletAdditionalInfoFactory {
|
|||
wallet.resolveSingleCurrencyInfo(currencyAmount)
|
||||
}
|
||||
}
|
||||
is UserWallet.Hot -> wallet.resolveAdditionalInfo()
|
||||
is UserWallet.Hot -> wallet.resolveAdditionalInfo(syncProgress)
|
||||
}
|
||||
}
|
||||
|
||||
private fun UserWallet.Hot.resolveAdditionalInfo(): WalletAdditionalInfo {
|
||||
private fun UserWallet.Hot.resolveAdditionalInfo(syncProgress: TokenSyncProgressUM): WalletAdditionalInfo {
|
||||
val content = if (syncProgress is TokenSyncProgressUM.InProgress) {
|
||||
WalletAdditionalInfo.Content.SyncProgress(syncProgress.progressPercent)
|
||||
} else {
|
||||
WalletAdditionalInfo.Content.Text(
|
||||
TextReference.Res(R.string.hw_mobile_wallet) +
|
||||
when {
|
||||
isLocked -> DIVIDER + TextReference.Res(R.string.common_locked)
|
||||
backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup)
|
||||
else -> TextReference.Str("")
|
||||
},
|
||||
)
|
||||
}
|
||||
return WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = TextReference.Res(R.string.hw_mobile_wallet) +
|
||||
when {
|
||||
isLocked -> DIVIDER + TextReference.Res(R.string.common_locked)
|
||||
backedUp.not() -> DIVIDER + TextReference.Res(R.string.hw_backup_no_backup)
|
||||
else -> TextReference.Str("")
|
||||
},
|
||||
content = content,
|
||||
isHotBackedUp = backedUp,
|
||||
)
|
||||
}
|
||||
|
|
@ -58,9 +70,11 @@ internal object WalletAdditionalInfoFactory {
|
|||
return if (isLocked) {
|
||||
WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = getBackupInfoWithDivider(
|
||||
backupCardsCount = getCardsCount(),
|
||||
) + TextReference.Res(R.string.common_locked),
|
||||
content = WalletAdditionalInfo.Content.Text(
|
||||
getBackupInfoWithDivider(
|
||||
backupCardsCount = getCardsCount(),
|
||||
) + TextReference.Res(R.string.common_locked),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
val cardTypeResolver = scanResponse.cardTypesResolver
|
||||
|
|
@ -76,8 +90,10 @@ internal object WalletAdditionalInfoFactory {
|
|||
return if (isImported) {
|
||||
WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res(
|
||||
id = R.string.common_seed_phrase,
|
||||
content = WalletAdditionalInfo.Content.Text(
|
||||
getBackupInfoWithDivider(backupCardsCount = getCardsCount()) + TextReference.Res(
|
||||
id = R.string.common_seed_phrase,
|
||||
),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
|
|
@ -94,7 +110,7 @@ internal object WalletAdditionalInfoFactory {
|
|||
}
|
||||
|
||||
private fun getBackupInfo(backupCardsCount: Int?): WalletAdditionalInfo {
|
||||
val content = if (backupCardsCount != null) {
|
||||
val ref = if (backupCardsCount != null) {
|
||||
getBackupInfoTextReference(count = backupCardsCount)
|
||||
} else {
|
||||
TextReference.EMPTY
|
||||
|
|
@ -102,7 +118,7 @@ internal object WalletAdditionalInfoFactory {
|
|||
|
||||
return WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = content,
|
||||
content = WalletAdditionalInfo.Content.Text(ref),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +134,7 @@ internal object WalletAdditionalInfoFactory {
|
|||
return if (isLocked) {
|
||||
WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = TextReference.Res(R.string.common_locked),
|
||||
content = WalletAdditionalInfo.Content.Text(TextReference.Res(R.string.common_locked)),
|
||||
)
|
||||
} else {
|
||||
val blockchain = scanResponse.cardTypesResolver.getBlockchain()
|
||||
|
|
@ -126,7 +142,7 @@ internal object WalletAdditionalInfoFactory {
|
|||
|
||||
WalletAdditionalInfo(
|
||||
hideable = true,
|
||||
content = TextReference.Str(value = amount.orEmpty()),
|
||||
content = WalletAdditionalInfo.Content.Text(TextReference.Str(value = amount.orEmpty())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.loaders.implementors
|
|||
import com.tangem.core.ui.DesignFeatureToggles
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.subscribers.*
|
||||
import com.tangem.features.hotwallet.HotWalletFeatureToggles
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
|
@ -17,10 +18,12 @@ internal class MultiWalletContentLoader @AssistedInject constructor(
|
|||
private val walletNotificationsSubscriberFactory: WalletNotificationsSubscriber.Factory,
|
||||
private val multiWalletActionButtonsSubscriberFactory: MultiWalletActionButtonsSubscriber.Factory,
|
||||
private val tangemPayMainSubscriberFactory: TangemPayMainSubscriber.Factory,
|
||||
private val tokenSyncSubscriberFactory: TokenSyncSubscriber.Factory,
|
||||
private val designFeatureToggles: DesignFeatureToggles,
|
||||
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
|
||||
) : WalletContentLoader(id = userWallet.walletId) {
|
||||
|
||||
override fun create(): List<WalletSubscriber> = listOf(
|
||||
override fun create(): List<WalletSubscriber> = listOfNotNull(
|
||||
accountListSubscriberFactory.create(userWallet),
|
||||
walletNFTListSubscriberFactory.create(userWallet),
|
||||
checkWalletWithFundsSubscriberFactory.create(userWallet),
|
||||
|
|
@ -31,6 +34,11 @@ internal class MultiWalletContentLoader @AssistedInject constructor(
|
|||
},
|
||||
multiWalletActionButtonsSubscriberFactory.create(userWallet),
|
||||
tangemPayMainSubscriberFactory.create(userWallet),
|
||||
if (hotWalletFeatureToggles.isTokenSyncEnabled && userWallet is UserWallet.Hot) {
|
||||
tokenSyncSubscriberFactory.create(userWallet)
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
|
||||
@AssistedFactory
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
internal sealed class TokenSyncProgressUM {
|
||||
|
||||
data object Idle : TokenSyncProgressUM()
|
||||
|
||||
data class InProgress(val progressPercent: Int) : TokenSyncProgressUM()
|
||||
|
||||
data object Completed : TokenSyncProgressUM()
|
||||
}
|
||||
|
|
@ -6,7 +6,12 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
@Immutable
|
||||
data class WalletAdditionalInfo(
|
||||
val hideable: Boolean,
|
||||
val content: TextReference,
|
||||
val content: Content,
|
||||
val isHotBackedUp: Boolean = false,
|
||||
val shouldShowProgress: Boolean = false,
|
||||
)
|
||||
) {
|
||||
@Immutable
|
||||
sealed interface Content {
|
||||
data class Text(val text: TextReference) : Content
|
||||
data class SyncProgress(val progressPercent: Int) : Content
|
||||
}
|
||||
}
|
||||
|
|
@ -85,7 +85,10 @@ internal sealed interface WalletCardState {
|
|||
|
||||
private companion object {
|
||||
val defaultAdditionalInfo: WalletAdditionalInfo
|
||||
get() = WalletAdditionalInfo(hideable = true, content = EMPTY_BALANCE_TEXT)
|
||||
get() = WalletAdditionalInfo(
|
||||
hideable = true,
|
||||
content = WalletAdditionalInfo.Content.Text(EMPTY_BALANCE_TEXT),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ internal sealed interface WalletState : WalletStateHolder {
|
|||
abstract val tangemPayState: TangemPayState
|
||||
abstract val tangemPayMainUM: TangemPayMainUM
|
||||
abstract val isTangemPayRefactorEnabled: Boolean // TANGEM_PAY_ACCOUNTS_REFACTOR_ENABLED
|
||||
abstract val tokenSyncProgressUM: TokenSyncProgressUM
|
||||
|
||||
data class Content(
|
||||
override val pullToRefreshConfig: PullToRefreshConfig,
|
||||
|
|
@ -40,6 +41,7 @@ internal sealed interface WalletState : WalletStateHolder {
|
|||
override val tangemPayState: TangemPayState,
|
||||
override val tangemPayMainUM: TangemPayMainUM,
|
||||
override val isTangemPayRefactorEnabled: Boolean,
|
||||
override val tokenSyncProgressUM: TokenSyncProgressUM = TokenSyncProgressUM.Idle,
|
||||
) : MultiCurrency()
|
||||
|
||||
data class Locked(
|
||||
|
|
@ -61,6 +63,7 @@ internal sealed interface WalletState : WalletStateHolder {
|
|||
override val tangemPayState: TangemPayState = TangemPayState.Empty
|
||||
override val tangemPayMainUM: TangemPayMainUM = TangemPayMainUM.Empty
|
||||
override val isTangemPayRefactorEnabled: Boolean = false
|
||||
override val tokenSyncProgressUM: TokenSyncProgressUM = TokenSyncProgressUM.Idle
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ internal class SetTokenListErrorTransformer(
|
|||
walletsBalanceUM = walletUM.walletsBalanceUM.toLoadedState(),
|
||||
tokensListUM = WalletTokensListUM.Empty(
|
||||
onEmptyClick = {
|
||||
clickIntents.onManageTokensClick(walletUM.walletsBalanceUM.id)
|
||||
clickIntents.onTokenSyncManageClick(walletUM.walletsBalanceUM.id)
|
||||
},
|
||||
),
|
||||
buttons = walletUM.disableButtons(),
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ internal class SetTokenListTransformer(
|
|||
if (params !is TokenConverterParams.Account) {
|
||||
return WalletTokensListUM.Empty(
|
||||
onEmptyClick = {
|
||||
clickIntents.onManageTokensClick(userWallet.walletId)
|
||||
clickIntents.onTokenSyncManageClick(userWallet.walletId)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +1,34 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.state.transformers
|
||||
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletAdditionalInfo
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
|
||||
|
||||
internal class SetTokenSyncProgressTransformer(
|
||||
userWalletId: UserWalletId,
|
||||
private val progressPercent: Int,
|
||||
) : WalletStateTransformer(userWalletId) {
|
||||
private val userWallet: UserWallet,
|
||||
private val progress: TokenSyncProgressUM,
|
||||
) : WalletStateTransformer(userWallet.walletId) {
|
||||
|
||||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
val updatedCardState = updateCardState(prevState.walletCardState)
|
||||
prevState.copy(walletCardState = updatedCardState)
|
||||
}
|
||||
else -> {
|
||||
prevState
|
||||
}
|
||||
is WalletState.MultiCurrency.Content -> prevState.copy(
|
||||
walletCardState = updateCardState(prevState.walletCardState),
|
||||
tokenSyncProgressUM = progress,
|
||||
)
|
||||
else -> prevState
|
||||
}
|
||||
}
|
||||
|
||||
override fun transform(walletUM: WalletUM): WalletUM {
|
||||
return walletUM
|
||||
}
|
||||
override fun transform(walletUM: WalletUM): WalletUM = walletUM
|
||||
|
||||
private fun updateCardState(cardState: WalletCardState): WalletCardState {
|
||||
val additionalInfo = WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = resourceReference(
|
||||
id = R.string.initial_wallet_sync_restore_progress,
|
||||
formatArgs = wrappedList(progressPercent),
|
||||
),
|
||||
shouldShowProgress = true,
|
||||
)
|
||||
val additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet, syncProgress = progress)
|
||||
return when (cardState) {
|
||||
is WalletCardState.Loading -> {
|
||||
cardState.copy(additionalInfo = additionalInfo)
|
||||
}
|
||||
is WalletCardState.Content -> {
|
||||
cardState.copy(additionalInfo = additionalInfo)
|
||||
}
|
||||
is WalletCardState.Loading -> cardState.copy(additionalInfo = additionalInfo)
|
||||
is WalletCardState.Content -> cardState.copy(additionalInfo = additionalInfo)
|
||||
else -> cardState
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.tangem.domain.card.common.util.getCardsCount
|
|||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletAdditionalInfoFactory
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.WalletImageResolver
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletCardState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletState
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
|
||||
|
|
@ -17,7 +18,9 @@ internal class UpdateWalletCardsCountTransformer(
|
|||
override fun transform(prevState: WalletState): WalletState {
|
||||
return when (prevState) {
|
||||
is WalletState.MultiCurrency.Content -> {
|
||||
prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState())
|
||||
prevState.copy(
|
||||
walletCardState = prevState.walletCardState.toUpdatedState(prevState.tokenSyncProgressUM),
|
||||
)
|
||||
}
|
||||
is WalletState.SingleCurrency.Content -> {
|
||||
prevState.copy(walletCardState = prevState.walletCardState.toUpdatedState())
|
||||
|
|
@ -35,10 +38,12 @@ internal class UpdateWalletCardsCountTransformer(
|
|||
return walletUM // todo redesign main
|
||||
}
|
||||
|
||||
private fun WalletCardState.toUpdatedState(): WalletCardState {
|
||||
private fun WalletCardState.toUpdatedState(
|
||||
syncProgress: TokenSyncProgressUM = TokenSyncProgressUM.Idle,
|
||||
): WalletCardState {
|
||||
return when (this) {
|
||||
is WalletCardState.Content -> copy(
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet),
|
||||
additionalInfo = WalletAdditionalInfoFactory.resolve(wallet = userWallet, syncProgress = syncProgress),
|
||||
imageResId = walletImageResolver.resolve(userWallet = userWallet),
|
||||
cardCount = (userWallet as? UserWallet.Cold)?.getCardsCount(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package com.tangem.feature.wallet.presentation.wallet.subscribers
|
||||
|
||||
import com.tangem.domain.models.wallet.UserWallet
|
||||
import com.tangem.domain.tokensync.model.TokenSyncProgress
|
||||
import com.tangem.domain.tokensync.usecase.ObserveTokenSyncUseCase
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.WalletStateController
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.model.TokenSyncProgressUM
|
||||
import com.tangem.feature.wallet.presentation.wallet.state.transformers.SetTokenSyncProgressTransformer
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
internal class TokenSyncSubscriber @AssistedInject constructor(
|
||||
@Assisted private val userWallet: UserWallet,
|
||||
private val stateController: WalletStateController,
|
||||
private val observeTokenSyncUseCase: ObserveTokenSyncUseCase,
|
||||
) : WalletSubscriber() {
|
||||
|
||||
override fun create(coroutineScope: CoroutineScope): Flow<*> {
|
||||
return observeTokenSyncUseCase(userWallet.walletId)
|
||||
.onEach { current -> handleProgress(userWallet, current) }
|
||||
}
|
||||
|
||||
private fun handleProgress(userWallet: UserWallet, current: TokenSyncProgress) {
|
||||
val progressUM = when (current) {
|
||||
is TokenSyncProgress.InProgress -> TokenSyncProgressUM.InProgress(current.progressPercent)
|
||||
is TokenSyncProgress.Completed -> TokenSyncProgressUM.Completed
|
||||
is TokenSyncProgress.Idle -> TokenSyncProgressUM.Idle
|
||||
}
|
||||
stateController.update(
|
||||
SetTokenSyncProgressTransformer(
|
||||
userWallet = userWallet,
|
||||
progress = progressUM,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
fun create(userWallet: UserWallet): TokenSyncSubscriber
|
||||
}
|
||||
}
|
||||
|
|
@ -41,6 +41,9 @@ import com.tangem.core.ui.extensions.TextReference
|
|||
import com.tangem.core.ui.extensions.conditional
|
||||
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.wrappedList
|
||||
import com.tangem.feature.wallet.impl.R
|
||||
import com.tangem.core.ui.res.TangemDimens
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
|
|
@ -156,16 +159,10 @@ private fun CardContainer(state: WalletCardState, isBalanceHidden: Boolean, item
|
|||
.padding(vertical = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
|
||||
val additionalText by remember(state.additionalInfo, isBalanceHidden) {
|
||||
mutableStateOf(
|
||||
state.additionalInfo?.content?.orMaskWithStars(
|
||||
maskWithStars = state.additionalInfo?.hideable == true && isBalanceHidden,
|
||||
),
|
||||
)
|
||||
}
|
||||
AdditionalInfo(
|
||||
text = additionalText,
|
||||
showProgress = state.additionalInfo?.shouldShowProgress == true,
|
||||
content = state.additionalInfo?.content,
|
||||
hideable = state.additionalInfo?.hideable == true,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier.conditional(
|
||||
state.imageResId == null,
|
||||
) { fillMaxWidth() },
|
||||
|
|
@ -300,28 +297,53 @@ private fun Modifier.nonContentBalanceSize(dimens: TangemDimens): Modifier {
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun AdditionalInfo(text: TextReference?, showProgress: Boolean, modifier: Modifier = Modifier) {
|
||||
private fun AdditionalInfo(
|
||||
content: WalletAdditionalInfo.Content?,
|
||||
hideable: Boolean,
|
||||
isBalanceHidden: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedContent(
|
||||
targetState = text,
|
||||
targetState = content,
|
||||
contentKey = { con ->
|
||||
when (con) {
|
||||
is WalletAdditionalInfo.Content.Text -> con
|
||||
is WalletAdditionalInfo.Content.SyncProgress -> WalletAdditionalInfo.Content.SyncProgress::class
|
||||
null -> null
|
||||
}
|
||||
},
|
||||
label = "Update the additional text",
|
||||
modifier = modifier,
|
||||
transitionSpec = {
|
||||
fadeIn(animationSpec = tween(durationMillis = 220, delayMillis = 90)) togetherWith
|
||||
fadeOut(animationSpec = tween(durationMillis = 90))
|
||||
},
|
||||
) { animatedText ->
|
||||
if (animatedText != null) {
|
||||
) { animatedContent ->
|
||||
if (animatedContent != null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
AdditionalInfoText(text = animatedText)
|
||||
if (showProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size16),
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
when (animatedContent) {
|
||||
is WalletAdditionalInfo.Content.Text -> {
|
||||
AdditionalInfoText(
|
||||
text = animatedContent.text.orMaskWithStars(
|
||||
maskWithStars = hideable && isBalanceHidden,
|
||||
),
|
||||
)
|
||||
}
|
||||
is WalletAdditionalInfo.Content.SyncProgress -> {
|
||||
AdditionalInfoText(
|
||||
text = resourceReference(
|
||||
id = R.string.initial_wallet_sync_restore_progress,
|
||||
formatArgs = wrappedList(animatedContent.progressPercent),
|
||||
),
|
||||
)
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size16),
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
strokeWidth = TangemTheme.dimens.size2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -396,7 +418,7 @@ private class WalletCardStateProvider : CollectionPreviewParameterProvider<Walle
|
|||
title = "Title",
|
||||
additionalInfo = WalletAdditionalInfo(
|
||||
hideable = false,
|
||||
content = TextReference.Str("3 cards"),
|
||||
content = WalletAdditionalInfo.Content.Text(TextReference.Str("3 cards")),
|
||||
),
|
||||
),
|
||||
WalletPreviewDataLegacy.walletCardContentState.copy(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue