diff --git a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameViewModel.kt index 35a5275775..134484812a 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/addusername/AddUsernameViewModel.kt @@ -126,6 +126,9 @@ class AddUsernameViewModel( * Non-digits are dropped as they're typed, since a discriminator can only ever be digits. * * Emptying the field hands control back to the service once the field loses focus (see [applyDiscriminatorFocusLost]). + * Until then we hold onto a service-assigned reservation, since that pairing is still the one the entry is showing. + * A user-typed reservation, on the other hand, is dropped as soon as its digits are erased -- the user asked for that + * exact number and no longer wants it, so there is nothing left to submit until the field settles. */ private fun applyDiscriminatorChanged(state: AddUsernameState, discriminator: String, stateEmitter: (AddUsernameState) -> Unit) { val digitsOnly = discriminator.filter { it in '0'..'9' } @@ -141,7 +144,7 @@ class AddUsernameViewModel( discriminator = digitsOnly, isDiscriminatorUserSet = isUserSet, validationError = null, - reservation = if (isUserSet) null else state.reservation, + reservation = state.reservation.takeIf { !isUserSet && !state.isDiscriminatorUserSet }, isReserving = false ) diff --git a/feature/registration/src/test/java/org/signal/registration/screens/addusername/AddUsernameViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/addusername/AddUsernameViewModelTest.kt index 068b715f63..81046dd533 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/addusername/AddUsernameViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/addusername/AddUsernameViewModelTest.kt @@ -205,6 +205,42 @@ class AddUsernameViewModelTest { assertThat(viewModel.state.value.isSubmittable).isTrue() } + @Test + fun `emptying a user-typed discriminator drops its reservation`() = runTest(testDispatcher) { + coEvery { mockRepository.reserveUsername("maya") } returns RequestResult.Success(Username("maya.45")) + coEvery { mockRepository.reserveUsername("maya", "77") } returns RequestResult.Success(Username("maya.77")) + + viewModel.onEvent(AddUsernameScreenEvents.UsernameChanged("maya")) + advanceUntilIdle() + viewModel.onEvent(AddUsernameScreenEvents.DiscriminatorChanged("77")) + advanceUntilIdle() + viewModel.onEvent(AddUsernameScreenEvents.DiscriminatorChanged("")) + advanceUntilIdle() + + assertThat(viewModel.state.value.discriminator).isEmpty() + assertThat(viewModel.state.value.isDiscriminatorUserSet).isFalse() + assertThat(viewModel.state.value.reservation).isNull() + assertThat(viewModel.state.value.isSubmittable).isFalse() + } + + @Test + fun `NextClicked cannot confirm a discriminator the user just erased`() = runTest(testDispatcher) { + coEvery { mockRepository.reserveUsername("maya") } returns RequestResult.Success(Username("maya.45")) + coEvery { mockRepository.reserveUsername("maya", "77") } returns RequestResult.Success(Username("maya.77")) + + viewModel.onEvent(AddUsernameScreenEvents.UsernameChanged("maya")) + advanceUntilIdle() + viewModel.onEvent(AddUsernameScreenEvents.DiscriminatorChanged("77")) + advanceUntilIdle() + viewModel.onEvent(AddUsernameScreenEvents.DiscriminatorChanged("")) + advanceUntilIdle() + viewModel.onEvent(AddUsernameScreenEvents.NextClicked) + advanceUntilIdle() + + coVerify(exactly = 0) { mockRepository.confirmUsername(any()) } + assertThat(parentEvents).isEmpty() + } + @Test fun `losing focus with an empty discriminator hands it back to the service`() = runTest(testDispatcher) { coEvery { mockRepository.reserveUsername("maya") } returns RequestResult.Success(Username("maya.45")) andThen RequestResult.Success(Username("maya.99"))