Updated on 2026-08-14

This commit is contained in:
Tangem 2026-04-14 15:45:39 +04:00
parent 9a0fbcbe1b
commit 58909db262
9 changed files with 214 additions and 103 deletions

View file

@ -1,14 +1,9 @@
package com.tangem.data.account.di
import android.content.Context
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Moshi
import com.tangem.data.account.converter.AccountConverterFactoryContainer
import com.tangem.data.account.fetcher.DefaultWalletAccountsFetcher
import com.tangem.data.account.repository.AccountsExpandedDTO
import com.tangem.data.account.repository.DefaultAccountsCRUDRepository
import com.tangem.data.account.repository.DefaultAccountsExpandedRepository
import com.tangem.data.account.store.AccountsResponseStoreFactory
import com.tangem.data.account.store.ArchivedAccountsStoreFactory
import com.tangem.data.account.tokens.DefaultMainAccountTokensMigration
@ -17,14 +12,9 @@ import com.tangem.data.common.account.WalletAccountsSaver
import com.tangem.data.common.cache.etag.ETagsStore
import com.tangem.data.common.currency.UserTokensSaver
import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.local.accounts.AccountTokenMigrationStore
import com.tangem.datasource.local.datastore.RuntimeStateStore
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes
import com.tangem.domain.account.repository.AccountsCRUDRepository
import com.tangem.domain.account.repository.AccountsExpandedRepository
import com.tangem.domain.account.tokens.MainAccountTokensMigration
import com.tangem.utils.coroutines.AppCoroutineScope
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -63,28 +53,6 @@ internal object AccountDataModule {
)
}
@Provides
@Singleton
fun provideAccountsExpandedRepository(
@NetworkMoshi moshi: Moshi,
@ApplicationContext context: Context,
appScope: AppCoroutineScope,
): AccountsExpandedRepository {
val store = DataStoreFactory.create<Map<String, Set<AccountsExpandedDTO>>>(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<AccountsExpandedDTO>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = "account_expanded_store") },
scope = appScope,
)
return DefaultAccountsExpandedRepository(
store = store,
)
}
@Provides
@Singleton
fun provideWalletAccountsFetcher(impl: DefaultWalletAccountsFetcher): WalletAccountsFetcher = impl

View file

@ -0,0 +1,18 @@
package com.tangem.data.account.di
import com.tangem.data.account.repository.DefaultAccountsExpandedRepository
import com.tangem.domain.account.repository.AccountsExpandedRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
internal interface AccountUtilsModule {
@Binds
fun provideAccountsExpandedRepositoryFactory(
factory: DefaultAccountsExpandedRepository.Factory,
): AccountsExpandedRepository.Factory
}

View file

@ -1,17 +1,28 @@
package com.tangem.data.account.repository
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.dataStoreFile
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import com.tangem.data.account.converter.toAccountId
import com.tangem.datasource.di.NetworkMoshi
import com.tangem.datasource.utils.MoshiDataStoreSerializer
import com.tangem.datasource.utils.mapWithStringKeyTypes
import com.tangem.datasource.utils.setTypes
import com.tangem.domain.account.models.AccountExpandedState
import com.tangem.domain.account.repository.AccountsExpandedRepository
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.AppCoroutineScope
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
internal class DefaultAccountsExpandedRepository(
internal class DefaultAccountsExpandedRepository constructor(
private val store: DataStore<Map<String, Set<AccountsExpandedDTO>>>,
) : AccountsExpandedRepository {
@ -41,7 +52,7 @@ internal class DefaultAccountsExpandedRepository(
}
override suspend fun clearStore() {
store.updateData { emptyMap() }
store.updateData { map -> map.mapValues { emptySet() } }
}
override suspend fun update(accountState: AccountExpandedState) {
@ -59,6 +70,28 @@ internal class DefaultAccountsExpandedRepository(
map.plus(walletId.stringValue to updatedSet)
}
}
internal class Factory @Inject constructor(
@NetworkMoshi private val moshi: Moshi,
@ApplicationContext private val context: Context,
private val appScope: AppCoroutineScope,
) : AccountsExpandedRepository.Factory {
override fun create(storeFileName: String): DefaultAccountsExpandedRepository {
val store = DataStoreFactory.create<Map<String, Set<AccountsExpandedDTO>>>(
serializer = MoshiDataStoreSerializer(
moshi = moshi,
types = mapWithStringKeyTypes(valueTypes = setTypes<AccountsExpandedDTO>()),
defaultValue = emptyMap(),
),
produceFile = { context.dataStoreFile(fileName = storeFileName) },
scope = appScope,
)
return DefaultAccountsExpandedRepository(
store = store,
)
}
}
}
@JsonClass(generateAdapter = true)

View file

@ -12,4 +12,13 @@ interface AccountsExpandedRepository {
suspend fun syncStore(walletId: UserWalletId, existAccounts: Set<AccountId>)
suspend fun clearStore()
suspend fun update(accountState: AccountExpandedState)
interface Factory {
fun create(storeFileName: String): AccountsExpandedRepository
}
companion object {
const val MAIN_STORE_FILE_NAME = "account_expanded_store"
const val CHOOSE_TOKEN_FILE_NAME = "choose_token_account_expanded_store"
}
}

View file

@ -0,0 +1,42 @@
package com.tangem.domain.account.status.utils
import com.tangem.domain.account.repository.AccountsExpandedRepository
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ChooseTokenExpandedAccountsHolder @Inject constructor(
private val mainHolder: MainExpandedAccountsHolder,
holderFactory: DefaultExpandedAccountsHolder.Factory,
repositoryFactory: AccountsExpandedRepository.Factory,
) : ExpandedAccountsHolder {
private val repository: AccountsExpandedRepository =
repositoryFactory.create(AccountsExpandedRepository.CHOOSE_TOKEN_FILE_NAME)
private val defaultHolder: DefaultExpandedAccountsHolder = holderFactory.create(repository)
override fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>> = flow {
val isStored = repository.expandedAccounts.first()[walletId] != null
if (isStored) {
emitAll(defaultHolder.expandedAccounts(walletId))
} else {
val initExpanded = mainHolder.expandedAccounts(walletId).first()
emitAll(defaultHolder.expandedAccounts(walletId, initExpanded))
}
}
override fun expandAccount(accountId: AccountId) {
defaultHolder.expandAccount(accountId)
}
override fun collapseAccount(accountId: AccountId) {
defaultHolder.collapseAccount(accountId)
}
}

View file

@ -9,20 +9,26 @@ import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import javax.inject.Inject
import javax.inject.Singleton
// todo swap separate for main and swap
@Singleton
class ExpandedAccountsHolder @Inject constructor(
interface ExpandedAccountsHolder {
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = expandedAccounts(userWallet.walletId)
fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>>
fun expandAccount(accountId: AccountId)
fun collapseAccount(accountId: AccountId)
}
class DefaultExpandedAccountsHolder @AssistedInject constructor(
private val singleAccountListSupplier: SingleAccountListSupplier,
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
private val accountsExpandedRepository: AccountsExpandedRepository,
@Assisted private val accountsExpandedRepository: AccountsExpandedRepository,
private val dispatchers: CoroutineDispatcherProvider,
) {
@ -31,71 +37,70 @@ class ExpandedAccountsHolder @Inject constructor(
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = expandedAccounts(userWallet.walletId)
fun expandedAccounts(walletId: UserWalletId, initExpanded: Set<AccountId> = emptySet()): Flow<Set<AccountId>> =
channelFlow {
val storedState = accountsExpandedRepository.expandedAccounts
.map { it[walletId] ?: initExpanded.map { id -> AccountExpandedState(id, true) } }
.stateIn(this)
fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>> = channelFlow {
val storedState = accountsExpandedRepository.expandedAccounts
.map { it[walletId].orEmpty() }
.stateIn(this)
val isAccountsMode = isAccountsModeEnabledUseCase.invoke()
.stateIn(this)
val isAccountsMode = isAccountsModeEnabledUseCase.invoke()
.stateIn(this)
val initExpandedState = storedState.value
.mapNotNull { it.takeIf { state -> state.isExpanded }?.accountId }
.toSet()
// main state holder
val expandedAccounts = MutableStateFlow(initExpandedState)
var debounceJob: Job? = null
val initExpandedState = storedState.value
.mapNotNull { it.takeIf { state -> state.isExpanded }?.accountId }
.toSet()
// main state holder
val expandedAccounts = MutableStateFlow(initExpandedState)
var debounceJob: Job? = null
actionChannel
.filter { (accountId, _) -> accountId.userWalletId == walletId }
.filter { debounceJob?.isActive != true }
.onEach { (accountId, isExpand) ->
debounceJob = launch { delay(DEBOUNCE_MILLIS) }
val newState = AccountExpandedState(accountId, isExpand)
launch { accountsExpandedRepository.update(newState) }
if (isExpand) {
expandedAccounts.update { it.plus(accountId) }
} else {
expandedAccounts.update { it.minus(accountId) }
actionChannel
.filter { (accountId, _) -> accountId.userWalletId == walletId }
.filter { debounceJob?.isActive != true }
.onEach { (accountId, isExpand) ->
debounceJob = launch { delay(DEBOUNCE_MILLIS) }
val newState = AccountExpandedState(accountId, isExpand)
launch { accountsExpandedRepository.update(newState) }
if (isExpand) {
expandedAccounts.update { it.plus(accountId) }
} else {
expandedAccounts.update { it.minus(accountId) }
}
}
}
.launchIn(this)
.launchIn(this)
walletAccounts(walletId).onEach { accountList ->
if (!isAccountsModeEnabledUseCase.invokeSync()) {
accountsExpandedRepository.clearStore()
expandedAccounts.update { emptySet() }
return@onEach
}
val idsSet = accountList.accounts.mapTo(mutableSetOf()) { it.accountId }
accountsExpandedRepository.syncStore(walletId, idsSet)
val isSingleAccount = accountList.accounts.size == 1
val storedMainAccountState = storedState.value
.find { it.accountId == accountList.mainAccount.accountId }
if (isSingleAccount && storedMainAccountState == null) {
// force expand for single and not stored account
expandedAccounts.update { setOf(accountList.mainAccount.accountId) }
}
}.launchIn(this)
combine(
flow = expandedAccounts,
flow2 = isAccountsMode,
transform = { expanded, isAccountMode ->
if (isAccountMode) {
channel.send(expanded)
} else {
channel.send(emptySet())
walletAccounts(walletId).onEach { accountList ->
if (!isAccountsModeEnabledUseCase.invokeSync()) {
accountsExpandedRepository.clearStore()
expandedAccounts.update { emptySet() }
return@onEach
}
},
).collect()
}
.flowOn(dispatchers.default)
.distinctUntilChanged()
val idsSet = accountList.accounts.mapTo(mutableSetOf()) { it.accountId }
accountsExpandedRepository.syncStore(walletId, idsSet)
val isSingleAccount = accountList.accounts.size == 1
val storedMainAccountState = storedState.value
.find { it.accountId == accountList.mainAccount.accountId }
if (isSingleAccount && storedMainAccountState == null) {
// force expand for single and not stored account
expandedAccounts.update { setOf(accountList.mainAccount.accountId) }
}
}.launchIn(this)
combine(
flow = expandedAccounts,
flow2 = isAccountsMode,
transform = { expanded, isAccountMode ->
if (isAccountMode) {
channel.send(expanded)
} else {
channel.send(emptySet())
}
},
).collect()
}
.flowOn(dispatchers.default)
.distinctUntilChanged()
fun expandAccount(accountId: AccountId) {
actionChannel.tryEmit(accountId to true)
@ -107,6 +112,11 @@ class ExpandedAccountsHolder @Inject constructor(
private fun walletAccounts(walletId: UserWalletId): Flow<AccountList> = singleAccountListSupplier(walletId)
@AssistedFactory
interface Factory {
fun create(accountsExpandedRepository: AccountsExpandedRepository): DefaultExpandedAccountsHolder
}
companion object {
private const val DEBOUNCE_MILLIS = 200L
}

View file

@ -0,0 +1,31 @@
package com.tangem.domain.account.status.utils
import com.tangem.domain.account.repository.AccountsExpandedRepository
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MainExpandedAccountsHolder @Inject constructor(
holderFactory: DefaultExpandedAccountsHolder.Factory,
repositoryFactory: AccountsExpandedRepository.Factory,
) : ExpandedAccountsHolder {
private val repository: AccountsExpandedRepository = repositoryFactory
.create(AccountsExpandedRepository.MAIN_STORE_FILE_NAME)
private val default: DefaultExpandedAccountsHolder = holderFactory.create(repository)
override fun expandedAccounts(walletId: UserWalletId): Flow<Set<AccountId>> {
return default.expandedAccounts(walletId)
}
override fun expandAccount(accountId: AccountId) {
default.expandAccount(accountId)
}
override fun collapseAccount(accountId: AccountId) {
default.collapseAccount(accountId)
}
}

View file

@ -3,7 +3,7 @@ package com.tangem.feature.swap.choosetoken.impl.model
import com.tangem.common.ui.tokens.TokenConverterParams
import com.tangem.domain.account.models.AccountStatusList
import com.tangem.domain.account.status.supplier.MultiAccountStatusListSupplier
import com.tangem.domain.account.status.utils.ExpandedAccountsHolder
import com.tangem.domain.account.status.utils.ChooseTokenExpandedAccountsHolder
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.account.AccountId
import com.tangem.domain.models.account.AccountStatus
@ -23,7 +23,7 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.*
internal class PortfolioListBlockDelegate @AssistedInject constructor(
private val expandedAccountsHolder: ExpandedAccountsHolder,
private val expandedAccountsHolder: ChooseTokenExpandedAccountsHolder,
private val settingContext: SettingContextUseCase,
private val multiAccountStatusListSupplier: MultiAccountStatusListSupplier,
private val getWalletsUseCase: GetWalletsUseCase,

View file

@ -3,14 +3,14 @@ package com.tangem.feature.wallet.presentation.account
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.domain.account.status.supplier.SingleAccountStatusListSupplier
import com.tangem.domain.account.status.supplier.SingleAccountStatusSupplier
import com.tangem.domain.account.status.utils.ExpandedAccountsHolder
import com.tangem.domain.account.status.utils.MainExpandedAccountsHolder
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
import javax.inject.Inject
@ModelScoped
internal class AccountDependencies @Inject constructor(
val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
val expandedAccountsHolder: ExpandedAccountsHolder,
val expandedAccountsHolder: MainExpandedAccountsHolder,
val singleAccountStatusListSupplier: SingleAccountStatusListSupplier,
val singleAccountStatusSupplier: SingleAccountStatusSupplier,
)