From 7e4736969c96fe1d7cf81c4bc73d9ee8b70bee86 Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Mon, 13 Apr 2026 18:37:06 -0400 Subject: [PATCH] Update country selection. --- .../registration/RegistrationRepository.kt | 13 ++++++++++ .../screens/countrycode/CountryUtils.kt | 22 ++++++++++++++++ .../phonenumber/PhoneNumberEntryScreen.kt | 26 ++++++++++++++----- .../phonenumber/PhoneNumberEntryState.kt | 8 +++--- .../phonenumber/PhoneNumberEntryViewModel.kt | 19 ++++++++++++++ .../PhoneNumberEntryViewModelTest.kt | 13 +++------- .../phonenumber/PhoneNumberScreenTest.kt | 6 ++--- 7 files changed, 84 insertions(+), 23 deletions(-) create mode 100644 feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryUtils.kt diff --git a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt index 6c81654784..6890ac8116 100644 --- a/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt +++ b/feature/registration/src/main/java/org/signal/registration/RegistrationRepository.kt @@ -8,6 +8,7 @@ package org.signal.registration import android.app.backup.BackupManager import android.content.Context import android.net.Uri +import com.google.i18n.phonenumbers.PhoneNumberUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext @@ -17,6 +18,7 @@ import org.signal.archive.LocalBackupRestoreProgress import org.signal.core.models.AccountEntropyPool import org.signal.core.models.MasterKey import org.signal.core.util.Base64 +import org.signal.core.util.Util import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.libsignal.protocol.IdentityKeyPair @@ -128,6 +130,17 @@ class RegistrationRepository(val context: Context, val networkController: Networ } } + fun getDefaultRegionCode(): String { + val maybeRegionCode = Util.getNetworkCountryIso(context) + val maybeCountryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(maybeRegionCode) + return if (maybeRegionCode != null && maybeCountryCode != 0) { + maybeRegionCode + } else { + Log.w(TAG, "Invalid region or country code. Defaulting to US.") + "US" + } + } + suspend fun getRestoredSvrCredentials(): List = withContext(Dispatchers.IO) { val data = storageController.readInProgressRegistrationData() data.svrCredentials.map { SvrCredentials(username = it.username, password = it.password) } diff --git a/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryUtils.kt b/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryUtils.kt new file mode 100644 index 0000000000..7190d1ccc3 --- /dev/null +++ b/feature/registration/src/main/java/org/signal/registration/screens/countrycode/CountryUtils.kt @@ -0,0 +1,22 @@ +package org.signal.registration.screens.countrycode + +import java.util.Locale + +/** + * Utility functions used when working with countries + */ +object CountryUtils { + + @JvmStatic + fun countryToEmoji(countryCode: String): String { + return if (countryCode.isNotEmpty()) { + countryCode + .uppercase(Locale.US) + .map { char -> Character.codePointAt("$char", 0) - 0x41 + 0x1F1E6 } + .map { codePoint -> Character.toChars(codePoint) } + .joinToString(separator = "") { charArray -> String(charArray) } + } else { + "" + } + } +} 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 d8948a6b56..096bebb051 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 @@ -38,6 +38,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.platform.testTag @@ -158,6 +160,7 @@ private fun ScreenContent(state: PhoneNumberEntryState, onEvent: (PhoneNumberEnt Spacer(modifier = Modifier.height(16.dp)) PhoneNumberInputFields( + hasValidCountry = state.countryName.isNotEmpty(), countryCode = state.countryCode, formattedNumber = state.formattedNumber, onCountryCodeChanged = { onEvent(PhoneNumberEntryScreenEvents.CountryCodeChanged(it)) }, @@ -221,15 +224,17 @@ private fun CountryPicker( .padding(start = 16.dp, end = 12.dp), verticalAlignment = Alignment.CenterVertically ) { - Text( - text = emoji, - fontSize = 24.sp - ) + if (emoji.isNotEmpty()) { + Text( + text = emoji, + fontSize = 24.sp + ) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(16.dp)) + } Text( - text = country, + text = country.takeIf { country.isNotEmpty() } ?: stringResource(R.string.RegistrationActivity_select_a_country), style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f) @@ -250,6 +255,7 @@ private fun CountryPicker( */ @Composable private fun PhoneNumberInputFields( + hasValidCountry: Boolean, countryCode: String, formattedNumber: String, onCountryCodeChanged: (String) -> Unit, @@ -258,6 +264,7 @@ private fun PhoneNumberInputFields( modifier: Modifier = Modifier ) { var phoneNumberTextFieldValue by remember { mutableStateOf(TextFieldValue(formattedNumber)) } + val focusRequester = remember { FocusRequester() } LaunchedEffect(formattedNumber) { if (phoneNumberTextFieldValue.text != formattedNumber) { @@ -284,6 +291,12 @@ private fun PhoneNumberInputFields( } } + LaunchedEffect(hasValidCountry) { + if (hasValidCountry) { + focusRequester.requestFocus() + } + } + Row( modifier = modifier, horizontalArrangement = Arrangement.Start, @@ -326,6 +339,7 @@ private fun PhoneNumberInputFields( }, modifier = Modifier .weight(1f) + .focusRequester(focusRequester) .testTag(TestTags.PHONE_NUMBER_PHONE_FIELD), label = { Text(stringResource(R.string.RegistrationActivity_phone_number_description)) diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt index dcc9ccbc2c..f76202ae86 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryState.kt @@ -14,10 +14,10 @@ import org.signal.registration.util.DebugLoggableModel import kotlin.time.Duration data class PhoneNumberEntryState( - val regionCode: String = "US", - val countryCode: String = "1", - val countryName: String = "United States", - val countryEmoji: String = "\uD83C\uDDFA\uD83C\uDDF8", + val regionCode: String = "", + val countryCode: String = "", + val countryName: String = "", + val countryEmoji: String = "", val nationalNumber: String = "", val formattedNumber: String = "", val sessionE164: String? = null, diff --git a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt index bb8588b308..41aa90140f 100644 --- a/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt +++ b/feature/registration/src/main/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModel.kt @@ -17,9 +17,11 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withTimeoutOrNull import org.signal.core.models.AccountEntropyPool +import org.signal.core.util.E164Util import org.signal.core.util.logging.Log import org.signal.libsignal.net.RequestResult import org.signal.registration.NetworkController @@ -29,6 +31,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.countrycode.CountryUtils import org.signal.registration.screens.localbackuprestore.LocalBackupRestoreResult import org.signal.registration.screens.phonenumber.PhoneNumberEntryState.OneTimeEvent import org.signal.registration.screens.util.navigateTo @@ -58,6 +61,20 @@ class PhoneNumberEntryViewModel( _state.value = state.value.copy( restoredSvrCredentials = repository.getRestoredSvrCredentials() ) + setDefaultCountry() + } + } + + fun setDefaultCountry() { + val regionCode = repository.getDefaultRegionCode() + formatter = phoneNumberUtil.getAsYouTypeFormatter(regionCode) + _state.update { + it.copy( + regionCode = regionCode, + countryName = E164Util.getRegionDisplayName(regionCode).orElse(""), + countryEmoji = CountryUtils.countryToEmoji(regionCode), + countryCode = PhoneNumberUtil.getInstance().getCountryCodeForRegion(regionCode).toString() + ) } } @@ -156,6 +173,8 @@ class PhoneNumberEntryViewModel( val formattedNumber = formatNumber(state.nationalNumber) return state.copy( + countryName = E164Util.getRegionDisplayName(regionCode).orElse(""), + countryEmoji = CountryUtils.countryToEmoji(regionCode).takeIf { regionCode != "ZZ" } ?: "", countryCode = sanitized, regionCode = regionCode, formattedNumber = formattedNumber diff --git a/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModelTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModelTest.kt index 0bce171d76..796047b44a 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModelTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberEntryViewModelTest.kt @@ -17,6 +17,7 @@ import assertk.assertions.isTrue import assertk.assertions.prop import io.mockk.coEvery import io.mockk.coVerify +import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runTest @@ -46,6 +47,8 @@ class PhoneNumberEntryViewModelTest { @Before fun setup() { mockRepository = mockk(relaxed = true) + every { mockRepository.getDefaultRegionCode() } returns "US" + parentState = MutableStateFlow(RegistrationFlowState()) emittedStates = mutableListOf() stateEmitter = { state -> emittedStates.add(state) } @@ -54,16 +57,6 @@ class PhoneNumberEntryViewModelTest { viewModel = PhoneNumberEntryViewModel(mockRepository, parentState, parentEventEmitter) } - @Test - fun `initial state has default US region and country code`() { - val state = PhoneNumberEntryState() - - assertThat(state.regionCode).isEqualTo("US") - assertThat(state.countryCode).isEqualTo("1") - assertThat(state.nationalNumber).isEqualTo("") - assertThat(state.formattedNumber).isEqualTo("") - } - @Test fun `PhoneNumberChanged extracts digits and formats number`() = runTest { val initialState = PhoneNumberEntryState() diff --git a/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberScreenTest.kt b/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberScreenTest.kt index ed460d06af..cc589ed414 100644 --- a/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberScreenTest.kt +++ b/feature/registration/src/test/java/org/signal/registration/screens/phonenumber/PhoneNumberScreenTest.kt @@ -71,7 +71,7 @@ class PhoneNumberScreenTest { } @Test - fun `when Next is clicked, PhoneNumberSubmitted event is emitted`() { + fun `when Next is clicked, PhoneNumberEntered event is emitted`() { // Given var emittedEvent: PhoneNumberEntryScreenEvents? = null @@ -94,8 +94,8 @@ class PhoneNumberScreenTest { composeTestRule.onNodeWithTag(TestTags.PHONE_NUMBER_NEXT_BUTTON).performClick() // Then - assert(emittedEvent is PhoneNumberEntryScreenEvents.PhoneNumberSubmitted) { - "Expected PhoneNumberSubmitted event but got $emittedEvent" + assert(emittedEvent is PhoneNumberEntryScreenEvents.PhoneNumberEntered) { + "Expected PhoneNumberEntered event but got $emittedEvent" } }