Remove unnecessary leading digits in phone number.

This commit is contained in:
Greyson Parrelli
2026-07-22 12:47:56 -04:00
committed by Michelle Tang
parent dfe6ae812d
commit 7a2c084b49
2 changed files with 99 additions and 3 deletions
@@ -117,7 +117,8 @@ class PhoneNumberEntryViewModel(
stateEmitter(applyPhoneNumberChanged(state, event.oldValue, event.newValue))
}
is PhoneNumberEntryScreenEvents.NextClicked -> {
stateEmitter(state.copy(dialogs = state.dialogs.copy(confirmNumber = true)))
val normalized = state.withNormalizedNationalNumber()
stateEmitter(normalized.copy(dialogs = normalized.dialogs.copy(confirmNumber = true)))
}
is PhoneNumberEntryScreenEvents.PhoneNumberCancelled -> {
stateEmitter(state.copy(dialogs = state.dialogs.copy(confirmNumber = false)))
@@ -275,8 +276,8 @@ class PhoneNumberEntryViewModel(
inputState: PhoneNumberEntryState,
parentEventEmitter: (RegistrationFlowEvent) -> Unit
): PhoneNumberEntryState {
val e164 = "+${inputState.countryCode}${inputState.nationalNumber}"
var state = inputState.copy()
var state = inputState.withNormalizedNationalNumber()
val e164 = "+${state.countryCode}${state.nationalNumber}"
// If the user selected a restore option before entering their phone number, navigate to the restore flow
if (state.pendingRestoreOption != null) {
@@ -891,6 +892,34 @@ class PhoneNumberEntryViewModel(
return (new.length - prefix - suffix).coerceAtLeast(0)
}
/**
* Strips a redundant national trunk prefix from the national number (e.g. the leading 0 a Dutch user habitually
* types in "0612345678"), which would otherwise produce a wrong E164 when naively concatenated with the country
* code (+310612345678 instead of +31612345678). Leading zeros that are a significant part of the number (e.g.
* Italian landlines) are preserved.
*/
private fun PhoneNumberEntryState.withNormalizedNationalNumber(): PhoneNumberEntryState {
if (countryCode.isEmpty() || nationalNumber.isEmpty()) {
return this
}
val parsedNumber = try {
phoneNumberUtil.parse("+$countryCode$nationalNumber", null)
} catch (_: NumberParseException) {
return this
}
val significantNumber = phoneNumberUtil.getNationalSignificantNumber(parsedNumber)
if (significantNumber == nationalNumber || parsedNumber.countryCode.toString() != countryCode) {
return this
}
return copy(
nationalNumber = significantNumber,
formattedNumber = formatNumber(significantNumber)
).withNumberValidity()
}
/**
* Recomputes [PhoneNumberEntryState.isNumberPossible] and [PhoneNumberEntryState.isNumberInvalid] from the current
* country code and national number. Should be applied to any state that changes either of those fields.
@@ -485,6 +485,73 @@ class PhoneNumberEntryViewModelTest {
assertThat(state.nationalNumber).isEqualTo("15123456789")
}
// ==================== Trunk Prefix Normalization Tests ====================
@Test
fun `NextClicked strips a redundant leading trunk prefix before showing the confirmation dialog`() = runTest {
// Dutch users habitually type their number with the leading national '0' (e.g. 0612345678), which must not end
// up in the E164 (+31612345678, not +310612345678).
val initialState = PhoneNumberEntryState(regionCode = "NL", countryCode = "31", nationalNumber = "0612345678", formattedNumber = "06 12345678")
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.NextClicked, parentEventEmitter, stateEmitter)
assertThat(emittedStates).hasSize(1)
val result = emittedStates.last()
assertThat(result.nationalNumber).isEqualTo("612345678")
assertThat(result.countryCode).isEqualTo("31")
assertThat(result.isNumberPossible).isTrue()
assertThat(result.dialogs.confirmNumber).isTrue()
}
@Test
fun `NextClicked leaves a number without a trunk prefix unchanged`() = runTest {
val initialState = PhoneNumberEntryState(regionCode = "US", countryCode = "1", nationalNumber = "5551234567", formattedNumber = "(555) 123-4567")
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.NextClicked, parentEventEmitter, stateEmitter)
assertThat(emittedStates).hasSize(1)
val result = emittedStates.last()
assertThat(result.nationalNumber).isEqualTo("5551234567")
assertThat(result.formattedNumber).isEqualTo("(555) 123-4567")
assertThat(result.dialogs.confirmNumber).isTrue()
}
@Test
fun `NextClicked preserves a leading zero that is a significant part of the number`() = runTest {
// Italian landlines include the leading zero as part of the number itself, so it must not be stripped.
val initialState = PhoneNumberEntryState(regionCode = "IT", countryCode = "39", nationalNumber = "0612345678", formattedNumber = "06 1234 5678")
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.NextClicked, parentEventEmitter, stateEmitter)
assertThat(emittedStates).hasSize(1)
val result = emittedStates.last()
assertThat(result.nationalNumber).isEqualTo("0612345678")
assertThat(result.dialogs.confirmNumber).isTrue()
}
@Test
fun `PhoneNumberConfirmed submits the E164 without a redundant leading trunk prefix`() = runTest {
val sessionMetadata = createSessionMetadata(requestedInformation = emptyList())
coEvery { mockRepository.createSession(any()) } returns
RequestResult.Success(sessionMetadata)
coEvery { mockRepository.requestVerificationCode(any(), any(), any()) } returns
RequestResult.Success(sessionMetadata)
val initialState = PhoneNumberEntryState(
regionCode = "NL",
countryCode = "31",
nationalNumber = "0612345678"
)
viewModel.applyEvent(initialState, PhoneNumberEntryScreenEvents.PhoneNumberConfirmed, parentEventEmitter, stateEmitter)
coVerify(exactly = 1) { mockRepository.createSession("+31612345678") }
assertThat(emittedStates.last().nationalNumber).isEqualTo("612345678")
assertThat(emittedEvents.filterIsInstance<RegistrationFlowEvent.E164Chosen>())
.isEqualTo(listOf(RegistrationFlowEvent.E164Chosen("+31612345678")))
}
// ==================== FullPhoneNumberEntered Tests ====================
@Test