mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
BIT-1623: Loading State Not Shown on Initial Vault Access (#1045)
This commit is contained in:
parent
7b7a1d15f5
commit
e6883d9599
2 changed files with 146 additions and 2 deletions
|
@ -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.
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue