diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt
index 1c4b4c6d53..85bae65a03 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryScreen.kt
@@ -185,7 +185,11 @@ fun PhoneNumberScreen(
onEvent(PhoneNumberEntryScreenEvents.ConsumeOneTimeEvent)
when (state.oneTimeEvent) {
OneTimeEvent.NetworkError -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__network_error)
- is OneTimeEvent.RateLimited -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.oneTimeEvent.retryAfter.toString())
+ is OneTimeEvent.RateLimited -> simpleErrorMessage = if (state.oneTimeEvent.retryAfter.isPositive()) {
+ resources.getString(R.string.VerificationCodeScreen__too_many_attempts_try_again_in_s, state.oneTimeEvent.retryAfter.toString())
+ } else {
+ resources.getString(R.string.VerificationCodeScreen__too_many_attempts)
+ }
OneTimeEvent.UnknownError -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__an_unexpected_error_occurred)
OneTimeEvent.CouldNotRequestCodeWithSelectedTransport -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__could_not_send_code_via_selected_method)
OneTimeEvent.UnableToSendSms -> simpleErrorMessage = resources.getString(R.string.VerificationCodeScreen__unable_to_send_sms)
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt
index 0d6d4725c5..582d51b362 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreen.kt
@@ -51,6 +51,7 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
@@ -92,11 +93,38 @@ fun PinCreationScreen(
) {
val activePin = remember { mutableStateOf("") }
val canSubmitPin = activePin.value.length >= 4
+ val resources = LocalResources.current
+ var errorMessage: String? by remember { mutableStateOf(null) }
BackHandler(enabled = state.isConfirmEnabled) {
onEvent(PinCreationScreenEvents.BackToPinEntry)
}
+ LaunchedEffect(state.oneTimeEvent) {
+ val event = state.oneTimeEvent ?: return@LaunchedEffect
+ onEvent(PinCreationScreenEvents.ConsumeOneTimeEvent)
+ errorMessage = when (event) {
+ is PinCreationState.OneTimeEvent.ServiceError -> {
+ resources.getString(R.string.PinCreationScreen__service_error)
+ }
+ is PinCreationState.OneTimeEvent.NetworkError -> {
+ if (event.retryAfter != null) {
+ resources.getString(R.string.PinCreationScreen__network_error_try_again_in_s, event.retryAfter.toString())
+ } else {
+ resources.getString(R.string.PinCreationScreen__network_error)
+ }
+ }
+ }
+ }
+
+ errorMessage?.let { message ->
+ Dialogs.SimpleMessageDialog(
+ message = message,
+ dismiss = stringResource(android.R.string.ok),
+ onDismiss = { errorMessage = null }
+ )
+ }
+
when (val params = RegistrationScaffold.rememberLayoutParams()) {
is RegistrationScaffold.Params.OnePane -> OnePaneLayout(
params = params,
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreenEvents.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreenEvents.kt
index 49b36472ad..5f5a6b029e 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreenEvents.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationScreenEvents.kt
@@ -13,4 +13,5 @@ sealed class PinCreationScreenEvents {
data object LearnMore : PinCreationScreenEvents()
data object OptOut : PinCreationScreenEvents()
data object BackToPinEntry : PinCreationScreenEvents()
+ data object ConsumeOneTimeEvent : PinCreationScreenEvents()
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt
index 238f7949a8..a817211fe8 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationState.kt
@@ -7,6 +7,7 @@ package org.signal.registration.screens.pincreation
import org.signal.core.models.AccountEntropyPool
import org.signal.core.util.censor
+import kotlin.time.Duration
data class PinCreationState(
val isAlphanumericKeyboard: Boolean = false,
@@ -14,9 +15,15 @@ data class PinCreationState(
val pinMismatch: Boolean = false,
val loading: Boolean = false,
val firstPin: String? = null,
- val accountEntropyPool: AccountEntropyPool? = null
+ val accountEntropyPool: AccountEntropyPool? = null,
+ val oneTimeEvent: OneTimeEvent? = null
) {
override fun toString(): String {
- return "PinCreationState(isAlphanumericKeyboard=$isAlphanumericKeyboard, isConfirmEnabled=$isConfirmEnabled, pinMismatch=$pinMismatch, loading=$loading, firstPin=${firstPin?.let { "${it.length} chars" }}, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()})"
+ return "PinCreationState(isAlphanumericKeyboard=$isAlphanumericKeyboard, isConfirmEnabled=$isConfirmEnabled, pinMismatch=$pinMismatch, loading=$loading, firstPin=${firstPin?.let { "${it.length} chars" }}, accountEntropyPool=${accountEntropyPool?.displayValue?.censor()}, oneTimeEvent=$oneTimeEvent)"
+ }
+
+ sealed interface OneTimeEvent {
+ data object ServiceError : OneTimeEvent
+ data class NetworkError(val retryAfter: Duration?) : OneTimeEvent
}
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt
index e6a44b3218..fd95892bdd 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pincreation/PinCreationViewModel.kt
@@ -23,6 +23,7 @@ import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RestoreDecision
import org.signal.registration.screens.EventDrivenViewModel
+import kotlin.time.toKotlinDuration
/**
* ViewModel for the PIN creation screen.
@@ -91,6 +92,9 @@ class PinCreationViewModel(
_state.value = state.copy(isConfirmEnabled = false)
applyOptOut()
}
+ is PinCreationScreenEvents.ConsumeOneTimeEvent -> {
+ _state.value = state.copy(oneTimeEvent = null)
+ }
}
}
@@ -129,8 +133,7 @@ class PinCreationViewModel(
when (val error = result.error) {
is NetworkController.BackupMasterKeyError.EnclaveNotFound -> {
Log.w(TAG, "[PinSubmitted] SVR enclave not found.")
- // TODO [registration] - Report to UI and indicate to library user that pin could not be created
- throw NotImplementedError("Report to UI and indicate to library user that pin could not be created")
+ state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError)
}
is NetworkController.BackupMasterKeyError.NotRegistered -> {
@@ -143,14 +146,12 @@ class PinCreationViewModel(
is RequestResult.RetryableNetworkError -> {
Log.w(TAG, "[PinSubmitted] Network error when backing up master key.", result.networkError)
- // TODO [registration] - Report to UI and indicate to library user that pin could not be created
- throw NotImplementedError("Report to UI and indicate to library user that pin could not be created")
+ state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.NetworkError(result.retryAfter?.toKotlinDuration()))
}
is RequestResult.ApplicationError -> {
Log.w(TAG, "[PinSubmitted] Application error when backing up master key.", result.cause)
- // TODO [registration] - Report to UI and indicate to library user that pin could not be created
- throw NotImplementedError("Report to UI and indicate to library user that pin could not be created")
+ state.copy(loading = false, oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError)
}
}
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt
index b2fd928ea7..49e8077102 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModel.kt
@@ -23,6 +23,7 @@ import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RegistrationRoute
import org.signal.registration.screens.EventDrivenViewModel
+import org.signal.registration.screens.util.navigateBack
import org.signal.registration.screens.util.navigateTo
/**
@@ -90,7 +91,13 @@ class PinEntryForRegistrationLockViewModel(
return when (val error = restoreResult.error) {
is NetworkController.RestoreMasterKeyError.WrongPin -> {
Log.w(TAG, "[PinEntered] Wrong PIN. Tries remaining: ${error.triesRemaining}")
- state.copy(loading = false, triesRemaining = error.triesRemaining)
+ if (error.triesRemaining <= 0) {
+ Log.w(TAG, "[PinEntered] Out of PIN attempts. Account is locked.")
+ parentEventEmitter.navigateTo(RegistrationRoute.AccountLocked(timeRemainingMs = timeRemaining))
+ state
+ } else {
+ state.copy(loading = false, triesRemaining = error.triesRemaining)
+ }
}
is NetworkController.RestoreMasterKeyError.NoDataFound -> {
Log.w(TAG, "[PinEntered] No SVR data found. Account is locked.")
@@ -144,9 +151,9 @@ class PinEntryForRegistrationLockViewModel(
is RequestResult.NonSuccess -> {
when (val error = registerResult.error) {
is NetworkController.RegisterAccountError.SessionNotFoundOrNotVerified -> {
- Log.w(TAG, "[PinEntered] Session not found or verified: ${error.message}")
- // TODO [registration] - Handle session not found or verified.
- throw NotImplementedError("Handle session not found or verified")
+ Log.w(TAG, "[PinEntered] Session not found or verified: ${error.message}. Resetting.")
+ parentEventEmitter(RegistrationFlowEvent.ResetState)
+ state
}
is NetworkController.RegisterAccountError.RegistrationLock -> {
Log.w(TAG, "[PinEntered] Still getting registration lock error after providing token. This shouldn't happen. Resetting state.")
@@ -166,9 +173,10 @@ class PinEntryForRegistrationLockViewModel(
state.copy(loading = false, oneTimeEvent = PinEntryState.OneTimeEvent.UnknownError)
}
is NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect -> {
- Log.w(TAG, "[PinEntered] Registration recovery password incorrect: ${error.message}")
- // TODO [registration] - Handle incorrect password
- throw NotImplementedError("Handle incorrect password")
+ Log.w(TAG, "[PinEntered] Registration recovery password incorrect: ${error.message}. Marking recovery password invalid and navigating back.")
+ parentEventEmitter(RegistrationFlowEvent.RecoveryPasswordInvalid)
+ parentEventEmitter.navigateBack()
+ state
}
}
}
diff --git a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt
index 5ab7cf7e81..1a60522f6f 100644
--- a/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt
+++ b/feature/registration/src/main/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModel.kt
@@ -307,9 +307,8 @@ class VerificationCodeViewModel(
return state.copy(oneTimeEvent = OneTimeEvent.IncorrectVerificationCode, incorrectCodeAttempts = newAttempts, digits = VerificationCodeState.emptyDigits(), focusedDigitIndex = 0)
}
is NetworkController.SubmitVerificationCodeError.SessionNotFound -> {
- Log.w(TAG, "[SubmitCode] Session not found: ${error.message}")
- // TODO don't start over, go back to phone number entry
- parentEventEmitter(RegistrationFlowEvent.ResetState)
+ Log.w(TAG, "[SubmitCode] Session not found: ${error.message}. Navigating back to phone number entry.")
+ parentEventEmitter.navigateBack()
return state
}
is NetworkController.SubmitVerificationCodeError.SessionAlreadyVerifiedOrNoCodeRequested -> {
@@ -365,8 +364,9 @@ class VerificationCodeViewModel(
is RequestResult.NonSuccess -> {
when (val error = registerResult.error) {
is NetworkController.RegisterAccountError.SessionNotFoundOrNotVerified -> {
- // TODO [registration] Handle session not found or not verified case.
- throw NotImplementedError("Handle session not found or not verified case.")
+ Log.w(TAG, "[Register] Session not found or not verified: ${error.message}. Navigating back to phone number entry.")
+ parentEventEmitter.navigateBack()
+ state
}
is NetworkController.RegisterAccountError.DeviceTransferPossible -> {
error("[Register] Got told a device transfer is possible. We should never get into this state. Resetting.")
@@ -454,9 +454,8 @@ class VerificationCodeViewModel(
)
}
is NetworkController.RequestVerificationCodeError.InvalidSessionId -> {
- Log.w(TAG, "[RequestCode][$transport] Invalid session ID: ${error.message}")
- // TODO don't start over, go back to phone number entry
- parentEventEmitter(RegistrationFlowEvent.ResetState)
+ Log.w(TAG, "[RequestCode][$transport] Invalid session ID: ${error.message}. Navigating back to phone number entry.")
+ parentEventEmitter.navigateBack()
state
}
is NetworkController.RequestVerificationCodeError.MissingRequestInformationOrAlreadyVerified -> {
@@ -469,9 +468,8 @@ class VerificationCodeViewModel(
)
}
is NetworkController.RequestVerificationCodeError.SessionNotFound -> {
- Log.w(TAG, "[RequestCode][$transport] Session not found: ${error.message}")
- // TODO don't start over, go back to phone number entry
- parentEventEmitter(RegistrationFlowEvent.ResetState)
+ Log.w(TAG, "[RequestCode][$transport] Session not found: ${error.message}. Navigating back to phone number entry.")
+ parentEventEmitter.navigateBack()
state
}
is NetworkController.RequestVerificationCodeError.ThirdPartyServiceError -> {
diff --git a/feature/registration/src/main/res/values/strings.xml b/feature/registration/src/main/res/values/strings.xml
index 18673a7a8b..1d5bc3e874 100644
--- a/feature/registration/src/main/res/values/strings.xml
+++ b/feature/registration/src/main/res/values/strings.xml
@@ -91,6 +91,8 @@
Unable to connect. Please check your network and try again.
Too many attempts. Try again in %s.
+
+ Too many attempts. Please try again later.
An unexpected error occurred. Please try again.
@@ -361,6 +363,12 @@
Disable PIN
Cancel
+
+ There was an issue connecting to the service.
+
+ Encountered a network error.
+
+ Encountered a network error. You can try again in %1$s.
Registration Lock
diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt
index 14e9aa28fa..19bed9d14d 100644
--- a/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt
+++ b/feature/registration/src/test/java/org/signal/registration/screens/pincreation/PinCreationViewModelTest.kt
@@ -9,8 +9,11 @@ import assertk.assertThat
import assertk.assertions.hasSize
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
+import assertk.assertions.isInstanceOf
+import assertk.assertions.isNotNull
import assertk.assertions.isNull
import assertk.assertions.isTrue
+import assertk.assertions.prop
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.mockk
@@ -34,6 +37,9 @@ import org.signal.registration.RegistrationFlowEvent
import org.signal.registration.RegistrationFlowState
import org.signal.registration.RegistrationRepository
import org.signal.registration.RestoreDecision
+import java.io.IOException
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.toJavaDuration
@OptIn(ExperimentalCoroutinesApi::class)
class PinCreationViewModelTest {
@@ -167,6 +173,68 @@ class PinCreationViewModelTest {
assertThat(emittedParentEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState)
}
+ @Test
+ fun `PinSubmitted with EnclaveNotFound error surfaces a service error and stops loading`() = runTest(testDispatcher) {
+ val states = collectStates()
+ val aep = AccountEntropyPool.generate()
+ val confirmState = PinCreationState(accountEntropyPool = aep, isConfirmEnabled = true, firstPin = "123456")
+
+ coEvery { mockRepository.setNewlyCreatedPin(any(), any(), any()) } returns
+ RequestResult.NonSuccess(NetworkController.BackupMasterKeyError.EnclaveNotFound)
+
+ viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456"))
+
+ assertThat(emittedParentEvents).hasSize(0)
+ assertThat(states.last().oneTimeEvent).isEqualTo(PinCreationState.OneTimeEvent.ServiceError)
+ assertThat(states.last().loading).isFalse()
+ }
+
+ @Test
+ fun `PinSubmitted with application error surfaces a service error and stops loading`() = runTest(testDispatcher) {
+ val states = collectStates()
+ val aep = AccountEntropyPool.generate()
+ val confirmState = PinCreationState(accountEntropyPool = aep, isConfirmEnabled = true, firstPin = "123456")
+
+ coEvery { mockRepository.setNewlyCreatedPin(any(), any(), any()) } returns
+ RequestResult.ApplicationError(RuntimeException("Unexpected"))
+
+ viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456"))
+
+ assertThat(emittedParentEvents).hasSize(0)
+ assertThat(states.last().oneTimeEvent).isEqualTo(PinCreationState.OneTimeEvent.ServiceError)
+ assertThat(states.last().loading).isFalse()
+ }
+
+ @Test
+ fun `PinSubmitted with retryable network error surfaces a network error with retryAfter`() = runTest(testDispatcher) {
+ val states = collectStates()
+ val aep = AccountEntropyPool.generate()
+ val confirmState = PinCreationState(accountEntropyPool = aep, isConfirmEnabled = true, firstPin = "123456")
+ val retryAfter = 30.seconds
+
+ coEvery { mockRepository.setNewlyCreatedPin(any(), any(), any()) } returns
+ RequestResult.RetryableNetworkError(IOException("Network error"), retryAfter.toJavaDuration())
+
+ viewModel.applyEvent(confirmState, PinCreationScreenEvents.PinSubmitted("123456"))
+
+ assertThat(emittedParentEvents).hasSize(0)
+ assertThat(states.last().oneTimeEvent).isNotNull()
+ .isInstanceOf()
+ .prop(PinCreationState.OneTimeEvent.NetworkError::retryAfter)
+ .isEqualTo(retryAfter)
+ assertThat(states.last().loading).isFalse()
+ }
+
+ @Test
+ fun `ConsumeOneTimeEvent clears the one-time event`() = runTest(testDispatcher) {
+ val states = collectStates()
+ val stateWithEvent = PinCreationState(oneTimeEvent = PinCreationState.OneTimeEvent.ServiceError)
+
+ viewModel.applyEvent(stateWithEvent, PinCreationScreenEvents.ConsumeOneTimeEvent)
+
+ assertThat(states.last().oneTimeEvent).isNull()
+ }
+
// ==================== OptOut Tests ====================
@Test
diff --git a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt
index a2375182a9..9c86d0c1c7 100644
--- a/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt
+++ b/feature/registration/src/test/java/org/signal/registration/screens/pinentry/PinEntryForRegistrationLockViewModelTest.kt
@@ -106,6 +106,26 @@ class PinEntryForRegistrationLockViewModelTest {
assertThat(emittedStates.last().loading).isEqualTo(false)
}
+ @Test
+ fun `PinEntered with wrong PIN and no tries remaining navigates to AccountLocked`() = runTest {
+ val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock)
+
+ coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), forRegistrationLock = true) } returns
+ RequestResult.NonSuccess(
+ NetworkController.RestoreMasterKeyError.WrongPin(0)
+ )
+
+ viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("wrong-pin"), parentEventEmitter, stateEmitter)
+
+ assertThat(emittedParentEvents).hasSize(1)
+ assertThat(emittedParentEvents.first())
+ .isInstanceOf()
+ .prop(RegistrationFlowEvent.NavigateToScreen::route)
+ .isInstanceOf()
+ .prop(RegistrationRoute.AccountLocked::timeRemainingMs)
+ .isEqualTo(testTimeRemaining)
+ }
+
@Test
fun `PinEntered with no SVR data navigates to AccountLocked`() = runTest {
val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock)
@@ -197,6 +217,45 @@ class PinEntryForRegistrationLockViewModelTest {
// ==================== Registration Error Tests ====================
+ @Test
+ fun `PinEntered with session not found during registration emits ResetState`() = runTest {
+ val masterKey = mockk(relaxed = true)
+ val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock)
+
+ coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), forRegistrationLock = true) } returns
+ RequestResult.Success(NetworkController.MasterKeyResponse(masterKey))
+ coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
+ RequestResult.NonSuccess(
+ NetworkController.RegisterAccountError.SessionNotFoundOrNotVerified("Session not found")
+ )
+
+ viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
+
+ assertThat(emittedParentEvents).hasSize(2)
+ assertThat(emittedParentEvents[0]).isInstanceOf()
+ assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.ResetState)
+ }
+
+ @Test
+ fun `PinEntered with recovery password incorrect during registration marks it invalid and navigates back`() = runTest {
+ val masterKey = mockk(relaxed = true)
+ val initialState = PinEntryState(mode = PinEntryState.Mode.RegistrationLock)
+
+ coEvery { mockRepository.restoreMasterKeyFromSvr(any(), any(), forRegistrationLock = true) } returns
+ RequestResult.Success(NetworkController.MasterKeyResponse(masterKey))
+ coEvery { mockRepository.registerAccountWithSession(any(), any(), any(), any()) } returns
+ RequestResult.NonSuccess(
+ NetworkController.RegisterAccountError.RegistrationRecoveryPasswordIncorrect("Wrong password")
+ )
+
+ viewModel.applyEvent(initialState, PinEntryScreenEvents.PinEntered("123456"), parentEventEmitter, stateEmitter)
+
+ assertThat(emittedParentEvents).hasSize(3)
+ assertThat(emittedParentEvents[0]).isInstanceOf()
+ assertThat(emittedParentEvents[1]).isEqualTo(RegistrationFlowEvent.RecoveryPasswordInvalid)
+ assertThat(emittedParentEvents[2]).isEqualTo(RegistrationFlowEvent.NavigateBack)
+ }
+
@Test
fun `PinEntered with registration lock error during registration emits ResetState`() = runTest {
val masterKey = mockk(relaxed = true)
diff --git a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt
index 5a22e381da..293403a8a3 100644
--- a/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt
+++ b/feature/registration/src/test/java/org/signal/registration/screens/verificationcode/VerificationCodeViewModelTest.kt
@@ -579,7 +579,7 @@ class VerificationCodeViewModelTest {
}
@Test
- fun `CodeEntered with session not found emits ResetState`() = runTest {
+ fun `CodeEntered with session not found navigates back to phone number entry`() = runTest {
val sessionMetadata = createSessionMetadata()
val initialState = VerificationCodeState(
sessionMetadata = sessionMetadata,
@@ -594,7 +594,7 @@ class VerificationCodeViewModelTest {
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.CodeEntered("123456"), stateEmitter)
assertThat(emittedEvents).hasSize(1)
- assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState)
+ assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.NavigateBack)
}
@Test
@@ -953,7 +953,7 @@ class VerificationCodeViewModelTest {
}
@Test
- fun `ResendSms with InvalidSessionId emits ResetState`() = runTest {
+ fun `ResendSms with InvalidSessionId navigates back to phone number entry`() = runTest {
val sessionMetadata = createSessionMetadata()
val initialState = VerificationCodeState(sessionMetadata = sessionMetadata)
@@ -965,11 +965,11 @@ class VerificationCodeViewModelTest {
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
assertThat(emittedEvents).hasSize(1)
- assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState)
+ assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.NavigateBack)
}
@Test
- fun `ResendSms with SessionNotFound emits ResetState`() = runTest {
+ fun `ResendSms with SessionNotFound navigates back to phone number entry`() = runTest {
val sessionMetadata = createSessionMetadata()
val initialState = VerificationCodeState(sessionMetadata = sessionMetadata)
@@ -981,7 +981,7 @@ class VerificationCodeViewModelTest {
viewModel.applyEvent(initialState, VerificationCodeScreenEvents.ResendSms, stateEmitter)
assertThat(emittedEvents).hasSize(1)
- assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.ResetState)
+ assertThat(emittedEvents.first()).isEqualTo(RegistrationFlowEvent.NavigateBack)
}
@Test