Improve PIN loading spinners in regV5.

This commit is contained in:
Greyson Parrelli
2026-06-29 12:49:35 -04:00
committed by Michelle Tang
parent 91965d205a
commit b87abdabfa
8 changed files with 61 additions and 32 deletions
@@ -67,9 +67,9 @@ class PinCreationViewModel(
else -> {
Log.d(TAG, "[PinSubmitted] Confirmation PIN matched.")
_state.value = state.copy(pinMismatch = false, loading = true)
val result = applyPinSubmitted(state, event.pin)
_state.value = result.copy(loading = false)
val loadingState = state.copy(pinMismatch = false, loading = true)
_state.value = loadingState
_state.value = applyPinSubmitted(loadingState, event.pin)
}
}
}
@@ -61,10 +61,9 @@ class PinEntryForRegistrationLockViewModel(
suspend fun applyEvent(state: PinEntryState, event: PinEntryScreenEvents, parentEventEmitter: (RegistrationFlowEvent) -> Unit, stateEmitter: (PinEntryState) -> Unit) {
when (event) {
is PinEntryScreenEvents.PinEntered -> {
var localState = state.copy(loading = true)
val localState = state.copy(loading = true)
stateEmitter(localState)
localState = applyPinEntered(localState, event, parentEventEmitter)
stateEmitter(localState.copy(loading = false))
stateEmitter(applyPinEntered(localState, event, parentEventEmitter))
}
is PinEntryScreenEvents.Skip -> {
handleSkip()
@@ -92,7 +91,7 @@ class PinEntryForRegistrationLockViewModel(
return when (val error = restoreResult.error) {
is NetworkController.RestoreMasterKeyError.WrongPin -> {
Log.w(TAG, "[PinEntered] Wrong PIN. Tries remaining: ${error.triesRemaining}")
state.copy(triesRemaining = error.triesRemaining)
state.copy(loading = false, triesRemaining = error.triesRemaining)
}
is NetworkController.RestoreMasterKeyError.NoDataFound -> {
Log.w(TAG, "[PinEntered] No SVR data found. Account is locked.")
@@ -103,11 +102,11 @@ class PinEntryForRegistrationLockViewModel(
}
is RequestResult.RetryableNetworkError -> {
Log.w(TAG, "[PinEntered] Network error when restoring master key.", restoreResult.networkError)
return state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
return state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
}
is RequestResult.ApplicationError -> {
Log.w(TAG, "[PinEntered] Application error when restoring master key.", restoreResult.cause)
return state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
return state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
}
@@ -157,15 +156,15 @@ class PinEntryForRegistrationLockViewModel(
}
is NetworkController.RegisterAccountError.RateLimited -> {
Log.w(TAG, "[PinEntered] Rate limited when registering. Retry After: ${error.retryAfter}")
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.RateLimited(error.retryAfter))
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.RateLimited(error.retryAfter))
}
is NetworkController.RegisterAccountError.InvalidRequest -> {
Log.w(TAG, "[PinEntered] Invalid request when registering: ${error.message}")
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
is NetworkController.RegisterAccountError.DeviceTransferPossible -> {
Log.w(TAG, "[PinEntered] Device transfer possible. This shouldn't happen when skipDeviceTransfer is true.")
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> {
Log.w(TAG, "[PinEntered] Registration recovery password incorrect: ${error.message}")
@@ -176,11 +175,11 @@ class PinEntryForRegistrationLockViewModel(
}
is RequestResult.RetryableNetworkError -> {
Log.w(TAG, "[PinEntered] Network error when registering.", registerResult.networkError)
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
}
is RequestResult.ApplicationError -> {
Log.w(TAG, "[PinEntered] Application error when registering.", registerResult.cause)
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
}
}
@@ -68,10 +68,9 @@ class PinEntryForSmsBypassViewModel(
) {
when (event) {
is PinEntryScreenEvents.PinEntered -> {
var localState = state.copy(loading = true)
val localState = state.copy(loading = true)
stateEmitter(localState)
localState = applyPinEntered(localState, event, parentEventEmitter)
stateEmitter(localState.copy(loading = false))
stateEmitter(applyPinEntered(localState, event, parentEventEmitter))
}
is PinEntryScreenEvents.Skip -> {
handleSkip()
@@ -112,7 +111,7 @@ class PinEntryForSmsBypassViewModel(
when (val error = result.error) {
is NetworkController.RestoreMasterKeyError.WrongPin -> {
Log.w(TAG, "[PinEntered] Wrong PIN. Tries remaining: ${error.triesRemaining}")
state.copy(triesRemaining = error.triesRemaining)
state.copy(loading = false, triesRemaining = error.triesRemaining)
}
is NetworkController.RestoreMasterKeyError.NoDataFound -> {
Log.w(TAG, "[PinEntered] No SVR data found for sms-bypass credential. Marking RRP as invalid and navigating back.")
@@ -124,11 +123,11 @@ class PinEntryForSmsBypassViewModel(
}
is RequestResult.RetryableNetworkError -> {
Log.w(TAG, "[PinEntered] Network error when restoring master key (sms-bypass).", result.networkError)
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
}
is RequestResult.ApplicationError -> {
Log.w(TAG, "[PinEntered] Application error when restoring master key (sms-bypass).", result.cause)
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
}
}
@@ -157,10 +156,10 @@ class PinEntryForSmsBypassViewModel(
state
}
is RequestResult.RetryableNetworkError -> {
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
}
is RequestResult.ApplicationError -> {
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
is RequestResult.NonSuccess -> {
when (val error = result.error) {
@@ -177,7 +176,7 @@ class PinEntryForSmsBypassViewModel(
}
is NetworkController.RegisterAccountError.RateLimited -> {
Log.w(TAG, "[Register] Rate limited (retryAfter: ${error.retryAfter}).")
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.RateLimited(error.retryAfter))
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.RateLimited(error.retryAfter))
}
is NetworkController.RegisterAccountError.RegistrationLock -> {
if (provideRegistrationLock) {
@@ -64,10 +64,9 @@ class PinEntryForSvrRestoreViewModel(
) {
when (event) {
is PinEntryScreenEvents.PinEntered -> {
var localState = state.copy(loading = true)
val localState = state.copy(loading = true)
stateEmitter(localState)
localState = applyPinEntered(localState, event, parentEventEmitter)
stateEmitter(localState.copy(loading = false))
stateEmitter(applyPinEntered(localState, event, parentEventEmitter))
}
is PinEntryScreenEvents.Skip -> {
handleSkip()
@@ -114,10 +113,10 @@ class PinEntryForSvrRestoreViewModel(
}
}
is RequestResult.RetryableNetworkError -> {
return state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
return state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
}
is RequestResult.ApplicationError -> {
return state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
return state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
}
@@ -134,21 +133,21 @@ class PinEntryForSvrRestoreViewModel(
when (val error = result.error) {
is NetworkController.RestoreMasterKeyError.WrongPin -> {
Log.w(TAG, "[PinEntered] Wrong PIN. Tries remaining: ${error.triesRemaining}")
state.copy(triesRemaining = error.triesRemaining)
state.copy(loading = false, triesRemaining = error.triesRemaining)
}
is NetworkController.RestoreMasterKeyError.NoDataFound -> {
Log.w(TAG, "[PinEntered] No SVR data found. Prompting user to create a new PIN.")
state.copy(showNoDataToRestoreDialog = true)
state.copy(loading = false, showNoDataToRestoreDialog = true)
}
}
}
is RequestResult.RetryableNetworkError -> {
Log.w(TAG, "[PinEntered] Network error when restoring master key.", result.networkError)
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.NetworkError)
}
is RequestResult.ApplicationError -> {
Log.w(TAG, "[PinEntered] Application error when restoring master key.", result.cause)
state.copy(oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
}
}
@@ -125,6 +125,7 @@ class PinCreationViewModelTest {
@Test
fun `matching confirmation PIN with valid AEP and successful SVR backup hands off to finishRegistrationOrCreateProfile`() = runTest(testDispatcher) {
val states = collectStates()
val aep = AccountEntropyPool.generate()
val confirmState = PinCreationState(accountEntropyPool = aep, isConfirmEnabled = true, firstPin = "123456")
@@ -135,6 +136,7 @@ class PinCreationViewModelTest {
coVerify { mockRepository.setRestoreDecision(RestoreDecision.NEW_ACCOUNT) }
coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) }
assertThat(states.last().loading).isTrue()
}
// ==================== PinSubmitted Missing AEP Test ====================
@@ -86,6 +86,7 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
assertThat(emittedParentEvents[1]).isInstanceOf<RegistrationFlowEvent.Registered>()
coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) }
assertThat(emittedStates.last().loading).isEqualTo(true)
}
@Test
@@ -102,6 +103,7 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().triesRemaining).isEqualTo(triesRemaining)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -122,6 +124,7 @@ class PinEntryForRegistrationLockViewModelTest {
.isInstanceOf<RegistrationRoute.AccountLocked>()
.prop(RegistrationRoute.AccountLocked::timeRemainingMs)
.isEqualTo(testTimeRemaining)
assertThat(emittedStates.last().loading).isEqualTo(true)
}
@Test
@@ -135,6 +138,7 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -148,6 +152,7 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -233,6 +238,7 @@ class PinEntryForRegistrationLockViewModelTest {
.isInstanceOf<PinEntryState.OneTimeEvent.RateLimited>()
.prop(PinEntryState.OneTimeEvent.RateLimited::retryAfter)
.isEqualTo(retryAfter)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -252,6 +258,7 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -271,6 +278,7 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -288,6 +296,7 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -305,6 +314,7 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
// ==================== Skip Tests ====================
@@ -79,6 +79,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) }
assertThat(emittedStates.last().loading).isEqualTo(true)
}
@Test
@@ -110,6 +111,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().triesRemaining).isEqualTo(triesRemaining)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -126,6 +128,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(2)
assertThat(emittedParentEvents[0]).isEqualTo(RegistrationFlowEvent.RecoveryPasswordInvalid)
assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.NavigateBack)
assertThat(emittedStates.last().loading).isEqualTo(true)
}
@Test
@@ -139,6 +142,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -152,6 +156,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -162,6 +167,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isEqualTo(RegistrationFlowEvent.ResetState)
assertThat(emittedStates.last().loading).isEqualTo(true)
}
// ==================== Registration Error Tests ====================
@@ -181,6 +187,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -198,6 +205,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -217,6 +225,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(2)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.ResetState)
assertThat(emittedStates.last().loading).isEqualTo(true)
}
@Test
@@ -260,6 +269,7 @@ class PinEntryForSmsBypassViewModelTest {
.isInstanceOf<PinEntryState.OneTimeEvent.RateLimited>()
.prop(PinEntryState.OneTimeEvent.RateLimited::retryAfter)
.isEqualTo(retryAfter)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -283,6 +293,7 @@ class PinEntryForSmsBypassViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) }
assertThat(emittedStates.last().loading).isEqualTo(true)
}
@Test
@@ -78,6 +78,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents[0]).isInstanceOf<RegistrationFlowEvent.MasterKeyRestoredFromSvr>()
coVerify { mockRepository.setRestoreDecision(RestoreDecision.COMPLETED) }
coVerify { mockRepository.finishRegistrationOrCreateProfile(parentEventEmitter, any()) }
assertThat(emittedStates.last().loading).isEqualTo(true)
}
// ==================== GetSvrCredentials Error Tests ====================
@@ -95,6 +96,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState)
assertThat(emittedStates.last().loading).isEqualTo(true)
}
@Test
@@ -110,6 +112,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents).hasSize(1)
assertThat(emittedParentEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState)
assertThat(emittedStates.last().loading).isEqualTo(true)
}
@Test
@@ -123,6 +126,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -136,6 +140,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
// ==================== RestoreMasterKey Error Tests ====================
@@ -160,6 +165,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().triesRemaining).isEqualTo(triesRemaining)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -181,6 +187,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().showNoDataToRestoreDialog).isEqualTo(true)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
// ==================== No Data To Restore Dialog Tests ====================
@@ -226,6 +233,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.NetworkError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
@Test
@@ -245,6 +253,7 @@ class PinEntryForSvrRestoreViewModelTest {
assertThat(emittedParentEvents).hasSize(0)
assertThat(emittedStates.last().oneTimeEvent).isEqualTo(PinEntryState.OneTimeEvent.UnknownError)
assertThat(emittedStates.last().loading).isEqualTo(false)
}
// ==================== Skip Tests ====================