Update country selection.

This commit is contained in:
Michelle Tang
2026-04-13 18:37:06 -04:00
committed by jeffrey-signal
parent 78940ffc17
commit 7e4736969c
7 changed files with 84 additions and 23 deletions
@@ -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<SvrCredentials> = withContext(Dispatchers.IO) {
val data = storageController.readInProgressRegistrationData()
data.svrCredentials.map { SvrCredentials(username = it.username, password = it.password) }
@@ -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 {
""
}
}
}
@@ -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))
@@ -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,
@@ -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
@@ -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()
@@ -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"
}
}