Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-14 12:20:34 +07:00
parent c425d8640d
commit a5b29a4449
8 changed files with 321 additions and 35 deletions

View file

@ -70,5 +70,6 @@ dependencies {
testImplementation(projects.common.test)
testImplementation(projects.test.core)
testRuntimeOnly(deps.test.junit5.engine)
testImplementation(deps.test.turbine)
// endregion
}

View file

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

View file

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

View file

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