Updated on 2026-08-14
This commit is contained in:
parent
c425d8640d
commit
a5b29a4449
8 changed files with 321 additions and 35 deletions
|
|
@ -70,5 +70,6 @@ dependencies {
|
|||
testImplementation(projects.common.test)
|
||||
testImplementation(projects.test.core)
|
||||
testRuntimeOnly(deps.test.junit5.engine)
|
||||
testImplementation(deps.test.turbine)
|
||||
// endregion
|
||||
}
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
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.core.configtoggle.feature.FeatureTogglesManager
|
||||
import com.tangem.data.account.converter.AccountConverterFactoryContainer
|
||||
import com.tangem.data.account.featuretoggle.DefaultAccountsFeatureToggles
|
||||
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
|
||||
|
|
@ -13,11 +18,16 @@ import com.tangem.data.common.account.WalletAccountsFetcher
|
|||
import com.tangem.data.common.account.WalletAccountsSaver
|
||||
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.local.userwallet.UserWalletsStore
|
||||
import com.tangem.datasource.utils.MoshiDataStoreSerializer
|
||||
import com.tangem.datasource.utils.mapWithStringKeyTypes
|
||||
import com.tangem.datasource.utils.setTypes
|
||||
import com.tangem.domain.account.featuretoggle.AccountsFeatureToggles
|
||||
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.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -25,6 +35,8 @@ import dagger.Provides
|
|||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
|
|
@ -63,6 +75,28 @@ internal object AccountDataModule {
|
|||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountsExpandedRepository(
|
||||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): 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 = CoroutineScope(context = dispatchers.io + SupervisorJob()),
|
||||
)
|
||||
|
||||
return DefaultAccountsExpandedRepository(
|
||||
store = store,
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideWalletAccountsFetcher(impl: DefaultWalletAccountsFetcher): WalletAccountsFetcher = impl
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
package com.tangem.data.account.repository
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import com.tangem.data.account.converter.toAccountId
|
||||
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 kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
internal class DefaultAccountsExpandedRepository(
|
||||
private val store: DataStore<Map<String, Set<AccountsExpandedDTO>>>,
|
||||
) : AccountsExpandedRepository {
|
||||
|
||||
override val expandedAccounts: Flow<Map<UserWalletId, Set<AccountExpandedState>>> = store.data
|
||||
.map { stored ->
|
||||
stored.map { (rawWalletId, setOfDto) ->
|
||||
val walletId = UserWalletId(rawWalletId)
|
||||
val setOfState = setOfDto.mapTo(mutableSetOf()) { dto ->
|
||||
AccountExpandedState(
|
||||
accountId = dto.accountId.toAccountId(walletId),
|
||||
isExpanded = dto.isExpanded,
|
||||
)
|
||||
}
|
||||
walletId to setOfState
|
||||
}.toMap()
|
||||
}
|
||||
|
||||
override suspend fun syncStore(walletId: UserWalletId, existAccounts: Set<AccountId>) {
|
||||
store.updateData { map ->
|
||||
val setOfDto = map[walletId.stringValue] ?: return@updateData map
|
||||
val existingAccountIds = existAccounts.map { it.value }.toSet()
|
||||
val syncedSet = setOfDto
|
||||
.filterTo(mutableSetOf()) { (accountId, _) -> existingAccountIds.contains(accountId) }
|
||||
|
||||
map.plus(walletId.stringValue to syncedSet)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun update(accountState: AccountExpandedState) {
|
||||
store.updateData { map ->
|
||||
val walletId = accountState.accountId.userWalletId
|
||||
val setOfDto = map[walletId.stringValue].orEmpty()
|
||||
val newDto = AccountsExpandedDTO(
|
||||
accountId = accountState.accountId.value,
|
||||
isExpanded = accountState.isExpanded,
|
||||
)
|
||||
val updatedSet = setOfDto
|
||||
.filterTo(mutableSetOf()) { it.accountId != accountState.accountId.value }
|
||||
.plus(newDto)
|
||||
|
||||
map.plus(walletId.stringValue to updatedSet)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
internal data class AccountsExpandedDTO(
|
||||
@Json(name = "accountId")
|
||||
val accountId: String,
|
||||
@Json(name = "isExpanded")
|
||||
val isExpanded: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
package com.tangem.data.account.repository
|
||||
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth
|
||||
import com.tangem.common.test.datastore.MockStateDataStore
|
||||
import com.tangem.domain.account.models.AccountExpandedState
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.account.DerivationIndex
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.TestInstance
|
||||
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class DefaultAccountsExpandedRepositoryTest {
|
||||
|
||||
private val walletId = UserWalletId("011")
|
||||
private val mainAccountId = AccountId.forMainCryptoPortfolio(walletId)
|
||||
private val secondAccountId = AccountId.forCryptoPortfolio(walletId, DerivationIndex(1).getOrNull()!!)
|
||||
|
||||
@Test
|
||||
fun `expandedAccounts emits updated state when store changes`() = runTest {
|
||||
val dataStore = MockStateDataStore<Map<String, Set<AccountsExpandedDTO>>>(
|
||||
default = emptyMap()
|
||||
)
|
||||
|
||||
val repository = DefaultAccountsExpandedRepository(dataStore)
|
||||
|
||||
repository.expandedAccounts.test {
|
||||
// initial emission
|
||||
val initial = awaitItem()
|
||||
Truth.assertThat(initial.isEmpty()).isTrue()
|
||||
|
||||
// update store
|
||||
dataStore.updateData {
|
||||
mapOf(
|
||||
walletId.stringValue to setOf(
|
||||
AccountsExpandedDTO(
|
||||
accountId = mainAccountId.value,
|
||||
isExpanded = true
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// next emission
|
||||
val updated = awaitItem()
|
||||
val states = updated[walletId]!!
|
||||
|
||||
Truth.assertThat(states.size).isEqualTo(1)
|
||||
val state = states.first()
|
||||
|
||||
Truth.assertThat(state.accountId).isEqualTo(mainAccountId)
|
||||
Truth.assertThat(state.isExpanded).isTrue()
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expandedAccounts emits when update is called`() = runTest {
|
||||
val dataStore = MockStateDataStore<Map<String, Set<AccountsExpandedDTO>>>(
|
||||
default = emptyMap()
|
||||
)
|
||||
|
||||
val repository = DefaultAccountsExpandedRepository(dataStore)
|
||||
|
||||
val state = AccountExpandedState(
|
||||
accountId = mainAccountId,
|
||||
isExpanded = true
|
||||
)
|
||||
|
||||
repository.expandedAccounts.test {
|
||||
// initial
|
||||
awaitItem()
|
||||
|
||||
// when
|
||||
repository.update(state)
|
||||
|
||||
// then
|
||||
val updated = awaitItem()
|
||||
val states = updated[walletId]!!
|
||||
|
||||
Truth.assertThat(states.size).isEqualTo(1)
|
||||
Truth.assertThat(states.first().isExpanded).isTrue()
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `expandedAccounts emits synced state after syncStore`() = runTest {
|
||||
val dataStore = MockStateDataStore(
|
||||
mapOf(
|
||||
walletId.stringValue to setOf(
|
||||
AccountsExpandedDTO(mainAccountId.value, true),
|
||||
AccountsExpandedDTO(secondAccountId.value, false)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val repository = DefaultAccountsExpandedRepository(dataStore)
|
||||
|
||||
repository.expandedAccounts.test {
|
||||
// initial
|
||||
val initial = awaitItem()
|
||||
Truth.assertThat(initial[walletId]!!.size).isEqualTo(2)
|
||||
|
||||
// when
|
||||
repository.syncStore(
|
||||
walletId = walletId,
|
||||
existAccounts = setOf(mainAccountId) // without secondAccountId
|
||||
)
|
||||
|
||||
// then
|
||||
val synced = awaitItem()
|
||||
val states = synced[walletId]!!
|
||||
|
||||
Truth.assertThat(states.size).isEqualTo(1)
|
||||
Truth.assertThat(states.first().accountId.value).isEqualTo(mainAccountId.value)
|
||||
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.domain.account.models
|
||||
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
|
||||
data class AccountExpandedState(
|
||||
val accountId: AccountId,
|
||||
val isExpanded: Boolean,
|
||||
)
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.tangem.domain.account.repository
|
||||
|
||||
import com.tangem.domain.account.models.AccountExpandedState
|
||||
import com.tangem.domain.models.account.AccountId
|
||||
import com.tangem.domain.models.wallet.UserWalletId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AccountsExpandedRepository {
|
||||
|
||||
val expandedAccounts: Flow<Map<UserWalletId, Set<AccountExpandedState>>>
|
||||
|
||||
suspend fun syncStore(walletId: UserWalletId, existAccounts: Set<AccountId>)
|
||||
suspend fun update(accountState: AccountExpandedState)
|
||||
}
|
||||
|
|
@ -233,15 +233,13 @@ internal class WalletContentClickIntentsImplementor @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onAccountExpandClick(account: Account) {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountShowTokens())
|
||||
accountDependencies.expandedAccountsHolder.expandAccount(userWalletId, account.accountId)
|
||||
accountDependencies.expandedAccountsHolder.expandAccount(account.accountId)
|
||||
}
|
||||
|
||||
override fun onAccountCollapseClick(account: Account) {
|
||||
val userWalletId = stateHolder.getSelectedWalletId()
|
||||
analyticsEventHandler.send(MainScreenAnalyticsEvent.AccountHideTokens())
|
||||
accountDependencies.expandedAccountsHolder.collapseAccount(userWalletId, account.accountId)
|
||||
accountDependencies.expandedAccountsHolder.collapseAccount(account.accountId)
|
||||
}
|
||||
|
||||
private fun openYieldSupply(
|
||||
|
|
|
|||
|
|
@ -1,60 +1,100 @@
|
|||
package com.tangem.feature.wallet.presentation.account
|
||||
|
||||
import com.tangem.core.decompose.di.ModelScoped
|
||||
import com.tangem.domain.account.models.AccountExpandedState
|
||||
import com.tangem.domain.account.models.AccountList
|
||||
import com.tangem.domain.account.producer.SingleAccountListProducer
|
||||
import com.tangem.domain.account.repository.AccountsExpandedRepository
|
||||
import com.tangem.domain.account.supplier.SingleAccountListSupplier
|
||||
import com.tangem.domain.account.usecase.IsAccountsModeEnabledUseCase
|
||||
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 kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@ModelScoped
|
||||
internal class ExpandedAccountsHolder @Inject constructor(
|
||||
private val singleAccountListSupplier: SingleAccountListSupplier,
|
||||
private val isAccountsModeEnabledUseCase: IsAccountsModeEnabledUseCase,
|
||||
private val accountsExpandedRepository: AccountsExpandedRepository,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
) {
|
||||
|
||||
private val expandedAccounts = MutableStateFlow<Map<UserWalletId, Set<AccountId>>>(mapOf())
|
||||
private val actionChannel = MutableSharedFlow<Pair<AccountId, Boolean>>(
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||
)
|
||||
|
||||
fun expandedAccounts(userWallet: UserWallet): Flow<Set<AccountId>> = channelFlow {
|
||||
combine(
|
||||
flow = walletAccounts(userWallet),
|
||||
flow2 = isAccountsModeEnabledUseCase.invoke(),
|
||||
transform = { accountList, isAccountsMode ->
|
||||
val isSingleAccount = accountList.accounts.size == 1
|
||||
val defaultExpanded = when {
|
||||
!isAccountsMode -> setOf()
|
||||
isSingleAccount -> setOf(accountList.mainAccount.accountId)
|
||||
else -> setOf()
|
||||
val walletId = userWallet.walletId
|
||||
|
||||
val storedState = accountsExpandedRepository.expandedAccounts
|
||||
.map { it[walletId].orEmpty() }
|
||||
.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)
|
||||
|
||||
actionChannel
|
||||
.filter { (accountId, _) -> accountId.userWalletId == walletId }
|
||||
.onEach { (accountId, isExpand) ->
|
||||
val newState = AccountExpandedState(accountId, isExpand)
|
||||
launch { accountsExpandedRepository.update(newState) }
|
||||
if (isExpand) {
|
||||
expandedAccounts.update { it.plus(accountId) }
|
||||
} else {
|
||||
expandedAccounts.update { it.minus(accountId) }
|
||||
}
|
||||
expandedAccounts.update { map ->
|
||||
var expandedSet = map[userWallet.walletId] ?: defaultExpanded
|
||||
// force expand for single account
|
||||
if (isSingleAccount || !isAccountsMode) expandedSet = defaultExpanded
|
||||
map.plus(userWallet.walletId to expandedSet)
|
||||
}
|
||||
.launchIn(this)
|
||||
|
||||
walletAccounts(walletId).onEach { accountList ->
|
||||
val idsSet = accountList.accounts.mapTo(mutableSetOf()) { it.accountId }
|
||||
accountsExpandedRepository.syncStore(walletId, idsSet)
|
||||
|
||||
if (!isAccountsMode.value) return@onEach
|
||||
|
||||
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(setOf())
|
||||
}
|
||||
},
|
||||
).launchIn(this)
|
||||
).collect()
|
||||
}
|
||||
.flowOn(dispatchers.default)
|
||||
.distinctUntilChanged()
|
||||
|
||||
expandedAccounts
|
||||
.mapNotNull { map -> map[userWallet.walletId] }
|
||||
.onEach { expanded -> channel.send(expanded) }
|
||||
.collect()
|
||||
fun expandAccount(accountId: AccountId) {
|
||||
actionChannel.tryEmit(accountId to true)
|
||||
}
|
||||
|
||||
fun expandAccount(userWalletId: UserWalletId, accountId: AccountId) = expandedAccounts.update { map ->
|
||||
val expandedSet = map[userWalletId]?.plus(accountId) ?: return@update map
|
||||
map.plus(userWalletId to expandedSet)
|
||||
fun collapseAccount(accountId: AccountId) {
|
||||
actionChannel.tryEmit(accountId to false)
|
||||
}
|
||||
|
||||
fun collapseAccount(userWalletId: UserWalletId, accountId: AccountId) = expandedAccounts.update { map ->
|
||||
val expandedSet = map[userWalletId]?.minus(accountId) ?: return@update map
|
||||
map.plus(userWalletId to expandedSet)
|
||||
}
|
||||
|
||||
private fun walletAccounts(userWallet: UserWallet): Flow<AccountList> =
|
||||
singleAccountListSupplier(SingleAccountListProducer.Params(userWallet.walletId))
|
||||
private fun walletAccounts(walletId: UserWalletId): Flow<AccountList> = singleAccountListSupplier(walletId)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue