BIT-1623: Loading State Not Shown on Initial Vault Access (#1045)

This commit is contained in:
Ramsey Smith 2024-02-21 11:32:03 -07:00 committed by Álison Fernandes
parent 7b7a1d15f5
commit e6883d9599
2 changed files with 146 additions and 2 deletions

View file

@ -367,8 +367,8 @@ class VaultRepositoryImpl(
userId = userId, userId = userId,
policies = syncResponse.policies, policies = syncResponse.policies,
) )
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
settingsDiskSource.storeLastSyncTime(userId = userId, clock.instant()) settingsDiskSource.storeLastSyncTime(userId = userId, clock.instant())
vaultDiskSource.replaceVaultData(userId = userId, vault = syncResponse)
}, },
onFailure = { throwable -> onFailure = { throwable ->
updateVaultStateFlowsToError(throwable) updateVaultStateFlowsToError(throwable)
@ -1335,6 +1335,7 @@ class VaultRepositoryImpl(
onFailure = { throwable -> DataState.Error(throwable) }, onFailure = { throwable -> DataState.Error(throwable) },
) )
} }
.map { it.orLoadingIfNotSynced(userId = userId) }
.onEach { mutableCiphersStateFlow.value = it } .onEach { mutableCiphersStateFlow.value = it }
private fun observeVaultDiskDomains( private fun observeVaultDiskDomains(
@ -1368,6 +1369,7 @@ class VaultRepositoryImpl(
onFailure = { throwable -> DataState.Error(throwable) }, onFailure = { throwable -> DataState.Error(throwable) },
) )
} }
.map { it.orLoadingIfNotSynced(userId = userId) }
.onEach { mutableFoldersStateFlow.value = it } .onEach { mutableFoldersStateFlow.value = it }
private fun observeVaultDiskCollections( private fun observeVaultDiskCollections(
@ -1392,6 +1394,7 @@ class VaultRepositoryImpl(
onFailure = { throwable -> DataState.Error(throwable) }, onFailure = { throwable -> DataState.Error(throwable) },
) )
} }
.map { it.orLoadingIfNotSynced(userId = userId) }
.onEach { mutableCollectionsStateFlow.value = it } .onEach { mutableCollectionsStateFlow.value = it }
private fun observeVaultDiskSends( private fun observeVaultDiskSends(
@ -1408,10 +1411,12 @@ class VaultRepositoryImpl(
sendList = it.toEncryptedSdkSendList(), sendList = it.toEncryptedSdkSendList(),
) )
.fold( .fold(
onSuccess = { sends -> DataState.Loaded(SendData(sends)) }, onSuccess = { sends -> DataState.Loaded(sends) },
onFailure = { throwable -> DataState.Error(throwable) }, onFailure = { throwable -> DataState.Error(throwable) },
) )
} }
.map { it.orLoadingIfNotSynced(userId = userId) }
.map { dataState -> dataState.map { SendData(it) } }
.onEach { mutableSendDataStateFlow.value = it } .onEach { mutableSendDataStateFlow.value = it }
private fun updateVaultStateFlowsToError(throwable: Throwable) { private fun updateVaultStateFlowsToError(throwable: Throwable) {
@ -1442,6 +1447,20 @@ class VaultRepositoryImpl(
} }
} }
/**
* Returns the given [DataState] as-is, or [DataState.Loading] if vault data for the given
* [userId] has not synced. This can be used to distinguish between empty data in the database
* because we are in the process of syncing from legitimately having no vault data.
*/
private fun <T> DataState<List<T>>.orLoadingIfNotSynced(
userId: String,
): DataState<List<T>> =
this
.takeUnless {
settingsDiskSource.getLastSyncTime(userId = userId) == null
}
?: DataState.Loading
//region Push notification helpers //region Push notification helpers
/** /**
* Deletes the cipher specified by [syncCipherDeleteData] from disk. * Deletes the cipher specified by [syncCipherDeleteData] from disk.

View file

@ -1573,6 +1573,103 @@ class VaultRepositoryTest {
} }
} }
@Test
fun `vaultDataStateFlow should return empty when last sync time is populated`() =
runTest {
val userId = "mockId-1"
coEvery {
vaultLockManager.waitUntilUnlocked(userId = userId)
} just runs
every {
settingsDiskSource.getLastSyncTime(userId = userId)
} returns clock.instant()
fakeAuthDiskSource.userState = MOCK_USER_STATE
setupEmptyDecryptionResults()
setupVaultDiskSourceFlows(
ciphersFlow = flowOf(emptyList()),
collectionsFlow = flowOf(emptyList()),
domainsFlow = flowOf(
SyncResponseJson.Domains(
globalEquivalentDomains = emptyList(),
equivalentDomains = emptyList(),
),
),
foldersFlow = flowOf(emptyList()),
sendsFlow = flowOf(emptyList()),
)
turbineScope {
val ciphersStateFlow = vaultRepository.ciphersStateFlow.testIn(backgroundScope)
val collectionsStateFlow =
vaultRepository.collectionsStateFlow.testIn(backgroundScope)
val foldersStateFlow = vaultRepository.foldersStateFlow.testIn(backgroundScope)
val sendsStateFlow = vaultRepository.sendDataStateFlow.testIn(backgroundScope)
val domainsStateFlow = vaultRepository.domainsStateFlow.testIn(backgroundScope)
assertEquals(
DataState.Loaded(emptyList<CipherView>()),
ciphersStateFlow.awaitItem(),
)
assertEquals(
DataState.Loaded(emptyList<CollectionView>()),
collectionsStateFlow.awaitItem(),
)
assertEquals(
DataState.Loaded(emptyList<FolderView>()),
foldersStateFlow.awaitItem(),
)
assertEquals(
DataState.Loaded(SendData(sendViewList = emptyList())),
sendsStateFlow.awaitItem(),
)
assertEquals(
DataState.Loaded(
DomainsData(
equivalentDomains = emptyList(),
globalEquivalentDomains = emptyList(),
),
),
domainsStateFlow.awaitItem(),
)
}
}
@Test
fun `vaultDataStateFlow should return loading when last sync time is null`() =
runTest {
val userId = "mockId-1"
coEvery {
vaultLockManager.waitUntilUnlocked(userId = userId)
} just runs
every {
settingsDiskSource.getLastSyncTime(userId = userId)
} returns null
fakeAuthDiskSource.userState = MOCK_USER_STATE
setupEmptyDecryptionResults()
setupVaultDiskSourceFlows(
ciphersFlow = flowOf(emptyList()),
collectionsFlow = flowOf(emptyList()),
domainsFlow = flowOf(),
foldersFlow = flowOf(emptyList()),
sendsFlow = flowOf(emptyList()),
)
turbineScope {
val ciphersStateFlow = vaultRepository.ciphersStateFlow.testIn(backgroundScope)
val collectionsStateFlow =
vaultRepository.collectionsStateFlow.testIn(backgroundScope)
val foldersStateFlow = vaultRepository.foldersStateFlow.testIn(backgroundScope)
val sendsStateFlow = vaultRepository.sendDataStateFlow.testIn(backgroundScope)
val domainsStateFlow = vaultRepository.domainsStateFlow.testIn(backgroundScope)
assertEquals(DataState.Loading, ciphersStateFlow.awaitItem())
assertEquals(DataState.Loading, collectionsStateFlow.awaitItem())
assertEquals(DataState.Loading, foldersStateFlow.awaitItem())
assertEquals(DataState.Loading, sendsStateFlow.awaitItem())
assertEquals(DataState.Loading, domainsStateFlow.awaitItem())
}
}
@Test @Test
fun `getVaultFolderStateFlow should update to NoNetwork when a sync fails from no network`() = fun `getVaultFolderStateFlow should update to NoNetwork when a sync fails from no network`() =
runTest { runTest {
@ -5463,6 +5560,34 @@ class VaultRepositoryTest {
coEvery { vaultDiskSource.getSends(MOCK_USER_STATE.activeUserId) } returns sendsFlow coEvery { vaultDiskSource.getSends(MOCK_USER_STATE.activeUserId) } returns sendsFlow
} }
private fun setupEmptyDecryptionResults() {
coEvery {
vaultSdkSource.decryptCipherList(
userId = MOCK_USER_STATE.activeUserId,
cipherList = emptyList(),
)
} returns emptyList<CipherView>().asSuccess()
coEvery {
vaultSdkSource.decryptFolderList(
userId = MOCK_USER_STATE.activeUserId,
folderList = emptyList(),
)
} returns emptyList<FolderView>().asSuccess()
coEvery {
vaultSdkSource.decryptCollectionList(
userId = MOCK_USER_STATE.activeUserId,
collectionList = emptyList(),
)
} returns emptyList<CollectionView>().asSuccess()
coEvery {
vaultSdkSource.decryptSendList(
userId = MOCK_USER_STATE.activeUserId,
sendList = emptyList(),
)
} returns emptyList<SendView>().asSuccess()
}
private suspend fun setupDataStateFlow(userId: String) { private suspend fun setupDataStateFlow(userId: String) {
coEvery { coEvery {
vaultSdkSource.decryptCipherList( vaultSdkSource.decryptCipherList(